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)
|
forgejo-package-prune-dry-run: ## List Forgejo package versions beyond retention depth (no deletes)
|
||||||
tools/cmd/forgejo-package-prune
|
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
|
forgejo-package-prune: ## Prune Forgejo packages — keep newest 3 versions per package
|
||||||
tools/cmd/forgejo-package-prune --apply
|
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-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-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-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-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-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 | 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-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-T01 | done | — | workplans/ADHOC-2026-08-23.md |
|
||||||
| task | ADHOC-2026-08-23-T02 | 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 |
|
| 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-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-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-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-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-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||||
| task | RPF-WP-0018-T03 | 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-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-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-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-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-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-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-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-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-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-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-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-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
|
with 13 org repos is ~670MiB — allow 10–20 minutes for stream + age on a typical
|
||||||
workstation link.
|
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).
|
Decrypt: `~/.config/age/railiance-backup.key` (same key as other Railiance backups).
|
||||||
|
|
||||||
## Nextcloud layout
|
## Nextcloud layout
|
||||||
|
|
@ -62,4 +69,4 @@ See `railiance-infra`:
|
||||||
- `docs/forgejo-restore-drill-evidence.md`
|
- `docs/forgejo-restore-drill-evidence.md`
|
||||||
|
|
||||||
Download and decrypt a dump from Nextcloud, set `BACKUP_LOCAL` to the zip path,
|
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
|
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`
|
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`,
|
(`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_require_openbao_lane() {
|
||||||
railiance_backup_load_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
|
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
|
echo "ERROR: governed backup credential unavailable (CCR-2026-0004)" >&2
|
||||||
exit 1
|
return 1
|
||||||
fi
|
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() {
|
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
|
# RAILIANCE_BACKUP_DRY_RUN=1 skip upload and remote side effects
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
: "${RAILIANCE_BACKUP_NC_TOKEN:=MfTBEjcJTGS8Ywo}"
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
# shellcheck source=lib/railiance-print.sh
|
# shellcheck source=lib/railiance-print.sh
|
||||||
|
|
@ -55,8 +54,10 @@ cleanup_plain() {
|
||||||
}
|
}
|
||||||
trap cleanup_plain EXIT
|
trap cleanup_plain EXIT
|
||||||
|
|
||||||
|
if [[ "${DRY_RUN}" != "1" ]]; then
|
||||||
|
railiance_backup_require_openbao_lane
|
||||||
|
fi
|
||||||
railiance_backup_require_tools
|
railiance_backup_require_tools
|
||||||
railiance_backup_require_openbao_lane
|
|
||||||
|
|
||||||
FORGEJO_DB_POD="${FORGEJO_DB_POD:-$(kubectl_get_pod "${FORGEJO_DB_NAMESPACE}" \
|
FORGEJO_DB_POD="${FORGEJO_DB_POD:-$(kubectl_get_pod "${FORGEJO_DB_NAMESPACE}" \
|
||||||
"cnpg.io/cluster=${FORGEJO_DB_CLUSTER}" || true)}"
|
"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
|
key) plus the post-rotate verification contract. Front-door disable comes
|
||||||
first in every action; audit evidence is never deleted; values stay in
|
first in every action; audit evidence is never deleted; values stay in
|
||||||
OpenBao/operator custody.
|
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