diff --git a/AGENTS.md b/AGENTS.md index 0fe17fb..7149c27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,8 +30,9 @@ the clean and disposable failure fixtures locally. plans. The maintainer-approved exception is authenticated ciphertext for essential bootstrap recovery in `recovery/`, bound by its registered package validator and an independently recoverable key. Ordinary service credentials - stay in OpenBao. The current package covers a historical DNS snapshot only; - adding state recovery requires its own reviewed format and recovery proof. + stay in OpenBao. Paired state packages bind encrypted state and desired + configuration to their reviewed source and run; authenticate recovery with + the independently held identity before using either file. - Import every existing resource before any apply. During migration, convert the plan to JSON and run `scripts/assert-no-destructive-plan.sh`; any delete or replacement is a hard stop. diff --git a/README.md b/README.md index 0993097..7fecc20 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,12 @@ The retained pre-migration DNS snapshot lives as authenticated ciphertext in Keychain. This historical snapshot is neither current desired configuration nor OpenTofu state; the runbook describes its limits and recovery checks. +The paired package in [`recovery/state/`](recovery/state/) retains captured +OpenTofu state and its complete desired configuration. Its manifest binds both +encrypted files to their source and run. Verify authenticated recovery before +using a generation, and refresh it after each reviewed apply. Provider convergence +and remote backend locking have separate acceptance checks in the runbook. + In those procedures the provider reads `CLOUDFLARE_API_TOKEN`, and the state backend reads the standard `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` variables. Keep all three out of shell history and plaintext repository files. diff --git a/docs/runbooks/bootstrap-and-recovery.md b/docs/runbooks/bootstrap-and-recovery.md index ba73b15..8ec46f0 100644 --- a/docs/runbooks/bootstrap-and-recovery.md +++ b/docs/runbooks/bootstrap-and-recovery.md @@ -2,7 +2,8 @@ This procedure keeps Cloudflare recovery independent of the Kubernetes platform. It never reads credentials from a cluster-hosted secret and never -writes credentials or state into this repository. +writes plaintext credentials or state into this repository. Essential encrypted +state and configuration recovery packages use the custody procedure below. ## Prerequisites @@ -311,8 +312,10 @@ Active state still belongs in the independently accessible locked R2 backend. Its minimum authenticated encrypted recovery copy belongs with this owner, including source revision, lineage and serial bindings. That state format, publication path, actual lock-contention test and clean-environment recovery -remain part of the first migration. No real state recovery package is currently -retained, and no apply automation is enabled by the packaging helpers. +remain part of the first migration. The retained initial generation contains the +runner-local zone import and matching desired configuration from the confirmed +bootstrap plan; it does not establish provider convergence or remote locking. +No apply automation is enabled by the packaging helpers. ### State recovery format diff --git a/recovery/state/configuration.json.age b/recovery/state/configuration.json.age new file mode 100644 index 0000000..530c36e Binary files /dev/null and b/recovery/state/configuration.json.age differ diff --git a/recovery/state/manifest.json b/recovery/state/manifest.json new file mode 100644 index 0000000..e444314 --- /dev/null +++ b/recovery/state/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "kind": "cloudflare-state-recovery", + "capturedAt": "2026-11-02T00:46:14Z", + "sourceCommit": "0925d157610d9e281e07557e689a321dadc791e9", + "sourceRun": 41368905763, + "recipient": "age1rk6fs67kly3h4zux5za429z3grjtvs8vcunav4sa28pxk738najsk8wgs6", + "lineage": "d981e029-9986-63a7-2f54-379ce10ee6a4", + "serial": 1, + "accountIdSha256": "3080d90d2ff8a5d38ec8c5dae7b9207b928b353c68f4dd81e0a378cf932a5f95", + "zoneNameSha256": "1fc6c364e0d592421f88ba2d2cf6cf15691bbc711113d7907c159a638f35aeb0", + "stateCiphertextSha256": "4dd0e162814c478c0d675e034eec887de40a382a96f9bb251afb841fabe2709f", + "configurationCiphertextSha256": "5c9527c73d0bfacf126939b555a609c6895ca98cbeb044d7793a33ee65460030", + "statePlaintextSha256": "bf7b431748872f53861aa655cb40c0f2734410f03514e97de5f30ea0aaf69442", + "configurationPlaintextSha256": "84c9b6ea504f82a9fd694e701287e30d54fe9b42ecf81dfccfbf2cba9873239a" +} diff --git a/recovery/state/state.json.age b/recovery/state/state.json.age new file mode 100644 index 0000000..8607037 Binary files /dev/null and b/recovery/state/state.json.age differ