# Contract — CONTRACT_001 — Bootstrap Config Schema **Between**: `bootstrap/config.ts` (producer) ↔ every component in `bootstrap/components/*` (consumers) **Status**: Agreed (pending implementation validation) **Realizes**: PLAN-002 §3, §4.2 · **Depends on**: ADR-004, ADR-005 ## Interface The single Pulumi stack `foundation` is configured through three channels. **No other inputs exist.** ``` 1. ENV PULUMI_CONFIG_PASSPHRASE (the master passphrase — the only external secret) SSH_PRIVATE_KEY_PATH (path to the key that reaches the VM; default ~/.ssh/id_rsa) 2. VERSIONS foundation/VERSIONS (image digests + tool versions — determinism, not in Pulumi config) 3. Pulumi config Pulumi.foundation.yaml (typed, non-secret) + secrets (secure: v1:…, passphrase-encrypted) ``` ### 1.1 Typed config shape (`config.ts` MUST export this) ```ts export interface FoundationConfig { // --- identity / networking --- baseDomain: string; // "olsitec.de" hosts: { // public FQDNs terminated by Caddy forge: string; // "forge.olsitec.de" (Forgejo web/API/registry) vault: string; // "vault.olsitec.de" (Vault UI/API — internal-restricted) s3: string; // "s3.olsitec.de" (RustFS API, optional public) }; forgeSshPort: number; // 2222 (git-over-ssh, published directly, not via Caddy) // --- deployment target (Docker-over-SSH provider) --- vm: { host: string; // IP or DNS of the foundation VM user: string; // ssh user (e.g. "root" or "deploy") // private key path comes from ENV SSH_PRIVATE_KEY_PATH, never config }; // --- container plane (see CONTRACT_003 for names/ports) --- network: { name: string; subnet: string }; // "foundation-net", "172.30.0.0/24" dataRoot: string; // host path for bind mounts / named-volume root (e.g. "/srv/foundation") // --- TLS strategy --- tls: { mode: "letsencrypt-dns01" | "internal-ca"; // day-zero may start internal-ca, switch later acmeEmail: string; // cloudflareApiToken is a SECRET (see §1.3) }; // --- service sizing / fixed names (derived, non-secret) --- postgres: { db: string; forgejoDb: string }; // names only; creds are generated → Vault rustfs: { buckets: string[] }; // ["forgejo-packages","forgejo-artifacts","forgejo-lfs","foundation-backups"] forgejo: { adminUser: string; orgName: string };// "platform-admin", "olsitec" runner: { labels: string[] }; // ["docker:docker://…","dind:docker://-"] (PLAN-001 §4a) // --- credential feature flags (ADR-002 style; selects what @pulumi/random generates) --- features: { postgres: boolean; rustfs: boolean; forgejo: boolean; runner: boolean; backup: boolean; registry: boolean; }; // --- backup --- backup: { bucket: string; // "foundation-backups" (in RustFS) offsiteEndpoint: string; // self-hosted second location (CONTRACT_004); creds are SECRET retentionDaily: number; retentionWeekly: number; }; } ``` ### 1.2 Non-secret config keys (`Pulumi.foundation.yaml` → `config:`) Namespace **`foundation:`**. Examples: `foundation:baseDomain`, `foundation:vm.host`, `foundation:tls.mode`, `foundation:rustfs.buckets` (array), `foundation:features.forgejo`. All are reproducible and safe to commit in plaintext. ### 1.3 Secret config keys (`secure: v1:…`, passphrase-encrypted, committable) Namespace **`vaultCredentials:`** and **`foundation:`** as appropriate: | Key | Source | Notes | |-----|--------|-------| | `vaultCredentials:rootToken` | captured after `vault operator init` | EXACT pattern from `olsitec-core/run.sh` | | `vaultCredentials:unsealKeys` | captured after init (JSON array) | used by the passphrase-gated unseal helper (D2/ADR-004) | | `foundation:cloudflareApiToken` | seeded once (manual) | DNS-01 ACME; also mirrored to Vault for renewal | | `foundation:backup.offsiteAccessKey` / `…offsiteSecretKey` | seeded once | offsite target creds; mirrored to Vault (`foundation/backup/backup-credentials`) | > **Everything else is generated** by `@pulumi/random` and written to Vault (CONTRACT_002) — never > placed in config. The passphrase is **never** stored anywhere (ENV only). ## Ownership - **Producer**: `bootstrap/config.ts` parses + validates (fails closed on missing required keys). - **Consumers**: components read typed config; they MUST NOT read raw `pulumi.Config` ad hoc. ## Assumptions - One stack, one environment ("foundation") at Layer 0. Multi-stage is a Layer-1 concern. - Image digests live in `VERSIONS`, not config, so an upgrade is a `VERSIONS` diff (PLAN-002 §7.1). ## Validation - `preflight/` asserts ENV + `VERSIONS` present and well-formed before `pulumi up`. - `pulumi preview` on an empty stack must report missing required config clearly (acceptance T02). ## Change Process Adding a service = add its `features.` flag + its fixed names here, then its Vault keys in CONTRACT_002 and its container in CONTRACT_003. Breaking key renames require a minor version note in this contract and a coordinated update across consumers. --- ## Amendment 2026-06-30 (steps 1+2) Added to the typed surface (FoundationConfig): - **`hosts.git`** — `git.olsitec.net`, dedicated Git-over-SSH host (forge+vault+s3+git set). - **`vm.sshPort`** — optional number, **default 22**; the test/initial Helsinki VM uses **222** (the vendored hetzner cloud-init moves sshd to 222). `lib/context.ts` builds the Docker-over-SSH provider host as `ssh://@:`. - **`cloudflare.zoneId`** — non-secret zone id for DNS records + ACME DNS-01. The matching API token is the secret `foundation:cloudflareApiToken` (§1.3). The `foundation` stack is now the **initial Hetzner home** (olsitec.net, vm `204.168.234.72:222`). Master passphrase: `pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE` (the single external secret).