Interface contracts unblocking the parallel fan-out (T01-T07): - 001 config schema (single stack, passphrase + VERSIONS + Pulumi config) - 002 Vault path layout (foundation/<service>/<type>-credentials, camelCase) - 003 container network/DNS/ports/volumes (foundation-net, named volumes) - 004 backup artifact format + restore order (Vault->PG->RustFS->Forgejo) ADR_F001 (layered platform) already satisfied by ADR-004. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.8 KiB
Contract — CONTRACT_002 — Vault Path Layout
Between: bootstrap/components/credentials.ts (writer) ↔ every service component (reader)
Status: Agreed (pending implementation validation)
Realizes: PLAN-002 §4 · Consistent with: ADR-002, 002_platform_architecture.md §3
Interface
2.1 Mount
- KV v2 mount:
foundation(one mount for the whole egg). - Path scheme:
foundation/<service>/<type>-credentials(mirrors the proven olsicloud4 schemeolsicloud4/<project>/<stage>/<type>-credentials, dropping the stage — Layer 0 is single-stage).
2.2 Key naming — camelCase, no exceptions
Keys are produced by JSON.stringify() of TypeScript objects, so they are camelCase
(e.g. postgresSuperPassword). Any future ESO remoteRef.property (Layer 1) must match exactly.
This is the documented footgun in 002_platform_architecture.md §3 — honour it from day one.
2.3 Paths and keys
| Path | Keys (camelCase) | Generated by |
|---|---|---|
foundation/postgres/service-credentials |
postgresSuperUser, postgresSuperPassword, forgejoDbUser, forgejoDbPassword |
@pulumi/random |
foundation/rustfs/service-credentials |
rustfsAdminUser, rustfsAdminPassword, rustfsServiceKeyId, rustfsServiceKeySecret |
@pulumi/random |
foundation/forgejo/service-credentials |
forgejoSecretKey, forgejoInternalToken, forgejoJwtSecret, forgejoOauth2JwtSecret, forgejoAdminUser, forgejoAdminPassword |
@pulumi/random |
foundation/forgejo/registry-credentials |
ociPushToken, npmPushToken |
Forgejo API post-bootstrap → Vault |
foundation/runner/service-credentials |
runnerRegistrationToken |
Forgejo generate-runner-token → Vault |
foundation/backup/backup-credentials |
offsiteAccessKey, offsiteSecretKey, offsiteEndpoint, backupAgeRecipient, backupAgeIdentity |
seeded once + @pulumi/random (age key) |
foundation/cloudflare/api-credentials |
cloudflareApiToken |
seeded once (mirror of config secret) |
foundation/project/project-credentials |
(empty, disableRead: true) |
manual one-time seed slot (ADR-002 pattern) |
2.4 What is NOT in Vault (the bootstrap exception)
Vault's own rootToken and unsealKeys cannot live in Vault (chicken-egg). They live in the
passphrase-encrypted Pulumi config (vaultCredentials:*, CONTRACT_001 §1.3). This is the single
deliberate exception and the hinge of the whole trust chain (PLAN-002 §4.1).
2.5 Access model
- Day-zero (Layer 0): components read from Vault using the root token (from config) during
pulumi up, or values are rendered into container env/app.inidirectly by Pulumi. No AppRole yet. - Steady-state / Layer 1: introduce a per-consumer AppRole + scoped policy per service
(
foundation/<service>/*read-only), mirroring theSecretStore vault-<project>-<stage>pattern. Policy stubs live inpackages/pulumi-vault/policy.ts(vendored from olsicloud4modules/vault).
Ownership
- Writer:
credentials.tsowns generation + write. It is the only writer of*-credentialspaths (single source of truth; rotation =pulumi up --replace, ADR-002). - Readers: each service component reads only its own service path.
Assumptions
- KV v2 (versioned) — enables rotation history + rollback.
- Vault audit log enabled at init (records every read).
Validation
- After T06: assert every key above exists at the correct path with non-empty value (idempotent
re-run produces no diff). A
vault kv getsmoke check per path.
Change Process
New credential = add a row here + flip the matching features.<x> flag (CONTRACT_001). Never add a
secret to git or config that could instead be generated into Vault. Renames are breaking — version
this contract and update writer + reader together.