From 0349a08e1b76dd02229253be043132284796f629 Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 5 Sep 2026 01:53:24 +0200 Subject: [PATCH] Harden backup credentials and add durable image inventory publication Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883 --- Makefile | 8 ++ WORK-RECORDS.md | 30 ++++---- docs/evidence/repo-review-2026-09-05.md | 44 +++++++++++ docs/forgejo-backup.md | 9 ++- docs/forgejo-package-prune.md | 33 +++++++- lib/railiance-backup-common.sh | 8 +- scripts/refresh_live_images.py | 75 +++++++++++++++++++ tests/test_backup_credentials.py | 51 +++++++++++++ tests/test_live_images_inventory.py | 42 +++++++++++ tools/cmd/forgejo-backup | 5 +- ...lm-connect-openrouter-provider-key-lane.md | 11 +++ ...PF-WP-0028-durable-live-image-inventory.md | 36 +++++++++ ...-0029-backup-credential-default-removal.md | 36 +++++++++ 13 files changed, 365 insertions(+), 23 deletions(-) create mode 100644 docs/evidence/repo-review-2026-09-05.md create mode 100644 scripts/refresh_live_images.py create mode 100644 tests/test_backup_credentials.py create mode 100644 tests/test_live_images_inventory.py create mode 100644 workplans/RPF-WP-0028-durable-live-image-inventory.md create mode 100644 workplans/RPF-WP-0029-backup-credential-default-removal.md diff --git a/Makefile b/Makefile index c8a0747..5c8d2c1 100644 --- a/Makefile +++ b/Makefile @@ -471,6 +471,14 @@ forgejo-backup-status: ## Show last Forgejo backup success and 7-day gate forgejo-package-prune-dry-run: ## List Forgejo package versions beyond retention depth (no deletes) tools/cmd/forgejo-package-prune +LIVE_IMAGES_OUTPUT ?= $(HOME)/.local/state/railiance-platform/live-images/all.txt +LIVE_IMAGES_SOURCES ?= +.PHONY: live-images-refresh +live-images-refresh: ## Atomically preserve and merge image exports outside the checkout + @test -n "$(LIVE_IMAGES_SOURCES)" || { echo 'set LIVE_IMAGES_SOURCES to nonempty cluster export files' >&2; exit 2; } + python3 scripts/refresh_live_images.py --output "$(LIVE_IMAGES_OUTPUT)" \ + $(foreach source,$(LIVE_IMAGES_SOURCES),--source "$(source)") + forgejo-package-prune: ## Prune Forgejo packages — keep newest 3 versions per package tools/cmd/forgejo-package-prune --apply diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index c599d65..8a5058f 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -26,13 +26,13 @@ | workplan | RPF-WP-0015 | active | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md | | workplan | RPF-WP-0016 | finished | — | workplans/RPF-WP-0016-versioned-ephemeral-custody-lifecycle.md | | workplan | RPF-WP-0017 | finished | — | workplans/RPF-WP-0017-attended-login-output-containment.md | -| workplan | RPF-WP-0018 | blocked | — | workplans/RPF-WP-0018-openbao-operator-only-access.md | | workplan | RPF-WP-0018 | finished | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md | -| workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-flex-auth-credential-grant-integration.md | | workplan | RPF-WP-0020 | finished | — | workplans/RPF-WP-0020-ccr-schema-drift.md | -| workplan | RPF-WP-0020 | active | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | | workplan | RPF-WP-0021 | finished | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | +| workplan | RPF-WP-0025 | blocked | — | workplans/RPF-WP-0025-openbao-operator-only-access.md | +| workplan | RPF-WP-0026 | finished | — | workplans/RPF-WP-0026-flex-auth-credential-grant-integration.md | +| workplan | RPF-WP-0027 | active | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | | task | ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md | | task | ADHOC-2026-08-23-T02 | done | — | workplans/ADHOC-2026-08-23.md | | task | RPF-WP-0001-T01 | done | — | workplans/RPF-WP-0001-credential-request-and-lease-broker.md | @@ -116,9 +116,6 @@ | task | RPF-WP-0016-T05 | done | — | workplans/RPF-WP-0016-versioned-ephemeral-custody-lifecycle.md | | task | RPF-WP-0016-T06 | done | — | workplans/RPF-WP-0016-versioned-ephemeral-custody-lifecycle.md | | task | RPF-WP-0017-T01 | done | — | workplans/RPF-WP-0017-attended-login-output-containment.md | -| task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-openbao-operator-only-access.md | -| task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-openbao-operator-only-access.md | -| task | RPF-WP-0018-T03 | wait | — | workplans/RPF-WP-0018-openbao-operator-only-access.md | | task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | @@ -130,21 +127,24 @@ | task | RPF-WP-0019-T02 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md | | task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md | | task | RPF-WP-0019-T04 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md | -| task | RPF-WP-0019-T01 | done | — | workplans/RPF-WP-0019-flex-auth-credential-grant-integration.md | -| task | RPF-WP-0019-T02 | done | — | workplans/RPF-WP-0019-flex-auth-credential-grant-integration.md | -| task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-flex-auth-credential-grant-integration.md | | task | RPF-WP-0020-T01 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md | | task | RPF-WP-0020-T02 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md | | task | RPF-WP-0020-T03 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md | | task | RPF-WP-0020-T04 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md | -| task | RPF-WP-0020-T01 | done | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | -| task | RPF-WP-0020-T02 | done | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | -| task | RPF-WP-0020-T03 | progress | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | -| task | RPF-WP-0020-T04 | wait | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | -| task | RPF-WP-0020-T05 | wait | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | -| task | RPF-WP-0020-T06 | progress | — | workplans/RPF-WP-0020-keycape-live-secret-exposure-recovery.md | | task | RPF-WP-0021-T01 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | | task | RPF-WP-0021-T02 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | | task | RPF-WP-0021-T03 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | | task | RPF-WP-0021-T04 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | | task | RPF-WP-0021-T05 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | +| task | RPF-WP-0025-T01 | done | — | workplans/RPF-WP-0025-openbao-operator-only-access.md | +| task | RPF-WP-0025-T02 | done | — | workplans/RPF-WP-0025-openbao-operator-only-access.md | +| task | RPF-WP-0025-T03 | wait | — | workplans/RPF-WP-0025-openbao-operator-only-access.md | +| task | RPF-WP-0026-T01 | done | — | workplans/RPF-WP-0026-flex-auth-credential-grant-integration.md | +| task | RPF-WP-0026-T02 | done | — | workplans/RPF-WP-0026-flex-auth-credential-grant-integration.md | +| task | RPF-WP-0026-T03 | done | — | workplans/RPF-WP-0026-flex-auth-credential-grant-integration.md | +| task | RPF-WP-0027-T01 | done | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | +| task | RPF-WP-0027-T02 | done | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | +| task | RPF-WP-0027-T03 | progress | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | +| task | RPF-WP-0027-T04 | wait | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | +| task | RPF-WP-0027-T05 | wait | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | +| task | RPF-WP-0027-T06 | progress | — | workplans/RPF-WP-0027-keycape-live-secret-exposure-recovery.md | diff --git a/docs/evidence/repo-review-2026-09-05.md b/docs/evidence/repo-review-2026-09-05.md new file mode 100644 index 0000000..76790e0 --- /dev/null +++ b/docs/evidence/repo-review-2026-09-05.md @@ -0,0 +1,44 @@ +# Repository review — 2026-09-05 + +Reviewed the working tree, fetched origin, scanned all local workplan statuses, +and read/acknowledged the State Hub inbox. Existing WORK-RECORDS.md changes +correct renumbered plans 0025–0027 and are retained. + +Open plans 0015, 0025 and 0027 retain their attended gates: approved exercise +windows/abort operators, operator listener cutover, and exact recovery/custody +receipts. No new qualifying receipts were in the unread inbox. + +New actionable source work is tracked by RPF-WP-0028 (durable image inventory) +and RPF-WP-0029 (backup credential fallback). Their live completion gates remain +explicit. No prune, reboot, lease revocation, or provider rotation was run. + +Other inbox demands requiring scoped design or owner inputs remain pending: + +- ops-mason fluid-telegram: tenant prefix and actual OIDC group claim remain + unconfirmed. Read/write policy requires reviewed CCR schema/validator support; + current read-only request shape must not be bypassed. +- secrets-engine: native AppRole apply still requires serving canonical action + authorization, exact targets, attended authority and consumer health proof. + Its separate service JWT mount/role contract is not yet established here. +- State Hub rename preflight: dedicated signing-secret lane remains to be + designed and approved; the rename is outside this session's scope. +- Policy Nexus CCR-2026-0014 request is superseded by the repository's existing + activation commit 62423fd; no duplicate provisioning performed. +- Activity-core OpenRouter recovery was reported complete and its intentional + reuse decision retained in RPF-WP-0004. + +No coordination messages were sent. Incoming requests are recorded here for +follow-up rather than treated as permission to invent identities or credentials. + +Validation: unittest discovery passed 158 tests; full pytest discovery passed +164 tests, including the function-based tests omitted by unittest. Bash syntax +and git diff whitespace checks passed. The inventory and credential tests use synthetic +inputs and mocked custody only. + +State Hub synchronization remains pending: `statehub fix-consistency` was +attempted in the sandbox and once outside it. Both were interrupted after +prolonged HTTP reads during the check phase, before a repair report was +returned. No registrar skip was reported, so registrar authority was not +assumed. Re-run `statehub fix-consistency` when these reads complete reliably +to index the new workplans and refresh the generated records/brief. Progress +was logged successfully as `7e317a96-eb7e-4b36-9d81-4efe49efa22c`. diff --git a/docs/forgejo-backup.md b/docs/forgejo-backup.md index 04b9da8..1fe2af9 100644 --- a/docs/forgejo-backup.md +++ b/docs/forgejo-backup.md @@ -26,6 +26,13 @@ Preflight: `forgejo-db` pod must be Ready (`make forgejo-db-status`). A full dum with 13 org repos is ~670MiB — allow 10–20 minutes for stream + age on a typical workstation link. +Uploads require the governed `CCR-2026-0004` OpenBao offsite lane or explicitly +provided `RAILIANCE_BACKUP_NC_TOKEN` / `RAILIANCE_BACKUP_NC_WEBDAV_URL` inputs. +There is no built-in credential fallback. Missing credentials stop execution +before any cluster dump. Local encryption dry-runs skip upload authentication. +Provider rotation and replacement upload/restore proof are tracked separately +in `RPF-WP-0029`; removing the source default does not prove revocation. + Decrypt: `~/.config/age/railiance-backup.key` (same key as other Railiance backups). ## Nextcloud layout @@ -62,4 +69,4 @@ See `railiance-infra`: - `docs/forgejo-restore-drill-evidence.md` Download and decrypt a dump from Nextcloud, set `BACKUP_LOCAL` to the zip path, -then run the drill script. \ No newline at end of file +then run the drill script. diff --git a/docs/forgejo-package-prune.md b/docs/forgejo-package-prune.md index baf6668..f79c3b1 100644 --- a/docs/forgejo-package-prune.md +++ b/docs/forgejo-package-prune.md @@ -73,7 +73,38 @@ make forgejo-package-prune-dry-run # list would-delete candidates make forgejo-package-prune # delete beyond retention depth ``` -## Rollback +## Durable multi-cluster image inventory + +The inventory belongs outside the platform checkout. On the production worker +host, use `/home/tegwick/.local/state/railiance-platform/live-images/all.txt`. +`make live-images-refresh LIVE_IMAGES_SOURCES="/path/railiance01.txt /path/coulombcore.txt"` +merges complete, nonempty exports with the prior inventory under a writer lock, +then atomically publishes sorted image references and a count/hash receipt. +Set `LIVE_IMAGES_OUTPUT` when publishing for a different host user. +Missing, empty, or malformed input fails without replacing the previous file. +Refresh only adds protection: removal of obsolete tags requires a separate +review of every production cluster and rollback requirement. + +Migration and rollout sequence: + +1. Seed from the restored `docs/evidence/live-images-all.txt` plus fresh exports + from railiance01 and coulombcore; retain the restored multi-cluster entries. +2. Project the durable **directory** read-only into the activity-core worker + and configure its inventory argument to `all.txt` in that directory. Do not + mount the single file: atomic replacement must be visible to the worker. +3. Verify the worker reads a nonempty inventory and retains the existing hard + refusal of apply when that file is missing or empty. Use only its dry-run + verification; do not manually trigger prune. +4. After each cluster image rollout, export pod regular, init, and ephemeral + container images to a temporary file. Require successful kubectl exit and + nonempty output, then invoke this refresh target with that export. Keep + other clusters' prior entries during outages. Run the hook on the durable + host, or transfer the complete export there before invoking it. + +The publisher is implemented here; host migration and the activity-core mount +and rollout-hook adoption are tracked in `RPF-WP-0028` until verified live. + +## Rollback procedure If a needed tag was removed, restore from the latest Nextcloud `forgejo-dump-*.zip.age` (`make forgejo-backup` evidence) using `railiance-infra/tools/forgejo-restore-drill.sh`, diff --git a/lib/railiance-backup-common.sh b/lib/railiance-backup-common.sh index 7a9a425..d63e76a 100644 --- a/lib/railiance-backup-common.sh +++ b/lib/railiance-backup-common.sh @@ -18,12 +18,12 @@ railiance_backup_load_openbao_lane() { railiance_backup_require_openbao_lane() { railiance_backup_load_openbao_lane - # Nextcloud *file drop* shares use /public.php/dav/filesdrop// — not /public.php/webdav - : "${RAILIANCE_BACKUP_NC_WEBDAV_URL:=https://nx4069.your-storageshare.de/public.php/dav/filesdrop/${RAILIANCE_BACKUP_NC_TOKEN}}" if [[ -z "${RAILIANCE_BACKUP_NC_TOKEN:-}" ]]; then - echo "ERROR: set RAILIANCE_BACKUP_NC_TOKEN or run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2 - exit 1 + echo "ERROR: governed backup credential unavailable (CCR-2026-0004)" >&2 + return 1 fi + # Build a file-drop URL only after the required token is present. + : "${RAILIANCE_BACKUP_NC_WEBDAV_URL:=https://nx4069.your-storageshare.de/public.php/dav/filesdrop/${RAILIANCE_BACKUP_NC_TOKEN}}" } railiance_backup_require_tools() { diff --git a/scripts/refresh_live_images.py b/scripts/refresh_live_images.py new file mode 100644 index 0000000..758804b --- /dev/null +++ b/scripts/refresh_live_images.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Atomically accumulate non-secret image exports outside replaceable checkouts.""" +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import tempfile + + +def read_images(path: Path) -> set[str]: + images = set() + for line in path.read_text(encoding="utf-8").splitlines(): + image = line.strip() + if not image or image.startswith("#"): + continue + if any(c.isspace() for c in image) or "/" not in image: + raise ValueError("invalid image export") + images.add(image) + if not images: + raise ValueError("empty image export") + return images + + +def refresh(output: Path, sources: list[Path]) -> dict: + # Read every required export before touching the last known good inventory. + images: set[str] = set() + for source in sources: + images.update(read_images(source)) + if not images: + raise ValueError("at least one nonempty export is required") + output.parent.mkdir(parents=True, exist_ok=True) + with output.with_name(output.name + ".lock").open("a") as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + if output.exists(): + images.update(read_images(output)) + data = ("\n".join(sorted(images)) + "\n").encode() + temporary = None + try: + with tempfile.NamedTemporaryFile(dir=output.parent, delete=False) as stream: + temporary = Path(stream.name) + stream.write(data) + stream.flush() + os.fchmod(stream.fileno(), 0o644) + os.fsync(stream.fileno()) + os.replace(temporary, output) + directory = os.open(output.parent, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + return {"path": str(output), "images": len(images), "sha256": hashlib.sha256(data).hexdigest()} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--source", type=Path, action="append", required=True) + args = parser.parse_args() + try: + receipt = refresh(args.output, args.source) + except (OSError, ValueError): + parser.exit(1, "inventory refresh failed; no successful publication receipt\n") + print(json.dumps(receipt, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_backup_credentials.py b/tests/test_backup_credentials.py new file mode 100644 index 0000000..9baa767 --- /dev/null +++ b/tests/test_backup_credentials.py @@ -0,0 +1,51 @@ +import os +from pathlib import Path +import subprocess +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +class BackupCredentialTests(unittest.TestCase): + def run_shell(self, body): + env = {k: v for k, v in os.environ.items() if not k.startswith("RAILIANCE_BACKUP_")} + return subprocess.run( + ["bash", "-c", 'source lib/railiance-backup-common.sh\n' + body], + cwd=ROOT, env=env, text=True, capture_output=True, + ) + + def test_missing_credentials_fail_with_safe_error(self): + result = self.run_shell( + 'bao() { return 1; }\nrailiance_backup_require_openbao_lane' + ) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, "") + self.assertIn("governed backup credential unavailable", result.stderr) + self.assertNotIn("unbound variable", result.stderr) + + def test_explicit_governed_credentials_need_no_bao(self): + result = self.run_shell(''' +bao() { echo unexpected-bao >&2; return 1; } +RAILIANCE_BACKUP_NC_TOKEN=test-only-placeholder +RAILIANCE_BACKUP_NC_WEBDAV_URL=https://example.invalid/upload +railiance_backup_require_openbao_lane +''') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout + result.stderr, "") + + def test_openbao_lane_supplies_missing_credentials(self): + result = self.run_shell(''' +bao() { + case "$*" in + "kv metadata get "*) return 0 ;; + "kv get -field=NC_WEBDAV_TOKEN "*) echo test-only-placeholder ;; + "kv get -field=NC_WEBDAV_URL "*) echo https://example.invalid/upload ;; + *) return 1 ;; + esac +} +railiance_backup_require_openbao_lane +[[ "$RAILIANCE_BACKUP_NC_TOKEN" == test-only-placeholder ]] +[[ "$RAILIANCE_BACKUP_NC_WEBDAV_URL" == https://example.invalid/upload ]] +''') + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout + result.stderr, "") diff --git a/tests/test_live_images_inventory.py b/tests/test_live_images_inventory.py new file mode 100644 index 0000000..8be8518 --- /dev/null +++ b/tests/test_live_images_inventory.py @@ -0,0 +1,42 @@ +import importlib.util +from pathlib import Path +import tempfile +import unittest + +SPEC = importlib.util.spec_from_file_location( + "refresh_live_images", Path(__file__).resolve().parents[1] / "scripts/refresh_live_images.py" +) +inventory = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(inventory) + + +class InventoryTests(unittest.TestCase): + def test_refresh_preserves_other_clusters_and_previous_tags(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + output = root / "durable" / "images.txt" + first, second = root / "first", root / "second" + first.write_text("forgejo.example/org/app:old\n") + second.write_text("forgejo.example/org/other:live\n") + inventory.refresh(output, [first, second]) + first.write_text("forgejo.example/org/app:new\n") + receipt = inventory.refresh(output, [first]) + self.assertEqual(receipt["images"], 3) + self.assertIn("forgejo.example/org/other:live", output.read_text()) + self.assertIn("forgejo.example/org/app:old", output.read_text()) + self.assertEqual(inventory.refresh(output, [first]), receipt) + + def test_failed_export_preserves_inventory(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + output, source = root / "images", root / "export" + output.write_text("forgejo.example/org/app:live\n") + before = output.read_bytes() + for content in ["", "# no images\n", "error fetching pods\n"]: + source.write_text(content) + with self.assertRaises(ValueError): + inventory.refresh(output, [source]) + self.assertEqual(output.read_bytes(), before) + with self.assertRaises(OSError): + inventory.refresh(output, [root / "missing"]) + self.assertEqual(output.read_bytes(), before) diff --git a/tools/cmd/forgejo-backup b/tools/cmd/forgejo-backup index 099bdc8..de765ab 100755 --- a/tools/cmd/forgejo-backup +++ b/tools/cmd/forgejo-backup @@ -10,7 +10,6 @@ # RAILIANCE_BACKUP_DRY_RUN=1 skip upload and remote side effects set -euo pipefail -: "${RAILIANCE_BACKUP_NC_TOKEN:=MfTBEjcJTGS8Ywo}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck source=lib/railiance-print.sh @@ -55,8 +54,10 @@ cleanup_plain() { } trap cleanup_plain EXIT +if [[ "${DRY_RUN}" != "1" ]]; then + railiance_backup_require_openbao_lane +fi railiance_backup_require_tools -railiance_backup_require_openbao_lane FORGEJO_DB_POD="${FORGEJO_DB_POD:-$(kubectl_get_pod "${FORGEJO_DB_NAMESPACE}" \ "cnpg.io/cluster=${FORGEJO_DB_CLUSTER}" || true)}" diff --git a/workplans/RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md b/workplans/RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md index 36d3850..0c45738 100644 --- a/workplans/RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md +++ b/workplans/RPF-WP-0004-llm-connect-openrouter-provider-key-lane.md @@ -404,3 +404,14 @@ consumers, restart requirements, provider-side revocation for the OpenRouter key) plus the post-rotate verification contract. Front-door disable comes first in every action; audit evidence is never deleted; values stay in OpenBao/operator custody. + + +## Recovery evidence reported 2026-09-04 + +Activity-core message `dcc0d7cc-0046-4c47-ba1c-fb1e0046e18e` supersedes +`c32901ff-2072-4b93-81cd-db93dddfa5f3`. Owner decision +`914e0e04-fd17-4fba-b2d5-be38f7aca185` approved intentional reuse from the +reins provider lane into the canonical activity-core lane. Activity-core reports +KV version 2, ESO synchronization, rollout, health and completion HTTP 200. +This is owner-reported evidence, not an independent platform live verification; +no further replacement was requested. diff --git a/workplans/RPF-WP-0028-durable-live-image-inventory.md b/workplans/RPF-WP-0028-durable-live-image-inventory.md new file mode 100644 index 0000000..eb26605 --- /dev/null +++ b/workplans/RPF-WP-0028-durable-live-image-inventory.md @@ -0,0 +1,36 @@ +--- +id: RPF-WP-0028 +type: workplan +title: "Make Forgejo image protection survive checkout replacement" +domain: financials +repo: railiance-platform +status: blocked +owner: codex +created: "2026-09-05" +updated: "2026-09-05" +--- + +# Make Forgejo image protection survive checkout replacement + +Source: State Hub message `868a2326-cdad-4256-be4c-7cd6c8ec4b5b`. Reviewed against current repository state +on 2026-09-05. Repository implementation is complete; live closure remains pending. + +## Implement and verify durable publication + +```task +id: RPF-WP-0028-T01 +status: done +priority: high +``` + +Implemented scripts/refresh_live_images.py and make live-images-refresh. Publication validates exports before mutation, locks concurrent writers, retains all previous cluster entries, and fsyncs an atomic replacement. Offline tests prove repeatability, multi-cluster retention, and preservation on missing, empty, or malformed input. + +## Install production projection and rollout refresh + +```task +id: RPF-WP-0028-T02 +status: wait +priority: high +``` + +Requires the production host inventory, activity-core directory mount/configuration update, and cluster rollout hook adoption. Follow docs/forgejo-package-prune.md. Preserve the restored union and worker refusal on missing/empty inventory; do not manually trigger prune. No production migration or live verification occurred in this session. diff --git a/workplans/RPF-WP-0029-backup-credential-default-removal.md b/workplans/RPF-WP-0029-backup-credential-default-removal.md new file mode 100644 index 0000000..7582518 --- /dev/null +++ b/workplans/RPF-WP-0029-backup-credential-default-removal.md @@ -0,0 +1,36 @@ +--- +id: RPF-WP-0029 +type: workplan +title: "Remove backup credential default and verify governed replacement" +domain: financials +repo: railiance-platform +status: blocked +owner: codex +created: "2026-09-05" +updated: "2026-09-05" +--- + +# Remove backup credential default and verify governed replacement + +Source: State Hub message `ee702ac9-9118-4b9b-963a-01943052b65a`. Reviewed against current repository state +on 2026-09-05. Repository implementation is complete; live closure remains pending. + +## Remove source fallback and verify fail-closed behavior + +```task +id: RPF-WP-0029-T01 +status: done +priority: high +``` + +Removed the literal upload credential default from tools/cmd/forgejo-backup. Missing governed input now fails before cluster operations with a value-free diagnostic; encryption dry-runs skip upload authentication. Offline tests prove missing-input denial, explicit input, and mocked OpenBao resolution. + +## Invalidate predecessor and prove replacement recovery + +```task +id: RPF-WP-0029-T02 +status: wait +priority: high +``` + +Provider-side invalidation and replacement custody need the attended provider owner and CCR-2026-0004 lifecycle procedure. Record only non-secret invalidation, encrypted upload and restore receipts. No provider authority or replacement receipt was available; source removal alone does not close the reported exposure. Never record the predecessor value, fingerprint, length, or shape.