# 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//-credentials` (mirrors the proven olsicloud4 scheme `olsicloud4///-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//*` read-only), mirroring the `SecretStore vault--` 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.` 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.