foundation/documentation/contracts/CONTRACT_001_config_schema.md
Andreas Niemann 185be52763 feat(bootstrap): real olsitec.net config + DNS records (steps 1+2)
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>
2026-06-30 20:47:30 +02:00

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.yamlconfig:)

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.<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.gitgit.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://<user>@<host>:<sshPort>.
  • 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).