From 6a4dc0ada76ce94b86b1ec0215db71b061ed9c6e Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 18 Sep 2026 21:39:42 -0500 Subject: [PATCH] =?UTF-8?q?docs:=20INCIDENT=20=E2=80=94=20I=20printed=20th?= =?UTF-8?q?e=20console=20password=20to=20the=20transcript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A kubectl jsonpath with {range .data}{@}{end} dumped the unifi Secret's data map, putting the base64 console-password in the session transcript. Exactly the constraint I put in every agent brief, broken by my own careless verification command. Exposure bounded: 0 commits contain it (git log -S), the capturing scratch file was shredded, and a repo-wide grep finds nothing on disk. Critically it is not yet a live credential — the Secret was created two days after the running VMI, whose volume list is rootdisk and cloudinit only, so the UOSDATA disk was never attached, install-uos never ran, and the ubuntu account is still locked. Action recorded with the ordering that matters: rotate console-password BEFORE recreating the VMI, so the leaked value never becomes valid. Lesson: checking a Secret's existence never needs its values. Listing .data's keys or .metadata.name answers the question; dumping .data answers one nobody asked. Co-Authored-By: Claude Opus 5 --- DECISIONS-LOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/DECISIONS-LOG.md b/DECISIONS-LOG.md index c961d04..689df16 100644 --- a/DECISIONS-LOG.md +++ b/DECISIONS-LOG.md @@ -4229,3 +4229,39 @@ both fields as literal `REPLACE-ME`, unlike every other `*.sops.yaml` here. `sop failed with "sops metadata not found", and Flux would have created the Secret containing the placeholder strings. Encrypted in place, round-trip verified. Both fields need setting, not just the password — I had omitted `installer-url` from the operator instructions. + +--- + +## 2026-09-18 — INCIDENT (mine): I printed the console password to the transcript + +**What happened.** Verifying that the `unifi-uos-installer` Secret had reached the cluster, I ran a +`kubectl get secret -o jsonpath` that included `{range .data}{@}{end}`. That dumps the whole `data` +map, so the base64 of `console-password` was printed to the session transcript. This is precisely the +constraint I have written into every agent brief in this repo — *never print decrypted secrets* — and I +broke it myself with a careless jsonpath while checking someone else's work. + +**Exposure, stated precisely rather than minimised or inflated:** +- The value reached the session transcript, which I cannot scrub. +- It did **not** reach git: `git log --all -S` returns **0 commits**. +- The scratch file that captured it (`tasks/*.output`) was shredded; a repo- and scratch-wide grep for + the string now returns nothing on disk. + +**Why it is not yet a working credential — the one thing that limits this.** The Secret was created +`2026-09-14T07:26:43Z`; the running VMI was created `2026-09-12T20:48:30Z`, and its live volume list is +only `rootdisk` and `cloudinit` — **the `UOSDATA` disk is not attached to the running VMI at all**. So +the guest has never read the Secret, `install-uos` has never executed, and the `ubuntu` account remains +locked. The leaked string authenticates nothing today. It would only become valid when the VMI is +recreated against the new spec. + +**Required action, and the ordering matters:** rotate `console-password` in the SOPS Secret **before** +recreating the VMI. Then the leaked value never becomes a working password anywhere: + + sops apps/vms/unifi/unifi-uos-installer.sops.yaml # set a NEW console-password + git commit && git push # let Flux update the Secret + kubectl -n unifi delete vmi unifi # only after the rotation has landed + +**The lesson, generalised.** Reading a Secret to check *existence* never requires reading its +*values*. `-o jsonpath='{.metadata.name}'` or listing `.data`'s KEYS answers "did it land?"; dumping +`.data` answers a question nobody asked. Every previous secret check in this session used a +keys-only or boolean form — this one regressed because I appended a convenience field to an +otherwise-safe command without re-reading what it expanded to.