Repository navigation
Description
Problem Statement
Moving a Project onto the estate path today means a person running estate/bootstrap/cluster.sh with a Vault token, once per Project and again after every restore. The script generates session and HMAC keys, creates a fixed Postgres login per Project and writes it to Vault, and copies each OIDC client secret into auth-api's document and checks the copies match. Every one of those steps is knowable from the project files, yet none of it happens in the cluster, so each move is blocked on a manual run and each new stage makes the script longer.
Solution
The render creates every per-Project secret the project files make knowable, in the cluster, before the release that needs it:
- Generated keys. A key declared on a
kvgrant may say how to generate it. A per-Project seed Job writes any generated key that is missing, under the Project's own Vault prefix, and never overwrites one. - Database credentials. An edge to a Postgres provider with
credentials:gets its database and a Vault database role. The Process receives rotating credentials as environment through VSO (Deliver every secret through VSO, as env or file #378), and no fixed login is created. - OIDC clients. An edge to auth declares the client it signs in with. The client secret is generated under the consuming Project's prefix, and auth registers every declared client and reads its secret from there.
After this, a Project moves with a project file and a release. cluster.sh keeps only what is once per cluster: Flux's deploy key, the render pull secret, the policy job's role and the bootstrap CRDs.
User Stories
- As an Application author, I want to declare that a session key is generated, so that I never create it by hand.
- As an Application author, I want a generated key to keep its value across releases, restarts and re-renders, so that sessions survive a deploy.
- As an Application author, I want to say how long a generated key is and how it is encoded, so that it fits what my app parses.
- As an operator, I want a generated key that already exists never to be overwritten, so that a seed run can never log everyone out or break a signature.
- As an operator, I want the seed Job to be able to create keys only under its own Project's prefix, so that a compromised seed cannot touch another Project's secrets.
- As an operator, I want the seed to run before the release that needs its keys, so that a Process never starts without them.
- As an operator, I want a seed failure to hold the release, as a failed prepare step does, so that a half-seeded Project never serves.
- As an Application author, I want my database created for me from my edge to Postgres, so that I declare it once and never run SQL by hand.
- As an Application author, I want rotating database credentials as environment variables, so that my app needs no Vault client.
- As an operator, I want no fixed database password anywhere, so that a leaked credential expires on its own.
- As an operator, I want an existing database adopted rather than recreated, so that moving a Project off the old path keeps its data and its owner role.
- As an Application author, I want to declare my OIDC client on my edge to auth, so that signing in works on the first release.
- As an operator, I want one copy of each client secret, so that auth and the app can never disagree.
- As auth's maintainer, I want auth to learn every declared client from the render, so that adding an app needs no change to auth's own files.
- As an operator, I want a restored Vault without these secrets to get them back on the next release, so that a restore needs no per-Project script.
- As an operator, I want composition to refuse a generated key that another Project also writes, so that two seeds never fight over one value.
- As an operator, I want
cluster.shreduced to the once-per-cluster stages, so that I run it when building a cluster and never when moving a Project.
Implementation Decisions
- Generated keys live on the grant. A
kvgrant'skeysentry may be an object withgenerate: {bytes, encoding}, whereencodingishexorbase64. A generated key must be under the Project's own prefix,secret/data/<project>-system/; anywhere else is refused. A plain string key stays as today. - One seed Job per Project, rendered like a
prepareProcess and ordered before every other prepare step and before any new version starts. Its Vault policy allowscreateon the Project's prefix andreadthere, and nothing else. It writes a key only when it is absent; a present key, of any value, is left alone. It needs no image of the Project's own. - Database credentials finish Deliver every secret through VSO, as env or file #378. The render already derives a Vault database role per consuming edge. A Job owned by the Postgres provider's Project creates the consumer's database and owner role when absent, and adopts an existing database by name. VSO's dynamic secret delivers the credential as environment.
- The OIDC client is declared on the edge to auth, with its redirect path. Its secret is a generated key under the consumer's prefix, so the seed Job creates it. auth receives a derived read grant on every declared client's secret path and the list of clients as rendered configuration; how auth-api consumes that list (a prepare step that registers clients, or reading the paths at start) is decided in its ticket.
- Composition refuses a generated key outside its Project's prefix, two Projects generating the same path, and an OIDC client declared on an edge to a provider that is not an auth provider.
- Both implementations and the oracles change together while
emf/exists. Spec chapters 10, 16, 20, 30 and 55 change, and the decisions get ADRs. - The Estate repository drops
cluster.sh's per-Project stages once each part has landed, and the estate-dashboard and tribelt move off their fixed logins.
Testing Decisions
- Each new field gets a worked example with its rendered tree, reviewed by hand, and a refusal example per refusal.
- The seed Job's policy is tested on the rendered object:
createon the prefix, nothing elsewhere. - Whether the seed overwrites is tested on the Job's script against a fake Vault: absent keys are written, present keys untouched.
- The live check for each part is one Project moved with no
cluster.shrun: tribelt off its fixed login, and a new Project with a generated session key and an OIDC client.
Out of Scope
- Secrets a person supplies (GitHub tokens, third-party API keys, GitHub App keys). They stay manual for now; a dashboard page for them is a later effort.
- The once-per-cluster bootstrap stages.
- Rotating a generated key; a generated key keeps its first value.
Further Notes
Part of #195. Builds on #378, which this makes unblocked work rather than a prerequisite to wait on.
Activity
ExtraToast commentedon Oct 15, 2026
Decided while splitting this into tickets:
Clientobjects; auth-api watches them and creates and rotates each secret atsecret/data/<namespace>/<application>/client; an Application reads${client:secret}. The edge-to-auth design above is dropped (The render emits chapter 17's Clients #449, auth-api serves every declared Client auth-api#104, tribelt and estate-dashboard sign in through their declared Client #450).generate: {bytes, encoding}, defaulting to 32 bytes hex, only under the Project's own prefix, with one writer per path.datais legacy. An existing<project>_dbis reassigned once to<project>-owner(The database catalog is applied from the estate-scoped artifact #447, tribelt and estate-dashboard sign in to Postgres with rotating credentials #448).3 remaining items
ExtraToast commentedon Oct 29, 2026
Picked up from the ready-for-agent backlog and skipped: nothing in this epic is agent work right now.
State on 2026-10-29:
bootstrap/cluster.shon JorisJonkers-dev/estatemainalready holds only the once-per-cluster stages (preflight, Flux's deploy key, the render pull access, Flagger's CRDs, the Flux read check). No per-Project or client stage is left in it.status: ready-for-human: the publish, host move and import run against production); Implement chapter 17: the client block and what it derives #429 (status: blocked, waiting on the same rollout and onemf/'s catch-up); Client block: optional back-channel logout path for native Clients #460 (not triaged).Question for the maintainer: should this epic drop
status: ready-for-agentand stay open until #450 lands, or do you want #460 triaged toready-for-agentso the next agent has something to start here?ExtraToast commentedon Oct 29, 2026
Decision (maintainer, 2026-10-29): #460 (back-channel logout) is triaged for an agent now. #450 waits for the identity cutover.