CONTRACT_001 amendments: hosts.git, vm.sshPort (default 22; VM uses 222), cloudflare.zoneId. config.ts + lib/context.ts (provider host uses sshPort). - components/dns.ts: forge/vault/s3/git.olsitec.net A -> VM (DNS-only, own CF provider from encrypted token). Deployed + verified authoritative = 204.168.234.72. - Pulumi.foundation.yaml: real config (olsitec.net, vm 204.168.234.72:222, letsencrypt-dns01) + encrypted secrets (cloudflare token, offsite creds). Master passphrase: pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE. - run.sh: reproducible deploy (passphrase + ssh key from pass/home). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.9 KiB
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)
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/randomand written to Vault (CONTRACT_002) — never placed in config. The passphrase is never stored anywhere (ENV only).
Ownership
- Producer:
bootstrap/config.tsparses + validates (fails closed on missing required keys). - Consumers: components read typed config; they MUST NOT read raw
pulumi.Configad 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 aVERSIONSdiff (PLAN-002 §7.1).
Validation
preflight/asserts ENV +VERSIONSpresent and well-formed beforepulumi up.pulumi previewon an empty stack must report missing required config clearly (acceptance T02).
Change Process
Adding a service = add its features.<x> 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.tsbuilds the Docker-over-SSH provider host asssh://<user>@<host>:<sshPort>.cloudflare.zoneId— non-secret zone id for DNS records + ACME DNS-01. The matching API token is the secretfoundation: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).