carlos / platform Public

Bind release pointers and secret envelopes to their account; close relay deny-list gaps Issue open

On branch tenant-iso-binding

Claimed by claude~paul@keymail.dev 3 weeks ago

Found by the 2026-10-03 tenant-isolation audit of main 78fc8d40, by reading code. None of these is reachable alone today; each would turn a second bug into a cross-tenant one.

  1. Channel pointers are not bound to the account. Verification checks app and channel but not the account (release/release.go:393,452), so a signed pointer for A's blog also verifies at B's blog.
  2. Secret envelopes have no associated data binding them to (acct, app) (appconfig/crypto.go:109). One deployment-wide CARLOS_SECRETS_KEY is on every host (host/agent.go:882). Open question for pinfra: do customer fleet boxes receive that key? If so, a customer holds the platform-wide decryption key.
  3. The relay's peer deny list has gaps (relay/server.go:201): 0.0.0.0/8 beyond the unspecified address, 192.0.0.0/24, 198.18.0.0/15, 64:ff9b::/96 (NAT64) and 2002::/16 (6to4).

Fixes for 1–3 are going on a branch from this session. Any wire change will be additive: old pointers and envelopes must still verify.

Follow-up from branch tenant-iso-binding (now at d2d074a2), which needs tracking:

v2 (bound) secret envelopes are read by every box with this build, but only written once CONSOLE_SECRETS_ENVELOPE_V2=true is set on the console (or in the operator's environment for direct-bucket carlos secrets set). Nothing enforces that the flag ever gets set. Until it is, every new secret is still a v1 envelope, and v1 envelopes can be copied across accounts and still open.

So two steps need owners:

  1. Rollout: roll every box to a build with this change, then set CONSOLE_SECRETS_ENVELOPE_V2=true on each deployment's console (flagship, dev, Tito). Then re-set existing secrets so they are re-sealed as v2.
  2. Sunset: once no stored envelope is v1, change appconfig.Open to refuse v1. This needs an inventory first, for example a console count of v1 envelopes per app.

Channel pointers have the matching residual, account-less pointers written before this change. Sunset there is to require account in validatePointer once no live pointer lacks one.

Merged to main as b94ad23. The v1 envelope and account-less pointer sunset stays open here, as tracked above: roll the boxes, set CONSOLESECRETSENVELOPE_V2 on each console, re-set existing secrets, then refuse v1.

Tracked under #247 (tenant isolation before Carloku takes outside tenants).