Skip to content
Like what we’re building? Star on GitHub

Flow — the sealed credentials bundle ​

A cloud run's credentials travel in two halves that never share a channel: the sealed bundle waits in Mongo under a random ref, and the only key that opens it rides the run's own queue message.

Drawn from the code at commit f564846d3; how this page is kept true: README.

The exchange ​

The sequence is drawn on the page that owns the bundle: Cloud architecture → Sealed credentials bundle lifecycle.

Reading notes:

  • The two channels are the feature. Mongo holds the ciphertext, the queue message holds the 32-byte DEK; neither alone opens a bundle, and the runner process boots with SealerOptional — no platform key is needed to open a current (dek) bundle.
  • The binding is cryptographic, not just a field check. RunBundleAAD is run_secrets:<tenant>/<pool>/<run>, so a record served under another identity cannot open even when the rec.TenantID == msg.TenantID comparison is satisfied. The run-only AAD is legacyRunBundleAAD, reserved for id-less records.
  • A resume re-resolves and re-seals. SubmitResume mints a fresh bundle, ref and DEK rather than re-publishing the old ref; the previous record is dropped on the paused run's clean exit, and the TTL covers the rest.
  • No bundle is a legitimate state. An empty SecretsRef proceeds credential-less (the runner falls back to pod env); the publisher also produces no bundle at all when the run has neither an LLM credential nor a generic secret, when the store or sealer is unwired, or when the tenant id is empty.
  • Local mode is the same pipeline over a file-backed sealed store: ResolveLocalCredentials stamps the same secrets.Credentials with no queue and no DEK.

Steps ​

#ActorActionCode
1team adminCreate a generic secret: seal the value under AAD generic_secret:<id>, stamp last4 + fingerprint, land the row in the team it is created forhandleCreateGenericSecret, SealGenericSecret, teamTenantCtx
2team adminBind that secret to one bot under the workflow's declared name, optionally narrowing allowed_hostshandleCreateBotBinding, validateBindingSecret, teamPathTenantCtx
3forgeThe managed-token path: mint the forge token, seal it with its own egress lock, ensure a binding per enabled bot; a GitHub App installation token is re-minted narrowed to the repo set and stamped with a fresh ForgeTokenProofforge.Orchestrator.ensureManagedSecret, narrowGitHubAppSecret, ensureBotBinding
4serverLaunch admitted, then the publisher resolves: the team's llm_fallback policy read fresh, empty RunBundle maps, audienceBotID folded for the BYOK audience onlyruns_launch.go, resolveAndSealCredentials, llmFallbackForTeam, botregistry.NormalizeName
5serverGeneric tier trust gate: an untrusted workspace resolves no workflow secret, because this tier is what writes a forge token into a tree the PR author wrotecredentialTierGeneric branch, store.RunTrust.Trusted
6storeFour-tier resolution per declared name; a binding-sourced value carries IntersectHosts(secret lock, binding hosts)ResolveGenericWithBindings, buildGenericResolution, IntersectHosts
7serverRequired-secret gate: every non-optional declared secret must hold a non-empty plaintext, before the run document is savedrequiredSecretNamesForWorkflow, UnresolvedRequired, RequiredSecretsError
8serverFill the bundle: plaintext, the narrowed host list, and the store id per name — ids only, so the runner can re-read mid-runRunBundle.GenericSecrets, GenericSecretHosts, GenericSecretRefs
9serverDetached, tenant-scoped, 5 s: bump last_used_at on every secret the bundle drew ongoSafeDetached, MongoGenericSecretStore.MarkUsed
10serverMint a fresh per-run DEK, seal the JSON bundle under the full-identity AAD, stamp key_id dekNewRunBundleDEK, NewAESGCMSealer, SealRunBundle
11storePersist the record under a random UUID ref with expires_at = now + 24hNewSecretsRef, RunSecretsRecord, MongoRunSecretsStore.Put
12serverSave the run (credential fingerprints and tiers stamped), then publish the message with both SecretsRef and BundleDEKstore.SaveRun, queue.RunMessage, SchemaVersion
13runnerConsume: decode and gate the schema version before anything touches the workflowdecodeOrTerm, RunMessage.Validate, handleSchemaMismatch
14runnerFetch the record by ref under the run's tenant ctx, then compare tenantsinjectCredentials, store.WithIdentity, MongoRunSecretsStore.Get
15runnerBuild the per-run sealer from the message key, open the bundle, derive the audit fingerprint map, stamp the ctxsecrets.DEKKeyID, OpenRunBundle, secrets.WithCredentials
16runnerMaterialise what must exist as a file: OAuth blobs in 0700/0600 temp dirs; as: file secrets as sandbox mounts or confined 0600 host filesmaterializeOAuthCredentials, addSecretFileMounts, materializeFileSecretsNoSandbox
17runnerMid-run freshness: re-read each GenericRefs record every 5 minutes and rewrite the file, or push it through the driverrefreshFileSecretsLoop, refreshSandboxFileSecretsLoop, sandbox.SecretFileRefresher
18sandboxEgress: a placeholder is substituted only toward a permitted host, and a real value leaving toward any other is blockedeffectiveSecretHosts, secretguard.Guard.ExfiltratesTo, Guard.MaterializeForHostWithin
19runnerDelete the record on a terminal-clean outcome, then wipe the plaintext — the wipe is the deferred closure, so it runs last on every returndeleteRunSecrets, the deferred cleanup closure

