Harden backup credentials and add durable image inventory publication
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
parent
62423fd092
commit
0349a08e1b
13 changed files with 365 additions and 23 deletions
8
Makefile
8
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
44
docs/evidence/repo-review-2026-09-05.md
Normal file
44
docs/evidence/repo-review-2026-09-05.md
Normal file
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
then run the drill script.
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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/<token>/ — 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() {
|
||||
|
|
|
|||
75
scripts/refresh_live_images.py
Normal file
75
scripts/refresh_live_images.py
Normal file
|
|
@ -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())
|
||||
51
tests/test_backup_credentials.py
Normal file
51
tests/test_backup_credentials.py
Normal file
|
|
@ -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, "")
|
||||
42
tests/test_live_images_inventory.py
Normal file
42
tests/test_live_images_inventory.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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)}"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
36
workplans/RPF-WP-0028-durable-live-image-inventory.md
Normal file
36
workplans/RPF-WP-0028-durable-live-image-inventory.md
Normal file
|
|
@ -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.
|
||||
36
workplans/RPF-WP-0029-backup-credential-default-removal.md
Normal file
36
workplans/RPF-WP-0029-backup-credential-default-removal.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue