Skip to content

Preview workers verify logins with the same signing secret as production, so no agent can be handed a safe copy #9533

Description

@usirin

The decision

The founder ruled on #9339 (2026-09-20) that the live BETTER_AUTH_SECRET stays with him and no
agent ever gets a copy. This issue is the sibling that ruling named: how does an agent seat get a
signing key for a preview worker, so :auth and :auth-caylak renders stop ending on a person?

Two named routes are on the table, and only the founder picks:

  1. Commit a fixed preview signing key to the repo. Every agent on every machine has it with no
    setup. A forged preview login then reaches only that preview's own throwaway database. Preview
    URLs are public, so anyone could sign in to a preview as a test user.
  2. Keep it secret and place it per machine as an environment variable on each machine that runs
    agents. One paste per machine, rotated if it leaks. No repo copy, but a new machine is a manual
    step before any signed-in render works.

Either answer is a founder ruling, not a build. That is why this leaves triage ready-for:human.

What is true at origin/main

Read at main, not in a lane's snapshot:

  • infra/ci-credentials/github.ts mints one repo-wide BETTER_AUTH_SECRET (a stable Random
    persisted in the ci-credentials stack state) and pushes it to GitHub as a write-only Actions
    secret. Its docblock names it as one of four roster secrets.
  • apps/web/worker/config.ts binds it as Config.redacted(ENV_BINDINGS.betterAuthSecret), a
    Cloudflare secret_text binding that does not read back at request time, with no default.
  • .github/workflows/deploy.yml:422 passes the same secrets.BETTER_AUTH_SECRET into every
    auth-binding leg — matrix.needs-auth && secrets.BETTER_AUTH_SECRET || '' — so a pr-<n> preview
    worker and the production worker verify against the identical key. There is no per-stage branch.
  • packages/fabrika-cli/src/capture/auth.ts signs the better-auth session cookie with whatever the
    caller names (signSessionToken) and refuses an empty value or the insecure_ placeholder
    prefix (classifyAuthSecret, PLACEHOLDER_SECRET_PREFIX).
  • packages/fabrika-cli/src/review-ui/render-verb.ts takes --auth-secret-from <file> and, on a
    visitor answer, points the caller back at the ci-credentials state behind $ALCHEMY_PASSWORD —
    a route no agent seat holds.

Triage note — the open check the founder named is partly answered. He asked whether a preview
database can ever hold real user data. ADR
0349
records that every preview runs on
its own per-PR D1 named phoenix-phoenix-db-pr-<n>-…, and that preview-seed test-account refuses
any target whose Cloudflare-recorded name lacks the -pr- segment — production and every named
stage are caught, fail closed. The same ADR records that the per-PR preview D1 also carries the
e2e suite's own sign-ups, which are test identities, not customers. So a preview holds no
production user rows. What is not verified, and what route 1 still rests on, is whether a forged
session signed with a committed preview key could reach anything beyond a preview origin — that is
the check the chosen route owes.

Why it matters

Four lanes have parked on the signing side already (#9276, #9279, #9288's fourth criterion, and
#9423 / #8776). #9281 and #9288 fix the account side of signed-in captures; neither touches the
key. With #9339 ruled, this is the remaining half, and until it is answered every auth-gated
review-ui verdict still ends on the founder.

Triage note — why standalone, not folded

Searched the board for an owner on this surface (preview-only auth signing secret for agents).
The nearest live tickets are #9339 (the founder-only ruling, now answered), #9337 (the five-surface
duplication of the secret's provenance), #9281 / #9288 / #9283 (the seeded-account and park-recipe
side) and #7777 (an out-of-band worker secret flake). None owns credential supply policy, and
#9339's own ruling comment says a separate ticket for a test-only key is expected. Folding this
into a buildable ticket is what left #9339 unreachable through four review rounds, so it is minted
standalone.

Pointers

Acceptance criteria

  • One of the two routes above is recorded as a founder ruling on this issue
  • The ruling is cited from the artifact it lands in, an ADR or a pattern amendment
  • The chosen route's blast-radius check is written down: what a forged preview session can reach, and why that is acceptable
  • If route 1: a fixed preview signing key is committed and the deploy path binds it to pr-<n> stages only, leaving production on the founder-held secret [evidence: a live review-ui render of an :auth surface on a preview returning a signed-in capture rather than exit 11]
  • If route 2: the per-machine env-var name and the paste step are written where a seat can follow them, and a seat missing it fails with that instruction rather than a bare visitor answer

Triage note: the criteria cover both shapes the ruling can take. Only the branch the founder picks
is graded.

Amendment — 2026-09-21: the ruling landed, so route 2 is off the table

