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>
This commit is contained in:
Andreas Niemann 2026-06-30 17:41:43 +02:00
parent f18676e6b3
commit 188e30e23e
5 changed files with 299 additions and 0 deletions

View file

@ -0,0 +1,60 @@
# 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.