The record's own lifecycle ​

StateWhat it meansCode
sealedCiphertext in run_secrets under a random ref, key_id dek, 24 h expiry; the key exists only on the publisher and the messageresolveAndSealCredentials, RunSecretsRecord
openedThe bundle is plaintext in the runner's memory and on the run ctxinjectCredentials, secrets.WithCredentials
materialisedValues also exist as files: OAuth config dirs, as: file mounts, the clone's git credentialmaterializeOAuthCredentials, addSecretFileMounts, materializeFileSecretsNoSandbox
wipedLocal hygiene: keys and generic values zeroed, temp dirs removed — the persistent record is untouched. Registered as a defer before the body runs, so it fires after the delete decisionthe cleanup closure of injectCredentials
deletedRemoved from the store, detached with a 5 s timeout, on a clean or paused outcome onlydeleteRunSecrets, called once in the body of executeRun
survivingKept on purpose so a redelivery of the same message re-opens the same refthe runErr == nil || ErrRunPaused condition at the delete site
ttl_expiredReaped by the run_secrets_ttl index on expires_at; a resume re-resolves and re-sealsDefaultRunSecretsTTL, MongoRunSecretsStore.EnsureSchema

Three key schemes read, one writes: dek (per-run key, full AAD) is all the publisher produces; an empty key_id opens through the ring with the legacy run-only AAD, and a ring id through KeyedSealer.OpenWith — the 24 h TTL retires both older cohorts.