Nothing above is withdrawn. This block is appended because the founder ruled route 1 on
2026-09-21 (#9533 (comment), marked
decision-ruled in the comment below it). The list above was written while both routes were live,
so its first row asks for a ruling that now exists and its last row grades a branch nobody will
build. The list below replaces it as the contract a builder works to, and grades route 1 alone.

The ruling, restated so a builder need not reconstruct it: preview workers get their own signing
key, that key is committed to the repo, the production BETTER_AUTH_SECRET stays founder-only per
#9339, and the committed key must never be accepted by the production worker. The accepted tradeoff
is that preview logins are open to anyone who reads the repo.

Acceptance criteria

  • A fixed preview signing key is committed to the repo at a named path, with a note beside it saying it is preview-only and deliberately public
  • .github/workflows/deploy.yml's needs-auth matrix leg is stage-keyed: a pr-<n> stage gets the committed preview key, and production and every named stage keep secrets.BETTER_AUTH_SECRET
  • The committed preview key can never reach a non-preview stage, and the deploy path shows which value each stage resolves to rather than leaving it to be inferred
  • packages/fabrika-cli/src/capture/auth.ts accepts the committed preview key: classifyAuthSecret and PLACEHOLDER_SECRET_PREFIX do not refuse the value that is actually committed
  • packages/fabrika-cli/src/review-ui/render-verb.ts resolves the preview key from the repo with no --auth-secret-from and no ci-credentials read, and its visitor-answer message stops pointing a seat at the $ALCHEMY_PASSWORD route no agent holds
  • infra/ci-credentials/github.ts still mints and pushes the production BETTER_AUTH_SECRET unchanged, and its roster docblock says the preview key is not one of its secrets
  • A decision record cites the ruling comment above and writes the blast radius down: a forged preview session reaches only that PR's own throwaway D1 (.decisions/0349-preview-seed-fence-keys-on-d1-name.md), preview logins are open to anyone who reads the repo, and the production worker rejects the committed key
  • A review-ui render of an :auth surface on this PR's own preview returns a signed-in capture rather than exit 11 [evidence: that render's own output on the PR's preview deploy, quoted in the PR body or in a comment on this issue]

Triage note: the ruling itself is already recorded, so it is stated above rather than carried as a
row to tick. No production secret is read, written or rotated anywhere in this work.


Original report (verbatim)

Summary

One repo-wide BETTER_AUTH_SECRET signs login sessions for every deployed web worker, preview and production alike. The founder ruled on 2026-09-20 (#9339) that this live secret stays with him only and no agent is ever given a copy. That leaves signed-in screenshot checks on preview deploys as founder work, unless previews verify against a separate secret that is safe to hand to an agent.

What I was doing

Recording the founder's ruling on #9339, which asked whether an agent should be given a route to the deployed auth secret so it can sign a session and capture a signed-in page.

What I observed

From #9339, read at main by its triager:

  • infra/ci-credentials/github.ts mints one repo-wide BETTER_AUTH_SECRET into the ci-credentials stack's state and pushes it to GitHub as a write-only Actions secret.
  • apps/web/worker/config.ts binds it as secret_text, which does not read back.
  • packages/fabrika-cli/src/capture/auth.ts (classifyAuthSecret, signSessionToken) signs the session cookie with whatever secret the caller names, and refuses an empty or insecure_ placeholder value.
  • The one readable copy sits behind the ci-credentials stack password, which no agent seat carries.

So the secret a preview worker verifies against is the same one production verifies against. A copy that lets an agent sign a preview session would also let it forge a production login for any user. That is why the ruling keeps it founder-only.

Why it matters

The founder ruled elsewhere that he wants out of the pipeline's day-to-day loop. With the ruling on #9339, every :auth and :auth-caylak rendered check still ends on him. #9281 and #9288 (preview test accounts) fix the account side of signed-in captures but not the signing side. Whether previews can take a different secret without breaking anything that shares sessions across stages is not verified.

Pointers

Suggested next step (non-binding)

A guess: mint a second secret used only by preview stages, keep the production one where it is, and give agent seats a read route to the preview one. A forged preview session then reaches only seeded test data. Worth checking first: whether any flow expects a session signed on one stage to verify on another.


Filed by an agent · session 1114d1ad-d769-4169-bb00-4d899c13bc18 · branch main · 2026-09-20T20:53:45Z

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    class:codecreated by fabrika status bootstrap label-taxonomyclass:doccreated by fabrika status bootstrap label-taxonomyp1Medium priorityready-for:agentAn execution engine may pick this up.status:triagedTriage signed off; ready for write-code to picktype:decisionOne question; output is a recorded choice

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions