Close the known gap: backup bundles were uploaded unencrypted, relying
solely on destination access control. Now every data artifact is
age-encrypted on the VM before upload and decrypted on restore.
- backup-remote.sh: assemble rustfs blobs into rustfs-blobs.tar.zst (so the
whole bundle is one encrypted unit), then age -r <recipient> each artifact
to <name>.age and drop the plaintext. MANIFEST.json stays cleartext — it
is the inventory + integrity gate and carries no secrets; it records each
artifact's PLAINTEXT sha256 so restore verifies after decrypt.
- restore-remote.sh: materialise the age identity to a 0600 file, decrypt
each .age, then run the existing sha + scratch-restore asserts; add a
rustfs-blobs extract+assert.
- backup.sh / restore.sh: pass the public recipient (arg) / secret identity
(stdin, never argv) from passphrase-encrypted config.
- provision/index.ts: install age + zstd on the VM via cloud-init so a fresh
DR VM (T13) has the backup tools from first boot.
- Pulumi.foundation.yaml: seed backup.ageRecipient (public) + backup.ageIdentity
(secure:). The identity lives in config so {repo + passphrase} can decrypt a
bundle even after total Vault loss (CONTRACT_004 §4.3).
Validated live: encrypted backup + restore-verify PASS from both RustFS and
offsite; bucket shows only *.age + cleartext MANIFEST.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
3.5 KiB
YAML
69 lines
3.5 KiB
YAML
# Pulumi.foundation.yaml — stack config for the `foundation` stack (initial Hetzner home).
|
|
#
|
|
# NON-SECRET values only here (CONTRACT_001 §1.2) — safe to commit in plaintext.
|
|
# Secrets (CONTRACT_001 §1.3) are added by `pulumi config set --secret` as
|
|
# `secure: v1:…` (passphrase-encrypted): foundation:cloudflareApiToken,
|
|
# foundation:backup.offsiteAccessKey/SecretKey, and later vaultCredentials:*.
|
|
#
|
|
# Master passphrase: pass olsitec-foundation/PULUMI_CONFIG_PASSPHRASE (the ONE
|
|
# external secret). Image digests live in foundation/VERSIONS, not here (D5).
|
|
config:
|
|
# --- identity / networking (real: olsitec.net) ---
|
|
foundation:baseDomain: olsitec.net
|
|
foundation:hosts.forge: forge.olsitec.net
|
|
foundation:hosts.vault: vault.olsitec.net
|
|
foundation:hosts.s3: s3.olsitec.net
|
|
foundation:hosts.git: git.olsitec.net
|
|
foundation:forgeSshPort: "2222"
|
|
# --- deployment target: the Helsinki cx33 VM (Docker-over-SSH, port 222) ---
|
|
foundation:vm.host: 204.168.234.72
|
|
foundation:vm.user: root
|
|
foundation:vm.sshPort: "222"
|
|
# --- container plane (CONTRACT_003) ---
|
|
foundation:network.name: foundation-net
|
|
foundation:network.subnet: 172.30.0.0/24
|
|
foundation:dataRoot: /srv/foundation
|
|
# --- TLS: real Let's Encrypt via Cloudflare DNS-01 ---
|
|
foundation:tls.mode: letsencrypt-dns01
|
|
foundation:tls.acmeEmail: a.niemann@olsitec.de
|
|
# --- Cloudflare (DNS records + DNS-01); token is a SECRET set separately ---
|
|
foundation:cloudflare.zoneId: 27e587d5574d5fd6e2cf75b9e914a02c
|
|
# --- fixed names (derived, non-secret; creds generated → Vault) ---
|
|
foundation:postgres.db: foundation
|
|
foundation:postgres.forgejoDb: forgejo
|
|
foundation:rustfs.buckets:
|
|
- forgejo-packages
|
|
- forgejo-artifacts
|
|
- forgejo-lfs
|
|
- foundation-backups
|
|
foundation:forgejo.adminUser: platform-admin
|
|
foundation:forgejo.orgName: olsitec
|
|
foundation:runner.labels:
|
|
- docker:docker://node:20-bookworm
|
|
- dind:docker://-
|
|
# --- credential feature flags (ADR-002) ---
|
|
foundation:features.postgres: "true"
|
|
foundation:features.rustfs: "true"
|
|
foundation:features.forgejo: "true"
|
|
foundation:features.runner: "true"
|
|
foundation:features.backup: "true"
|
|
foundation:features.registry: "true"
|
|
# --- backup: offsite = home Synology MinIO (CONTRACT_004); creds are SECRET ---
|
|
foundation:backup.bucket: foundation-backups
|
|
foundation:backup.offsiteEndpoint: https://minio.wob.olsitec.de:19000
|
|
foundation:backup.retentionDaily: "7"
|
|
foundation:backup.retentionWeekly: "4"
|
|
foundation:cloudflareApiToken:
|
|
secure: v1:xDFqTVZxRm2nvIrQ:ddjNyqKi4C27Fppp9YA0B+gNZPtjWig/NBC6y9dR3cQ8xfNfwEsEHxvRgn8aUTH9UrmjXtLEoYk=
|
|
foundation:backup.offsiteAccessKey:
|
|
secure: v1:svEvJ5K9u+FMnpV/:RztjS8VMSxrdgpBtbNpBPA6gfPLVgnABp77diBC6nWGHZRnG
|
|
foundation:backup.offsiteSecretKey:
|
|
secure: v1:lkkGBjgmJqVziusc:gpmw5lkfFAjXzeFikhtQnvWObYpKD3Bq5XSmrBA/vlLaoqqxFGAAO4Cq7V8nOLZ926x3fXukPQI=
|
|
vaultCredentials:unsealKeys:
|
|
secure: v1:9YpTkFoQanMwxAQV:dJ4YmXS0aOTHPbuK1H6AJ0SAJ0CjYX0iIyLOQAUNfsOWLsSy5TXxPpGecieBWkzc4AALDkJNlQN9Xo6Q0ZcaSg==
|
|
vaultCredentials:rootToken:
|
|
secure: v1:OUpYMjnaftxMUKjv:2m+dydQopXGRleeX6ddhYSHgHP7HHZXYLAvQHXUvaA91qajoxU+VugDB/Rs=
|
|
foundation:backup.ageRecipient: age1x6dmgtt2eahpvyzkmy6j80rts28chw2lcam0rcxq3nhc8ld649sslzpsy4
|
|
foundation:backup.ageIdentity:
|
|
secure: v1:VCFVXswrmMrXyFbr:p4pfG/Kp2lreetYX4O86rZqpU1xQugRycF+PBBiNGZnaD0c15R+mJuLNrl0rBXY5vJwyZTbNSpFY1zPQ7TwuQcVp9h8oiGcgVEobsbb4BBp3lFhsObllgYM9
|
|
encryptionsalt: v1:5YhUt8BVfH0=:v1:DPCHl+7zwn4RaMPj:A19tZzBlZ1NmDtTWrHreEKk5e8idyw==
|