O8E pre-production hardening (Contabo) — 03-backup-readiness
O8E Report 03 — Backup readiness (G3 / F4)
- macro:
v0.6-o8e-pre-production-hardening-bundle - date_utc: 2026-05-21 · host: Contabo
vmi3080463 - gate covered: G3 — F4 backup readiness
- result: G3 PASS · F4 PARTIAL — mechanism READY, key + runner MISSING (operator package below)
1. Backup readiness matrix
| Component | State | Evidence |
|---|---|---|
gpg binary |
READY | GnuPG 2.4.4, libgcrypt 1.10.3 |
| GPG encrypt/decrypt mechanism | READY — PROVEN | ephemeral-key round-trip, §2 |
BACKUP_GPG_FPR keypair |
MISSING | gpg --list-keys → 0 public keys; BACKUP_GPG_FPR absent from /opt/incomex/docker/.env |
pg_dump |
READY | host 16.13 + postgres container 16.13 |
| 9 backup-target tables | READY — EXIST | §3 |
backup_runner concrete impl |
MISSING | adapter default = _refuse_backup (refuses) |
pre_write_backup seam |
READY | phases/backup.py Mode.LIVE → adapter.pre_write_backup; Mode.DRYRUN simulates a deterministic backup_sha |
2. GPG mechanism proof — non-production, no persistent key
To prove the encrypt envelope without provisioning the real (sensitive)
BACKUP_GPG_FPR private key, an ephemeral throwaway key was generated in
an isolated temporary GNUPGHOME, used for one encrypt+decrypt round-trip,
then the entire keyring was destroyed:
ephemeral key: generated in mktemp -d GNUPGHOME (chmod 700)
encrypt: 236 bytes ciphertext produced
decrypt: round-trip sha256 == plaintext sha256 → faithful
GPG_MECHANISM_PROOF: PASS
cleanup: temp GNUPGHOME rm -rf'd — host root keyring still 0 keys
secrets exposed: NONE — ephemeral key never left the temp dir, never logged
This proves the host can run gpg --encrypt / --decrypt for the backup
envelope. It does not provision the production backup key — that is a
deliberate operator step (§5).
3. Backup scope — 9 mutation-surface tables (all exist)
ProductionLiveExecutionAdapter.pre_write_backup builds a narrow pg_dump
spec over exactly the mutation-surface tables. Catalog-verified present:
public: information_unit, unit_version, iu_lifecycle_log
cutter_governance: cut_change_set, cut_change_set_affected_row,
manifest_envelope, review_decision, verify_result,
dot_pair_signature
(cutter_governance tables are catalog-confirmed via pg_class; the
read-only query_pg role is privilege-filtered out of information_schema
and the schema data itself — a known boundary, not a missing-table problem.)
4. What backup_runner must return
pre_write_backup refuses to advance unless the injected runner returns a
complete envelope: {sha256, size_bytes, gpg_fpr, artifact_ref}. A missing
field raises StopInvariantFailed — fail-closed.
5. Operator command package — provision F4 (NOT executed here)
Provisioning a private GPG key is a sensitive credential-creation step and is intentionally not performed by this macro (forbidden: write secrets). Exact operator steps:
# 1. Provision a dedicated, passphrase-protected backup keypair (operator host)
gpg --batch --gen-key <<'EOF'
Key-Type: default
Key-Length: 4096
Subkey-Type: default
Name-Real: dot-iu-cutter backup
Name-Email: cutter-backup@incomexsaigoncorp.vn
Expire-Date: 2y
Passphrase: <OPERATOR-CHOSEN — store in a vault, never in .env plaintext>
EOF
# 2. Capture the fingerprint
gpg --list-keys --with-colons | awk -F: '/^fpr:/{print $10; exit}'
# 3. Publish (operator edits /opt/incomex/docker/.env — NOT this macro):
# BACKUP_GPG_FPR=<fingerprint>
# 4. Author backup_runner: narrow `pg_dump --table=<9 tables>` | `gpg --encrypt
# --recipient $BACKUP_GPG_FPR` → /var/lib/cutter/backups/<run_id>.sql.gpg ;
# return {sha256,size_bytes,gpg_fpr,artifact_ref}.
# 5. Rollback-only-prove pre_write_backup (Mode.LIVE, post-GAP7 flip) — confirm
# the encrypted artifact verifies and 0 production rows change.
backup_runner is a small VPS-side script — no Mac source patch required for
the runner itself; it is injected into ProductionLiveExecutionAdapter by the
first-run command package.
6. Verdict
backup_mechanism: READY — gpg proven, pg_dump present, 9 tables exist
backup_key: MISSING — BACKUP_GPG_FPR not provisioned (operator step §5)
backup_runner: MISSING — concrete impl to be authored (§5 step 4)
f4_status: PARTIAL — mechanism ready; key + runner packaged
secrets_exposed: NONE
g3: PASS