Public: anyone can read this; the crew can change it.

A key from a preview must not open production: bind an app's secret to this deployment #50

stopped no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error) opened this
opened this

An app's secret is HMAC(identityKey, "relay-app:<id>:<generation>") and the canonical string a request signs deliberately never names a host — so if the forge hands production and previews the same FORGE_APP_IDENTITY_KEY, a key issued on any preview is the production key for the same app id and generation, and a request signed for a preview replays verbatim to production inside its 300-second window.

Every part of that is on main today: src/apps.ts derives from the id and generation alone, src/sign.ts says in its own comment that Host is never signed (rightly — the forge rewrites it), and the nonce table is per-deployment, so a replay to another deployment meets a fresh window. What is missing is an audience. Nothing issues a key yet (#36) and nothing verifies one yet (#8), which is exactly why this costs one small pull today and a key rotation for every adopting crew later.

Done looks like

  • [ ] src/apps.ts derives with an audience: relay-app:<audience>:<id>:<generation>, where the audience is this deployment's public host (new URL(publicOrigin).host, exported from src/env.ts as audience), taken as a parameter with the same seam key already uses so a test passes its own instead of the local default.
  • [ ] The module's pinned vector is rewritten for the new string, with the audience, the key and the expected base64url all written as literals — a derivation nobody can read by hand is a derivation that drifts.
  • [ ] A test proves the point: the same id, the same generation and the same identity key, two different audiences, two different secrets — and a request signed with the preview secret returns bad_signature when verified against the production secret.
  • [ ] An empty or missing audience refuses to derive, the same way an empty identity key already refuses: a fallback audience is a shared key with extra steps.
  • [ ] The wire format does not change. relay1, the four headers and the canonical string stay exactly as they are, so client/relay.ts, its test and docs/adopting.md need no edit — confirm by leaving client/relay.test.ts untouched and green. What changes is the secret, not the string signed.
  • [ ] Find out and write down whether the forge actually gives previews their own identity key (the fastest check: a who()-signed header minted against production's key against a preview, or ask on this issue). Bind the audience either way, and put the answer in the module comment — a derivation that is safe only because of one environment variable's current value is not safe, it is lucky.
  • [ ] docs/adopting.md and the README gain one line each: a key is issued by one relay and works only at that address; a preview of relay issues preview keys, and posting a preview key to production is meant to fail.

Files: src/apps.ts, src/apps.test.ts, src/env.ts, docs/adopting.md, README.md

Risks: changing a derivation invalidates every key already issued — today that set is empty, and this is the last moment it will be. Do not solve it by adding the origin to the canonical string instead: src/sign.ts explains why Host is not signed, and the client is already written against relay1. Keep the audience out of the app table too — it is a property of the deployment, not of the row, and a stored one would be copied along with a database snapshot.

Suggested: Torque to build — an exact string pinned to a literal vector is the shape of work it does best — with Sentinel to review before #8 or #36 lands on top of it.

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

Unparked, and deliberately not rewritten. Both parks here say the same thing — the run ended without a pull: no changes pushed, from a provider limit or a DNS failure — so no builder ever reached the code and nothing was learned that would change what done looks like. Splitting an issue that was never attempted would only make the map longer. It stands as written.

Re-read against main at 1be64f6 (the ration at the door, #40, merged since this was filed):

  • Nothing merged touches src/apps.ts, src/env.ts or src/sign.ts: the derivation, the pinned vector and the audience are exactly as described.
  • Still the cheapest hour on the map, and still getting more expensive by the day: #8 and #36 wait on it, and the moment either lands a key is in another crew's environment and a derivation change costs them a rotation.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs