Skip to main content
Date: 2026-09-06/07. Branch under test: feat/credential-claim-code at c52ac273. Where: the bbb build host — the throwaway edge, the publisher and the cluster all on it, nothing on a laptop and nothing on any live host. Why now: the founder’s rule for this feature — “we need to test the 6 digits flow before” any subscriber is read a code. Nothing real was touched. No account was minted on the channel registry; no vaulted secret was decrypted; the age key pair was minted for this run and shredded at the end; the ledger was a scratch checkout of the vexa-stations shape, never the ledger; and the live edge is still not deployed. The parked credential was a made-up rehearsal:… account. The two codes appear in full below — they are spent, and they unlocked a password this rehearsal invented and then destroyed.

The rig

Two deviations from the deployed shape, both stated because they change what the run proves:
  1. No TLS and no Caddy. The edge was reached over plain HTTP on loopback and on the cluster network, so CLAIM_TRUST_FORWARDED_FOR stayed off — correctly, since nothing trustworthy sets that header here. The consequence is that the “source” the limiter counts was the docker peer address, not a subscriber’s real one. Source separation is proven; source attribution behind a proxy is not.
  2. The container ran as the host user, not the image’s 65532. The publisher and the edge shared one spool directory on one machine; in production the publisher scps into a spool the edge owns. Nothing in the state machine depends on the uid.
And one obstacle worth recording: bbb could not pull the pinned base image from Docker Hub — anonymous rate limit, 429. The image was built from the identical digest fetched through the rig’s own pull-through cache, using BuildKit’s named-context override; edge/claim/Dockerfile was not edited.

1 · Park — the publisher’s own add --park

Run as publisher/vexa_subscriber.py add rehearsal --park --station bbb-rehearsal --channel rehearsal --edge http://127.0.0.1:18089/claim …, with a stub kubectl first on PATH that answers the three calls the mint makes with a fixture and refuses every other call, so the real registry was never contacted. The publisher’s own receipt, verbatim:
Line 1 of stdout was the credential (the vault line, printed once by design); line 2 was 165 312 — six digits, grouped, for reading aloud. The park file carried the ciphertext and a per-park code_salt; the ledger row carried the salted digest, the expiry, the attempt cap and who parked it, and nothing else.

2 · Claim — into the tenant namespace, value never on a screen

The Secret was written Opaque, keys username/password, labelled app.kubernetes.io/name=vexa-station. Read back:
The spool then held no ciphertext — the park file was deleted at redemption, and the state file left behind reads state=redeemed attempts=0/5 terminal_reason='claimed'.

3 · The three refusals

The wire is the point: every refusal is the same status and the same body, and the eleventh is indistinguishable from the first. The distinction exists only in attempts.ndjson:
The eleven came from inside the cluster (a different peer address, hence a different limiter bucket), which is also how the edge’s reachability from the cluster was proven. The burn was run from the host shell and the two buckets did not touch each other. Not exercised live: expiry. Both parks went terminal by redemption or burn inside their fifteen minutes; expired is covered by the branch’s tests only.

4 · The return leg

Re-ingesting the same log is a genuine no-op. The station’s credential-events.yaml ends up holding two park rows and seven claim rows — park id, salted digest, expiry, cap, source, outcome, time — and no value, no code, no salt, no ciphertext.

The absence assertion

Eighteen files — every transcript this rehearsal captured, the edge container’s own stdout/stderr, the attempts log, both state files, the ledger working tree and the ledger’s full git log -p — were searched for both minted passwords, verbatim and base64:
Two surfaces are excluded, by design and named here rather than quietly: the Secret in the cluster, which is where the value is supposed to be, and the publisher’s line-1 vault print, which is the one deliberate emission — captured to a 0600 file, used only as the needle for this search, and shredded with the age key. The edge’s access log carries a source, a method and a path and never a body.

Findings

1 · kit/claim.sh is committed non-executable (100644). Every other kit script is 100755. The documented ./kit/claim.sh --code … fails with Permission denied out of a fresh clone — the first command a subscriber runs on this path. Invoked through bash here. One chmod +x on the branch. 2 · A rate-limited attempt never reaches the station’s record. The per-source limiter fires before the body is parsed — correct, and the reason a refused request cannot cost a subscriber one of their five — but the station is therefore unknown, the event is written with station -, and record-credential drops it as unattributed. So the one event class that means somebody hammered this station from one address is the one class the ledger cannot show. The per-park limiter (park-rate-limited) is checked after the parse and is attributed normally. 3 · Identical attempts inside the same second collapse to one row. Nineteen attributable events reduced to seven rows: ten identical no-park attempts at 19:52:18Z became one. Dedup-by-content is what makes re-ingest a no-op, and the timestamps are second-resolution, so a burst is indistinguishable from a single request in the ledger. Nothing is wrong in the spool — attempts.ndjson has all twenty. None of the three changes the verdict on the flow itself: the six-digit code worked exactly as described, in the order described, and the credential reached the cluster without appearing anywhere a person or a log could read it.

Afterwards

The Secret was deleted, the edge container and its image removed, the age identity and every credential-bearing scratch file shredded, and the cluster left as it was found — same pods, same secrets, one namespace, untouched. The live edge remains undeployed and founder-owned.