Skip to content

ADR-0006: No long-lived credential reachable from inside a container

  • Status: Proposed
  • Date: 2026-08-06
  • Implementation: partial — machinery shipped (src/engine/credentials.ts:200 catalog, :264 resolve; src/engine/git-scrub.ts:91), adoption pending. ../credential-boundary.md records CLOUDFLARE_API_TOKEN and NPM_TOKEN as “machinery done, adoption pending” and operator secrets as open.

flare-dispatch injects secret values into the command env with best-effort substring redaction (6 of ~19 catalog runs take a secrets input; wrangler deploy needs CLOUDFLARE_API_TOKEN), and embeds GitHub installation tokens in clone URLs that survive in .git/config. fractalbot’s ADR-0005 designed credential injection but never built it. Meanwhile the platform caught up: Cloudflare Sandboxes’ outbound handlers can inject credentials Worker-side so “the secret lives in the Worker’s environment and is never passed into the sandbox” — the machinery exists first-party; the substrate’s job is deciding which credential attaches under which grant. flare-dispatch’s writeback path already embodies the target: the Worker, never the container, performs the authenticated write.

No long-lived credential is reachable from inside a container — env, argv, or filesystem. Writes leave by two sanctioned shapes:

  1. Worker-side writeback (preferred): the sandbox produces an artifact; the substrate or the consumer’s Worker performs the authenticated write.
  2. Handler-injected credentials for in-sandbox writes that cannot writeback (wrangler deploy → api.cloudflare.com is the acceptance case): credentials are per-host descriptors {secretName, host, headerTemplate} in the run/recipe definition; the substrate resolves the value and injects it in the egress handler on requests that pass the grant, strips container-authored auth headers, and hands the container nothing (or an inert placeholder for tools that refuse to start without one).
Where a credential attaches Flowchart, top to bottom. 7 nodes, 7 edges. Container — no long-lived credential: command → egress handler drops container auth headers injects the host's headerTemplate [request that passes the grant]; → artifact written under /artifacts Container — no long-lived credential: artifact written under /artifacts → writeback substrate or consumer Worker Worker side: Worker secrets → egress handler drops container auth headers injects the host's headerTemplate; → writeback substrate or consumer Worker Worker side: egress handler drops container auth headers injects the host's headerTemplate → credentialed host e.g. api.cloudflare.com Worker side: writeback substrate or consumer Worker → authenticated write Container — nolong-lived credential command artifactwritten under/artifacts Worker side Worker secrets egress handlerdrops container authheadersinjects the host'sheaderTemplate writebacksubstrate orconsumer Worker request that passesthe grant credentialed hoste.g.api.cloudflare.com authenticated write
Where a credential attaches
Diagram source
flowchart TB
accTitle: Where a credential attaches
subgraph box["Container — no long-lived credential"]
cmd["**command**"]
art["**artifact**<br/>written under `/artifacts`"]
end
subgraph worker["Worker side"]
secrets[("Worker secrets")]
handler["**egress handler**<br/>drops container auth headers<br/>injects the host's `headerTemplate`"]
wb["**writeback**<br/>substrate or consumer Worker"]
end
cmd -->|request that passes the grant| handler
cmd --> art
art --> wb
secrets --> handler
secrets --> wb
handler --> api["credentialed host<br/>e.g. `api.cloudflare.com`"]
wb --> write["authenticated write"]
class secrets accent

The one sanctioned in-container credential is the per-execution model-proxy token: execution-scoped, budget-capped, header-only transport (query-param rejected), revoked by a DO alarm at the run’s max wall-clock — revocation does not depend on finalize running — and never logged.

  • The rule takes effect per credential class as its shape lands; a migration table (GitHub App token, CLOUDFLARE_API_TOKEN, npm token, operator secrets) tracks each. The secrets / secretPrefix run inputs are deprecated at adoption and removed at stage-2 exit.
  • Clone URLs are scrubbed from git remotes immediately post-clone; .git config is redacted at the artifact/checkpoint capture chokepoint.
  • The substrate’s never-store/never-log list: consumer bot tokens (never reach the substrate), installation tokens, secret values, capability-token values, raw prompts beyond metering metadata.