Guards that refuse ​

  • canManageTeam → 403 admin or owner required on creating a team secret and on every binding mutation; canViewTeam → 403 not a member on the lists. canMutateGenericSecret adds the per-record rule: a user-scoped record only for its owner, a team-scoped one under the create right, a super-admin always.
  • Request shape → 400 name + secret required (validGenericSecretName, empty value), 400 no active team on /api/me/secrets without an active team, 400 secret_id is not a team-scoped secret in this org (validateBindingSecret).
  • Uniqueness → the tenant_bot_name_unique index forbids two bindings of one declared name for one bot in one team.
  • Fallback policy → llmFallbackForTeam refuses the launch, before any tier is consulted, on an unreadable teams row or on an llm_fallback value identity.ValidLLMFallback rejects: the policy decides whether platform credentials may be sealed into a sovereign team's run, so it is read fresh and never guessed. A missing team row is a definitive answer — the platform default.
  • Trust → errUntrustedRequiresSecrets: an untrusted workspace plus a non-optional declared secret stops the launch; with only optional secrets, the tier is skipped and logged.
  • Required secrets → secrets.RequiredSecretsError, raised before SaveRun, so the run never exists with a credential silently unset.
  • AAD grammar → SealRunBundle refuses a tenant id carrying / and a pool name failing validPoolName, at the chokepoint both seal callers traverse.
  • Gateway carriage → SealRunBundle refuses (typed ErrGatewayKeyWithoutURL) an openai_compatible key without its canonical base_url, a URL without a key, or a key without the server's fingerprint, on either channel.
  • Wire shape → RunMessage.Validate refuses queue: BundleDEK without SecretsRef — corrupt publish and a DEK that is not exactly 32 bytes, on publish and again on consume.
  • Schema version → queue.ErrSchemaVersion outside [MinSchemaVersion, SchemaVersion] goes to handleSchemaMismatch and on to handleAdmissionMismatch: a delayed Nak on every delivery, and on the last permitted one — planAdmissionMismatch marks the plan final when delivered >= maxDeliver — the payload parks on its pool's DLQ, the shared DLQ when the run carries no pool (dlqPublishSubject), with the run document flipped from queued to failed_resumable. A pre-v24 runner refuses a v24 message instead of failing at injection.
  • Tenant binding → run_secrets tenant mismatch (msg=%q sealed=%q); an empty stored tenant does not bypass it.
  • Key scheme → secrets.ErrBundleDEKMissing for a dek record whose message carries no 32-byte key; the record predates per-run keys (key_id=%q) but this runner carries no key ring for an older cohort on a keyless pod.
  • Store tenancy → ErrGenericSecretTenantMissing and ErrBindingTenantMissing refuse a call with no tenant in ctx; withRunSecretsTenantFilter instead passes through for privileged tenant-less callers, and the AAD is what keeps that safe.
  • Credential-level drops (warned, the launch continues) → a dangling binding or webhook override, one pointing at a personal or cross-team secret (bindableGenericSecretForBotBinding), a sealed blob that will not open.
  • Mount paths → an unsandboxed run refuses an out-of-tree mount_path (RelativeToSecretFilesMountDir, confined to /run/iterion/secrets); a sandboxed one refuses a relative, unclean, control-character or duplicated mount path.
  • Egress → effectiveSecretHosts encodes two disjoint allowlists as the deny-all sentinel [""], which hostMatch never matches; the proxy answers 403 blocked by sandbox secret policy when a real value heads for an unapproved host.

Collections touched ​

CollectionWhat this flow does to it
generic_secretsWritten sealed on create and on rotation (value, fingerprint, and the forge proof — re-stamped by the minting and refresh paths, narrowGitHubAppSecret and RefreshWorker.rewriteManagedSecret, cleared only on a hand-rotated value, by secrets.SealInto and handleUpdateGenericSecret); read by the resolver, and re-read by the mid-run loops through GenericSecretRefs; last_used_at bumped detached
bot_secret_bindingsWritten by an admin or by the forge orchestrator; read per (tenant, bot) to resolve the binding tier and its host narrowing
run_secretsOne record per launch or resume: sealed bundle, key_id, tenant, run, 24 h TTL; read once by the runner, deleted on a clean outcome
api_keys, oauth_credentialsRead by the LLM tiers of the same resolution; their plaintext and blobs ride the same bundle
cred_pools, cred_pledges, cred_leasesThe last-resort tier, reached only by a run holding no credential of its own: credpool.Broker.Acquire supersedes the team's open leases, walks the eligible pledges and admits one — the donated subscription or key rides the same bundle, in the slot its kind belongs to
teamsRead fresh at every launch and resume for the credential-fallback policy (llm_fallback), which decides whether the shared tiers are walked at all
runsSaved before the publish with the credential fingerprints and tier tags; flipped by a DLQ park
webhook_configsThe webhook token or HMAC selects the tenant, and its secret_overrides are the highest resolution tier
forge_connectionsThe managed forge token's own record: the minter writes the secret and the binding beside it
audit_eventssecret.created, binding.created and their siblings, under the team the record belongs to