The operator's UOS bring-up becomes one field. apps/vms/unifi/unifi-uos-installer.sops.yaml
(SOPS/age, with a .example sibling per the crafty/headlamp convention) carries `installer-url`
and `console-password`. KubeVirt mounts it as a read-only iso9660 disk labelled UOSDATA and an
in-guest `uos-install.service` reads it on EVERY boot, so a later URL change needs only a commit
and a VM restart.
Mechanism verified against the DEPLOYED KubeVirt v1.9.0, not from memory:
* inline userData cap is exactly 2048 bytes — server-side dry-run apply accepted 2048 and
rejected 2049; the payload here is 1454.
* `cloudInitNoCloud.secretRef` and `secret.volumeLabel` both exist in the live CRD; the secret
disk + third disk were accepted by a server-side dry-run of the real manifest.
* the NoCloud seed's instance-id IS spec.template.spec.domain.firmware.uuid (read out of
noCloud.iso inside virt-launcher), stamped on the VM OBJECT, so it survives restarts. Editing
userData alone therefore changes NOTHING — cloud-init restores its cached datasource and skips
write_files/runcmd. The UUID is now pinned in git and bumped once so the new payload lands.
secretRef was deliberately NOT used for the whole cloud-config: it would put the shell in
ciphertext, out of reach of the L0 dollar-collapse guard, while Flux's envsubst still runs over
decrypted resources — the 2026-09-13 nextcloud-db shape. Script in plaintext, operator values in
SOPS. apps-vms had no `decryption` stanza (first encrypted content under apps/vms/), so one was
added to both overlays.
CORRECTION, and it is the headline: UNIFI OS SERVER IS ALREADY INSTALLED on the live guest. The
serial console log starts uosserver.service and uosserver-updater.service, and / is 6.3 GB of
40 GB used. The controller is unreachable because THE GUEST HAS NO IP: .status.interfaces shows
enp1s0 up with no address, and `ip neigh` in virt-launcher reports 10.0.2.2 FAILED. 11443/8443/
8080 and 22 all RST, from the pod IP and from 10.0.2.2 directly, so the VIP and the CNP are
exonerated. install-uos therefore bails out the moment systemd knows a uosserver.service —
without that guard this change would have reinstalled over a working controller.
There was also no way into the guest at all: no key, no password, and 22 is not a declared
masquerade port so SSH cannot work by construction. Hence console-password.
README corrections (it doubles as the rebuild runbook and was misdirecting):
* VIP 192.168.1.54 -> 192.168.2.21; pool 192.168.1.53-.60/.53 -> 192.168.2.20-.30 with the
Gateway on .20; device subnet 192.168.1.0/24 -> 192.168.2.0/24; node k3s-dev-1 -> k3s-prod-1.
All re-read live (pool blocks, cluster-vars, the Service EXTERNAL-IPs, LAN_CIDR).
* "UOS is NOT installed" -> it is; the real fault is the lost IP, with the evidence table.
* "apps-vms must NOT use postBuild.substituteFrom" -> it has since 2026-08-31 and the escape
contract is in force.
* "install-uos is no longer baked into the guest" -> it was baked in, and it was broken twice
over (an unverified firmware API, and no `install` subcommand).
* "SSH in, cloud-init installs the operator's key" -> it installs none, and 22 is not forwarded.
* externalTrafficPolicy Local -> Cluster (the 8-day outage fix already in 50-service-l4.yaml).
* 60 GB -> 40Gi (the DataVolume and the Bound PVC); "10 GB minimum" -> ~25 GB.
* userDataSecretRef -> the YAML key is `secretRef`.
* 90-httproute-optional.yaml referenced as if it existed; it was dropped.
Unattended install, honestly: Ubiquiti documents no silent flag, no answer file, no env vars.
`yes y | <installer> install` is the convergent community practice and is what ships here, with a
bare-invocation fallback. It has NOT been run end-to-end on our guest. The installer URL also
does not look signed or time-limited — it is account-derived and per-release; it is in SOPS
anyway, but nothing should depend on expiry.
The running VM was not touched: no restart, no migration, no re-import. Every cluster call was a
read or an explicit --dry-run=server; the live VM still carries firmware uuid ...c07 and the
34h-old virt-launcher pod is untouched. The change takes effect on the operator's next VM restart.
Co-Authored-By: Claude Opus 5 <[email protected]>
72 lines
4.8 KiB
Plaintext
72 lines
4.8 KiB
Plaintext
# =============================================================================
|
|
# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize and never
|
|
# auto-encrypted by SOPS). Reference scaffold + OPERATOR HAND-OFF.
|
|
#
|
|
# WHAT THIS IS: the two values that (a) let the UniFi VM install UniFi OS Server by itself, with
|
|
# nobody typing commands inside the guest, and (b) give the operator a way INTO that guest at all.
|
|
# KubeVirt mounts this Secret into the VM as a tiny read-only iso9660 disk labelled `UOSDATA` (see
|
|
# the `uosdata` volume in 30-virtualmachine.yaml); each key below becomes a file of the same name on
|
|
# that disk, and the in-guest `uos-install.service` reads them on EVERY boot.
|
|
#
|
|
# READ THIS FIRST — UOS IS ALREADY INSTALLED ON THE LIVE PROD GUEST (verified 2026-09-14, see the
|
|
# README "What is actually wrong"). `installer-url` is therefore NOT the fix for today's outage; it
|
|
# is the rebuild path and the "the install never happened" path. `install-uos` refuses to run the
|
|
# installer at all while systemd knows a `uosserver.service`, so filling this in cannot clobber a
|
|
# working controller. `console-password` IS useful today: it is the only way to get a shell.
|
|
#
|
|
# WHY THE URL IS BEHIND SOPS: it is obtained through the operator's own (free) Ubiquiti account
|
|
# session at ui.com/download/software/unifi-os-server, it identifies a specific build, and it is not
|
|
# ours to redistribute. Treat it as account-derived material, not as public configuration.
|
|
# HONEST CAVEAT, checked 2026-09-14: the resulting `https://fw-download.ubnt.com/data/
|
|
# unifi-os-server/<hash>-linux-x64-<version>-<uuid>.<n>-x64` URL carries NO signature query string,
|
|
# and public community install scripts hard-code these URLs — so it does not behave like a
|
|
# short-lived signed link. It does change every release. Encrypting it costs nothing and keeps the
|
|
# repo's "account-derived material is not config" line clean, but do NOT design around "it expires".
|
|
#
|
|
# HAND-OFF STEPS (operator) — this is the whole job:
|
|
# (a) Sign in at https://ui.com/download/software/unifi-os-server, pick the Linux (x64) build, and
|
|
# copy the download link (right-click -> Copy link address). Do NOT download it here; the guest
|
|
# fetches it itself.
|
|
# (b) Open the encrypted sibling in your editor:
|
|
# sops apps/vms/unifi/unifi-uos-installer.sops.yaml
|
|
# (c) Replace the two REPLACE-ME values. Either may be left as the placeholder — the guest skips a
|
|
# placeholder rather than failing.
|
|
# (d) Save/quit (sops re-encrypts in place), commit, push, merge to the branch prod tracks.
|
|
# (e) Restart the VM once so the new cloud-init instance and the new disk are picked up:
|
|
# kubectl -n unifi delete vmi unifi # `running: true` recreates it; no virtctl needed
|
|
# Watch it come up WITHOUT virtctl:
|
|
# kubectl -n unifi logs -f $(kubectl -n unifi get pod -l kubevirt.io=virt-launcher \
|
|
# -o name | head -1) -c guest-console-log
|
|
# (f) UI: https://192.168.2.21:11443 (prod) or https://unifi.home.keithjackson.dev:11443.
|
|
#
|
|
# IF YOU LATER NEED A NEW URL (new release, dead link): repeat (b)-(e). No firmware-UUID bump is
|
|
# needed for that — the disk is rebuilt from this Secret on every VMI start and the systemd unit
|
|
# re-reads it every boot. The UUID bump in 30-virtualmachine.yaml is only needed when the cloud-init
|
|
# payload ITSELF changes.
|
|
#
|
|
# The file is already listed under `resources:` in apps/vms/unifi/kustomization.yaml, and `apps-vms`
|
|
# is patched with the SOPS `decryption` stanza in clusters/dev + clusters/prod.
|
|
# =============================================================================
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: unifi-uos-installer
|
|
namespace: unifi
|
|
type: Opaque
|
|
stringData:
|
|
# KEY NAMES ARE LOAD-BEARING: each key becomes a FILE of that exact name on the UOSDATA disk, and
|
|
# /usr/local/sbin/install-uos reads them by path. Renaming a key breaks the install silently.
|
|
#
|
|
# The signed-in download link for UniFi OS Server, Linux x64. Anything that is not an https URL
|
|
# (this placeholder included) is treated by the guest as "not provided yet": the unit logs
|
|
# "UOS installer-url not set yet" and exits 0, so the VM stays healthy and simply does not install.
|
|
installer-url: REPLACE-ME
|
|
# Password set on the guest's `ubuntu` account on every boot, for `virtctl -n unifi console unifi`.
|
|
# THIS IS THE ONLY DOOR INTO THE GUEST. The stock Ubuntu 24.04 cloud image ships `ubuntu` locked,
|
|
# no key was ever installed, and an SSH key would not help: the VM's masquerade interface forwards
|
|
# only the ports declared in 30-virtualmachine.yaml and 22 is not among them (probed from inside
|
|
# virt-launcher, 2026-09-14 — the guest RSTs on 22 just as it does on 11443). Generate one with
|
|
# openssl rand -base64 24
|
|
# Leave it as REPLACE-ME to keep the account locked; the script skips the placeholder by name.
|
|
console-password: REPLACE-ME
|