fix(deploy): the rollback guard never fired, and an aborted run rotated the cluster's secrets #114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/stack-yaml-describes-reality"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Three defects in the standalone deploy path, each found by the previous one.
No product code changes —
infra/k8s/standalone/deploy.shandstack.yaml.1. The rollback guard has never fired
deploy.shcarries a guard written after the user lost a day to a silentimage rollback ("tests passed against an image they never saw"). It greps
chromeless/chromeless:<tag>;stack.yamlpinschromeless/chromeless@sha256:<digest>.So
pinned_workerwas empty and the guard's own[ -n "$pinned_worker" ]test skipped the whole check — silently, since digest pinning landed.
Now matches both forms, and exits if it finds no pin at all: a guard that
cannot determine what it is guarding must not pass.
2. No guard on the gateway, which had drifted
Live
sha256:63d6cfa7vs the pinned9ef7f4db, three hand-rolls behind.Genuinely different images — 13 layers each, 5654728 vs 5609791 bytes,
different top layer. The gateway serves the client bundle, so a silent
downgrade changes what every viewer runs. Same guard, same
CHROMELESS_ALLOW_ROLLBACK=1override.I did not pin the live digest, though that was the obvious fix. No tag in
the registry points at it and no commit records it — it was pushed untagged.
A pin is a provenance claim; writing one for an image whose source nobody can
name would make
stack.yamllook authoritative while being unverifiable.Recorded as a DRIFT note beside the pin with what it would take to resolve,
and the guard blocks the apply until someone does.
3. An aborted run was not a no-op — it locked me out of the cluster
The gateway guard fired, correctly, and that exposed the real bug: the guards
sat after the secrets step, so a refused run had already rotated the Ed25519
keypair and the login password into the Secret.
hammered it with
auth rejectedabout once a second;secretKeyRefisinjected at pod start, not re-read — so the new password did not work either;
.standalone-credsis at the end of the script and never ran.Both guards now run before anything touches the cluster. Verified by
measurement: captured the keypair and password, ran the script, confirmed it
refused, confirmed both byte-identical after. Before the reorder the same
abort changed both.
What I got wrong on the way
My first commit said a bare
kubectl apply"strips" undeclared env. It doesnot, and the difference decides what the fix can be. Measured on a scratch
Deployment:
Apply prunes via three-way merge against
last-applied-configuration— itremoves what a previous apply claimed, not everything the current manifest
omits. An older
stack.yamldeclared the TURN vars; that is why omitting themdeleted them. Confirmed:
last-appliedon both live deployments no longernames them.
That ruled out the repair I was about to make. Declaring the TURN vars with
placeholder values makes apply overwrite what
deploy.shsets (measured —the placeholder wins), trading a silent drop for a silent wrong value, which is
worse because the stack then looks configured. And the credential is
HMAC-derived with a 24 h expiry, so it cannot live in a static manifest
regardless.
kubectl set envstays the owner; what was missing was anyonesaying so, which
deploy.shnow does — it lists every live env varstack.yamldoes not declare before applying (checked against the cluster:reports all 8).
Verification
make verify/make lintcoturn on triform-7, dialled 3478, reachableice=connected, 128 frames, 15284 audio bytesAlso replaces
deploy.sh's hardcodedTURN_IP=95.217.200.179, which refuses3478 — coturn had moved — with discovery that dials the relay before using it.
Selecting by the
app=label, becausecoturn-2matches every name-prefixpattern for
coturn(2is a hex digit) and two earlier attempts returned aplausible-but-wrong node.
Three defects in the apply path, found while cleaning up after a bare `kubectl apply -f stack.yaml` broke a working stack. 1. THE GUARD NEVER FIRED. It greps `chromeless/chromeless:<tag>`, and stack.yaml pins `chromeless/chromeless@sha256:<digest>`. So pinned_worker was empty, and the guard's own `[ -n "$pinned_worker" ]` test skipped the whole check — silently. A protection written after the user lost a day to exactly this ("tests passed against an image they never saw") has not run once since digest pinning. Now matches both forms and EXITS if it finds no pin at all: a guard that cannot determine what it is guarding must not pass. 2. NO GUARD ON THE GATEWAY, which has drifted the same way — live sha256:63d6cfa7 vs the pinned 9ef7f4db, three hand-rolls behind. Verified different images (13 layers each, 5654728 vs 5609791 bytes, different top layer). The gateway serves the client bundle, so a silent downgrade changes what every viewer runs. Same guard, same override. 3. AN APPLY DROPS ENV NOBODY DECLARED. stack.yaml does not declare the TURN settings; this script sets them afterwards, so `deploy.sh` is fine and a bare apply is not — it strips them and leaves a stack that connects, negotiates SDP and shows no video. The guest falls back to stun.l.google.com, the client gathers one .local candidate, and tests/interactive reports "the worker already served a session", which is not what happened. Cost two verification runs today. deploy.sh now lists every live env var stack.yaml does not declare before applying. Checked against the cluster: reports all 8. I did NOT pin the live gateway digest, though that was the obvious fix. No tag in the registry points at it and no commit records it — it was pushed untagged. A pin is a provenance claim, and writing one for an image whose source nobody can name would make stack.yaml look authoritative while being unverifiable. Recorded as a DRIFT note beside the pin, with what it would take to resolve, and the new guard blocks the apply until someone does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>