foundation/documentation/contracts/CONTRACT_002_vault_path_layout.md
Andreas Niemann 188e30e23e docs(contracts): add CONTRACT_001-004 — T00
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>
2026-06-30 17:41:43 +02:00

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 scheme olsicloud4/<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.ini directly by Pulumi. No AppRole yet.
  • Steady-state / Layer 1: introduce a per-consumer AppRole + scoped policy per service (foundation/<service>/* read-only), mirroring the SecretStore vault-<project>-<stage> pattern. Policy stubs live in packages/pulumi-vault/policy.ts (vendored from olsicloud4 modules/vault).

Ownership

  • Writer: credentials.ts owns generation + write. It is the only writer of *-credentials paths (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 get smoke 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.