Identity database recovery
Source of truth:
docs/runbooks/identity-database-recovery.md(synced at build time — edit the original, not this copy). This runbook covers the independent, fixed-realmveltro_identityPostgreSQL 16 database introduced by issue #235. It is the suite’s identity authority. It does not back up, restore, or modify VectorFlow.
For full-suite disaster recovery (all product databases, OpenSearch, Redis,
named volumes, sealed identity keyrings), use
backup-restore.md (veltro-backup/v1). This runbook
remains the identity-only recovery path and the recovery-evidence contract the
suite backup reuses for schema-regression refuse rules.
Invariants
Section titled “Invariants”- Database:
veltro_identity; schemas:veltroandveltro_migrations. - Realm: exactly one row whose literal ID is
default. - Migration owner:
veltro_identity_owner; runtime:veltro_identity_runtime; recovery evidence:veltro_identity_recovery. - Migrations run once through
identity-initbefore the runtime starts. Recovery is repair-forward. - Expected head:
038_identity_sigmatic_bootstrap_admin. - Expected normalized migration SHA-256:
22c0f33fe737c9d10e9f82fef82771a0ae4fc3a386ab6ef1d61f9d1ecb27e48a. - Expected schema fingerprint:
1a82d8bc3b8dfc3adb1913f3ef43fb576996baec5cf0cf5fbe8849c7daf3ecbf. - Every live function body and constraint definition in the identity schemas that names
chadmust also namesigmatic. The PostgreSQL harness checks the persisted catalogs after upgrade, fresh migration and restore, including historical helpers. - Veltro-native
admin/memberauthority is independent of product grants; the database rejects removal, disablement, or demotion of the final active administrator, including concurrent attempts. - Personal access token plaintext is never stored. OIDC and SCIM secrets use the
veltro-setting:v1envelope and the mounted Veltro keyring contract. - Recovery is repair-forward. There is no automatic or destructive
downmigration.
The machine-readable source of truth is conformance/identity-persistence-v1.json.
Create a checkpoint
Section titled “Create a checkpoint”-
Establish the source and destination write-fence state for the coordinate being captured. This issue does not authorize destination-only writes, reverse synchronization, cutover, contract, or any point of no return.
-
Using the recovery credential, require
veltro.recovery_evidence()to report the expected database, PostgreSQL major, authenticated recovery role, singleton realm, migration head/checksum, schema fingerprint, exact privileges, and row counts. The role can append/read recovery evidence but cannot read identity application tables or migration ledgers directly. -
Prepare a mode-0600 checkpoint JSON document containing:
- application version and commit;
- PostgreSQL major;
- migration head and every migration checksum;
- schema fingerprint;
- realm literal (
default); - VectorFlow source repository, commit, migration head, watermark, and snapshot digest;
- destination authority generation and deterministic state digest;
- row counts returned by
veltro.recovery_evidence(); - encryption key identifiers needed by encrypted TOTP and identity-setting values;
- explicit source/destination fence state, pre-cutover phase, and rollback mode.
-
Append the validated checkpoint using the recovery role. Append one monotonic reconciliation watermark per covered domain; each watermark must prove identical source/destination PK-set and effective-permission digests. Checkpoints and watermarks are immutable even to the owner:
Terminal window pnpm --filter @veltrosecurity/identity recovery:checkpoint \append-checkpoint recovery-checkpoint.json -
Never put passwords, DSNs containing passwords, TOTP plaintext, backup-code plaintext, encryption keys, or other key material in the checkpoint.
-
Produce a plain-SQL dump with the owner credential, then create the external manifest. The manifest binds the canonical checkpoint digest to the dump SHA-256 without requiring the dump to contain its own digest (an entertainingly impossible circle):
Terminal window pg_dump "$VELTRO_IDENTITY_MIGRATION_DATABASE_URL" \--format=plain --no-owner --file=veltro_identity.sqlpnpm --filter @veltrosecurity/identity recovery:checkpoint \manifest recovery-checkpoint.json veltro_identity.sql recovery-manifest.json
Store the dump, checkpoint, and veltro-identity-recovery-manifest/v1 artifact independently from product databases. Redis rate-limit/failure-window state is explicitly excluded: it is transient protection state, not PostgreSQL recovery state, and this issue adds no Redis dependency or backup procedure.
Restore into an empty database
Section titled “Restore into an empty database”-
Provision the owner, runtime, importer, and recovery login roles before restoring because dump grants name all four. Their credentials must be physically distinct. Every role is
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS; runtime, importer, and recovery roles have no memberships. -
Create an empty PostgreSQL 16 database owned by
veltro_identity_owner. RevokePUBLICaccess, includingCREATEandTEMPORARY; grant only the requiredCONNECTprivileges. -
Rebuild the manifest from the retained checkpoint JSON and dump, then compare it byte-for-byte with the retained manifest. This verifies both the checkpoint digest and dump SHA-256.
-
Restore as the owner:
Terminal window psql "$VELTRO_IDENTITY_MIGRATION_DATABASE_URL" \-X --set=ON_ERROR_STOP=1 --file=veltro_identity.sql -
Do not run migrations to rewrite a restored ledger. First prove that the restored ledger already matches the recorded source checksums.
-
With the runtime credential, call
veltro.persistence_evidence(); with the recovery credential, callveltro.recovery_evidence(). For a temporary restore database, database-name fields intentionally differ; every other head, checksum, fingerprint, realm, role, and privilege field must match. Rename or promote only through the operator-controlled database procedure. -
Compare every table’s deterministic row digest with the checkpoint, including
users.realm_role,personal_access_tokens,identity_settings,mcp_policy, immutable audit events, recovery checkpoints, and reconciliation watermarks. A schema/head match plus one surviving user is insufficient. Exercise foreign keys, fixed-realm checks, product/role and MCP-scope checks, PAT revocation/expiry, final-admin concurrency, monotonic TOTP state and reconciliation chains, disabled-user state, and both append-only evidence boundaries. -
In an authorized recovery environment that has access to the encryption service, prove encrypted values are decryptable by the recorded key IDs. Do not copy key material into the checkpoint or this runbook.
-
Enable writes only after every check succeeds.
apps/identity/db/verify-postgres.sh is the disposable PostgreSQL evidence harness used by CI. It performs fresh migration, replay, checksum-tamper rejection, runtime/importer/recovery privilege negatives, typed checkpoint insertion, monotonic watermark and append-only trigger proofs, concurrent final-admin protection, PAT/settings/policy proofs, manifest-bound plain-SQL dump, exact authority/recovery-data restore comparison, and an adversarial child-table omission rejection.
Failure and repair-forward policy
Section titled “Failure and repair-forward policy”- A checksum, ledger, schema fingerprint, realm, role, privilege, constraint, row-count, or decryptability mismatch fails closed. Keep writes disabled.
- Never edit the migration ledger to make verification green. Diagnose source/dump provenance and create a new forward migration for an approved schema repair.
- Never silently merge, rename, or delete a non-default realm. Such state is unsupported and requires an explicit export or later migration contract.
- Before destination-only authoritative writes exist, discard the shadow restore and retry from a known checkpoint; there is no point of no return.
- After destination-only authoritative writes begin, an image downgrade is not a lossless rollback. Freeze writes and reconcile explicitly, or repair forward with a reviewed migration.