Establish railiance backup credentials in OpenBao (CCR-2026-0004).
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

Add workload KV lane for Nextcloud WebDAV token, URL, and age recovery
escrow at platform/workloads/railiance/backup/offsite-lane. Apply read
policy and OIDC role railiance-backup-workload-kv-read; wire forgejo-backup
to load credentials from OpenBao when env is unset.
This commit is contained in:
tegwick 2026-07-07 17:16:30 +02:00
parent f428940da8
commit 0055e8f3f7
8 changed files with 430 additions and 1 deletions

View file

@ -356,9 +356,25 @@ argocd-status: ## Show Railiance ArgoCD projects, root app, and registered repos
##@ Backup
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-hosteurope
backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud
@test -x tools/cmd/railiance-backup || { echo "tools/cmd/railiance-backup not installed; use forgejo-backup for Forgejo." >&2; exit 1; }
sudo tools/cmd/railiance-backup
forgejo-backup: ## Forgejo dump + forgejo-db pg_dump → age → Nextcloud (T04/T09 Option A)
KUBECONFIG="$(RAILIANCE01_KUBECONFIG)" tools/cmd/forgejo-backup
forgejo-backup-dry-run: ## Forgejo backup without Nextcloud upload
KUBECONFIG="$(RAILIANCE01_KUBECONFIG)" RAILIANCE_BACKUP_DRY_RUN=1 tools/cmd/forgejo-backup
forgejo-backup-status: ## Show last Forgejo backup success and 7-day gate
@STAMP="$(HOME)/.cache/railiance/backups/forgejo/success-log"; \
if [[ ! -f "$$STAMP" ]]; then echo "No forgejo backups recorded yet."; exit 1; fi; \
echo "Last success: $$(tail -1 "$$STAMP")"; \
echo "Recent successes:"; tail -7 "$$STAMP"; \
echo "7-day gate: $$(tail -7 "$$STAMP" | wc -l)/7 consecutive days logged (verify cron separately)"
##@ Help
help: ## Show this help
@ -366,4 +382,4 @@ help: ## Show this help
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status help

View file

@ -0,0 +1,116 @@
id: CCR-2026-0004
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: Railiance offsite backup lane (Nextcloud WebDAV + age recovery)
status: active
created: '2026-07-07'
updated: '2026-07-07'
requester:
agent: grok
reason: Move railiance-backup / forgejo-backup credentials from hardcoded script
values into OpenBao custody per DISCTL-WP-0003 credential-separation policy.
review:
required: true
required_approvers:
- platform-operator
comments:
- at: '2026-07-07T15:00:00+00:00'
reviewer: bernd.worsch
decision: approved
comment: "Approved in chat \u2014 establish backup lane credentials in OpenBao\
\ for workstation forgejo-backup and railiance-backup tooling (Option A Nextcloud)."
target:
domain: financials
tenant: railiance
workload: backup
environment: production
purpose: age-encrypted offsite backup upload through Nextcloud WebDAV file drop
openbao:
mount: platform
kv_path: platform/workloads/railiance/backup/offsite-lane
fields:
- NC_WEBDAV_TOKEN
- NC_WEBDAV_URL
- AGE_PRIVATE_KEY
policy_name: workload-kv-read-railiance-backup-offsite-lane
policy_file: openbao/policies/workload-kv-read-railiance-backup-offsite-lane.hcl
auth:
method: oidc
mount: netkingdom
role: railiance-backup-workload-kv-read
allowed_redirect_uris:
- https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
- http://localhost:8250/oidc/callback
- http://127.0.0.1:8250/oidc/callback
oidc_scopes:
- openid
- profile
- email
- groups
user_claim: sub
groups_claim: groups
bound_claims:
groups:
- net-kingdom-admins
bound_claims_confirmed: true
policies:
- workload-kv-read-railiance-backup-offsite-lane
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: railiance-backup-offsite-lane
selector: railiance backup nextcloud webdav token
command: warden access railiance-backup-offsite-lane --fetch NC_WEBDAV_TOKEN
resolvable: false
readiness: applied-pending-verify
delivery:
surface: operator-workstation
target: railiance-platform lib/railiance-backup-common.sh and forgejo-backup via
RAILIANCE_BACKUP_NC_TOKEN env (fetched through OpenBao or warden access)
risk:
classification: high
notes:
- NC_WEBDAV_TOKEN grants upload to the offsite backup file drop.
- "AGE_PRIVATE_KEY decrypts all age-encrypted backup artifacts \u2014 recovery escrow\
\ only."
- Credentials must not be stored on production hosts with delete permission.
- ops-warden must proxy reads as the caller and must not retain values.
verification:
positive:
- Approved net-kingdom-admins identity can read NC_WEBDAV_TOKEN and AGE_PRIVATE_KEY
through OpenBao or ops-warden without printing values.
negative:
- default-policy token denied on the KV data path.
activation_conditions:
- Policy and OIDC role applied with platform-admin/operator authority.
- Secret values provisioned through approved operator custody from existing sources.
- Positive and negative verification recorded with non-secret audit references.
evidence:
- at: '2026-07-07T15:14:59+00:00'
actor: grok
kind: delegated_metadata_apply
result: passed
details:
- Delegated metadata applier ran as grok using local bao CLI ambient authority.
- 'Policy metadata write: sys/policies/acl/workload-kv-read-railiance-backup-offsite-lane'
- 'Auth role metadata write: auth/netkingdom/role/railiance-backup-workload-kv-read'
- No secret values were read, written, printed, or accepted in argv.
- at: '2026-07-07T15:15:28+00:00'
actor: grok
kind: secret_provisioned
result: passed
details:
- KV path platform/workloads/railiance/backup/offsite-lane provisioned with NC_WEBDAV_TOKEN,
NC_WEBDAV_URL, AGE_PRIVATE_KEY; field presence verified without printing values
- 'Delegated metadata apply: policy workload-kv-read-railiance-backup-offsite-lane
and OIDC role railiance-backup-workload-kv-read'
lifecycle:
deactivate: Disable ops-warden catalog entry and detach OIDC role policy; rotate
Nextcloud file-drop token at provider.
rotate: Mint a new Nextcloud upload token, update NC_WEBDAV_TOKEN in OpenBao, and
record non-secret rotation evidence.
compromised: Deactivate front door, rotate Nextcloud token and age key, re-encrypt
and re-upload backup artifacts where feasible, record blast-radius notes.
state_hub:
workplan_id: DISCTL-WP-0003

61
docs/forgejo-backup.md Normal file
View file

@ -0,0 +1,61 @@
# Forgejo backup (railiance01)
Workplan: `RAIL-HO-WP-0005` T04/T09 · Decision: Option A (Nextcloud + age)
## What is backed up
| Artifact | Source | Format |
| --- | --- | --- |
| Blob state | `forgejo dump` in production pod | zip → age |
| PostgreSQL | `pg_dump -Fc` from CNPG `forgejo-db` | custom dump → age |
Covers git repos, packages (OCI/npm/generic), attachments, LFS, avatars, and DB metadata.
## Operator commands
```bash
cd ~/railiance-platform
make forgejo-backup-dry-run # local encrypt only, no upload
make forgejo-backup # encrypt + upload to Nextcloud forgejo/
make forgejo-backup-status # last success + 7-day gate hint
```
Requires: `kubectl`, `age`, `curl`, `KUBECONFIG=~/.kube/config-hosteurope`.
Decrypt: `~/.config/age/railiance-backup.key` (same key as other Railiance backups).
## Nextcloud layout
```
forgejo/forgejo-dump-<timestamp>.zip.age
forgejo/forgejo-db-<timestamp>.sql.age
forgejo/forgejo-dump-weekly-<timestamp>.zip.age # Sundays only
forgejo/forgejo-db-weekly-<timestamp>.sql.age
```
Retention target: **14 daily + 4 weekly** on Nextcloud (operator may prune old
objects in the WebDAV folder; local cache keeps 7 per type).
## Cron (workstation)
```cron
# Daily 02:15 UTC — Forgejo backup (RPO 24h)
15 2 * * * cd $HOME/railiance-platform && make forgejo-backup >>$HOME/.cache/railiance/backups/forgejo/cron.log 2>&1
```
## Promotion gate (tier-3 cutover)
Do not promote further production repos until:
1. `make forgejo-backup` succeeds **7 consecutive days** (check `success-log`).
2. One restore drill uses a Nextcloud artifact (not `/tmp/forgejo-drill/`).
## Restore
See `railiance-infra`:
- `tools/forgejo-restore-drill.sh`
- `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.

View file

@ -228,3 +228,44 @@ runbook: docs/workload-kv-access-lanes.md
Until positive and negative caller verification are complete, ops-warden should
keep the catalog entry in `applied-pending-verify`/non-active state with
`resolvable=false`.
## Railiance offsite backup lane (`CCR-2026-0004`)
Workstation and platform backup tools (`railiance-backup`, `forgejo-backup`) upload
age-encrypted artifacts to the Nextcloud file drop (Option A, 2026-07-09).
| Item | Value |
| --- | --- |
| CCR | `CCR-2026-0004-railiance-backup-offsite-lane` |
| KV mount | `platform` |
| OpenBao CLI path | `platform/workloads/railiance/backup/offsite-lane` |
| Fields | `NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` |
| Read policy | `workload-kv-read-railiance-backup-offsite-lane` |
| Policy file | `openbao/policies/workload-kv-read-railiance-backup-offsite-lane.hcl` |
| OIDC auth mount | `netkingdom` |
| OIDC role | `railiance-backup-workload-kv-read` |
| Bound claim | `groups=net-kingdom-admins` (confirmed via live `platform-admin` role) |
| ops-warden catalog id | `railiance-backup-offsite-lane` (draft until front door verified) |
Caller login:
```bash
bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read
```
Fetch upload token for a backup run (do not log the value):
```bash
export RAILIANCE_BACKUP_NC_TOKEN=$(
bao kv get -field=NC_WEBDAV_TOKEN platform/workloads/railiance/backup/offsite-lane
)
export RAILIANCE_BACKUP_NC_WEBDAV_URL=$(
bao kv get -field=NC_WEBDAV_URL platform/workloads/railiance/backup/offsite-lane
)
```
Or let `lib/railiance-backup-common.sh` load from OpenBao after login when env is unset.
Recovery escrow (`AGE_PRIVATE_KEY`) is stored in the same path for disaster
recovery; prefer the password-manager copy on restore drills per
`railiance-cluster/docs/backup-restore.md`. Fetch only in attended sessions.

View file

@ -4,6 +4,7 @@
#
# Apply: KUBECONFIG=~/.kube/config-hosteurope make forgejo-db-deploy
# Status: make forgejo-db-status
# Backup: make forgejo-backup (pg_dump + forgejo dump → Nextcloud; docs/forgejo-backup.md)
#
# Pre-condition: forgejo-db-credentials Secret in databases namespace.
# See helm/forgejo-db-secret.sops.yaml.template

View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# lib/railiance-backup-common.sh — age encrypt + Nextcloud upload helpers (Option A)
set -euo pipefail
: "${RAILIANCE_BACKUP_AGE_PUBLIC_KEY:=age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz}"
: "${RAILIANCE_BACKUP_BAO_PATH:=platform/workloads/railiance/backup/offsite-lane}"
: "${RAILIANCE_BACKUP_NC_PREFIX:=forgejo}"
railiance_backup_load_openbao_lane() {
if [[ -n "${RAILIANCE_BACKUP_NC_TOKEN:-}" && -n "${RAILIANCE_BACKUP_NC_WEBDAV_URL:-}" ]]; then
return 0
fi
command -v bao >/dev/null 2>&1 || return 0
bao kv metadata get "${RAILIANCE_BACKUP_BAO_PATH}" >/dev/null 2>&1 || return 0
: "${RAILIANCE_BACKUP_NC_TOKEN:=$(bao kv get -field=NC_WEBDAV_TOKEN "${RAILIANCE_BACKUP_BAO_PATH}")}"
: "${RAILIANCE_BACKUP_NC_WEBDAV_URL:=$(bao kv get -field=NC_WEBDAV_URL "${RAILIANCE_BACKUP_BAO_PATH}")}"
}
railiance_backup_require_openbao_lane() {
railiance_backup_load_openbao_lane
: "${RAILIANCE_BACKUP_NC_WEBDAV_URL:=https://nx4069.your-storageshare.de/public.php/webdav}"
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
fi
}
railiance_backup_require_tools() {
local t
for t in age curl kubectl; do
command -v "$t" >/dev/null 2>&1 || { echo "ERROR: missing required tool: $t" >&2; exit 1; }
done
}
railiance_backup_nc_upload() {
local file="$1" remote_name="$2"
local dest="${RAILIANCE_BACKUP_NC_WEBDAV_URL}/${RAILIANCE_BACKUP_NC_PREFIX}/${remote_name}"
curl -sf -u "${RAILIANCE_BACKUP_NC_TOKEN}:" -T "$file" "$dest" \
|| { echo "ERROR: Nextcloud upload failed: ${remote_name}" >&2; return 1; }
}
railiance_backup_encrypt() {
local src="$1" dest="$2"
age -r "${RAILIANCE_BACKUP_AGE_PUBLIC_KEY}" -o "$dest" "$src"
}
railiance_backup_prune_local() {
local dir="$1" pattern="$2" keep="${3:-7}"
find "$dir" -maxdepth 1 -name "$pattern" -type f 2>/dev/null \
| sort -r | tail -n +"$((keep + 1))" | xargs -r rm -f
}
railiance_backup_record_success() {
local stamp_dir="$1" ts="$2"
mkdir -p "$stamp_dir"
echo "$ts" > "${stamp_dir}/.last-success"
echo "$ts" >> "${stamp_dir}/success-log"
}

View file

@ -0,0 +1,13 @@
# Least-privilege read policy for the Railiance offsite backup lane (Option A).
#
# Grants read access to Nextcloud WebDAV upload credentials and age recovery
# escrow used by railiance-backup / forgejo-backup on operator workstations.
# No write, list, or sibling-workload capabilities.
path "platform/data/workloads/railiance/backup/offsite-lane" {
capabilities = ["read"]
}
path "platform/metadata/workloads/railiance/backup/offsite-lane" {
capabilities = ["read"]
}

123
tools/cmd/forgejo-backup Executable file
View file

@ -0,0 +1,123 @@
#!/usr/bin/env bash
# tools/cmd/forgejo-backup — Forgejo Option A backup (RAIL-HO-WP-0005 T04/T09)
#
# Daily: forgejo dump (blob state) + pg_dump (forgejo-db) → age → Nextcloud forgejo/
# Retention: 14 daily + 4 weekly uploads (Sunday); local cache keep 7 per type.
#
# Env:
# KUBECONFIG (default ~/.kube/config-hosteurope)
# RAILIANCE_BACKUP_NC_TOKEN Nextcloud WebDAV token
# 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
source "${ROOT}/lib/railiance-print.sh"
# shellcheck source=lib/railiance-backup-common.sh
source "${ROOT}/lib/railiance-backup-common.sh"
KUBECONFIG="${KUBECONFIG:-${HOME}/.kube/config-hosteurope}"
export KUBECONFIG
DRY_RUN="${RAILIANCE_BACKUP_DRY_RUN:-0}"
FORGEJO_NAMESPACE="${FORGEJO_NAMESPACE:-forgejo}"
FORGEJO_RELEASE="${FORGEJO_RELEASE:-forgejo}"
FORGEJO_DB_NAMESPACE="${FORGEJO_DB_NAMESPACE:-databases}"
FORGEJO_DB_CLUSTER="${FORGEJO_DB_CLUSTER:-forgejo-db}"
FORGEJO_DB_POD="${FORGEJO_DB_POD:-$(kubectl get pods -n "${FORGEJO_DB_NAMESPACE}" \
-l "cnpg.io/cluster=${FORGEJO_DB_CLUSTER}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)}"
BACKUP_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/railiance/backups/forgejo"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
DOW="$(date -u +%u)" # 7 = Sunday
DUMP_PLAIN="${BACKUP_DIR}/forgejo-dump-${TS}.zip"
DUMP_AGE="${DUMP_PLAIN}.age"
DB_PLAIN="${BACKUP_DIR}/forgejo-db-${TS}.sql"
DB_AGE="${DB_PLAIN}.age"
cleanup_plain() {
rm -f "${DUMP_PLAIN}" "${DB_PLAIN}"
}
trap cleanup_plain EXIT
railiance_backup_require_tools
railiance_backup_require_openbao_lane
if [[ -z "${FORGEJO_DB_POD}" ]]; then
bad "preflight" "forgejo-db pod not found (cluster ${FORGEJO_DB_CLUSTER})"
exit 1
fi
PROD_POD="$(kubectl get pods -n "${FORGEJO_NAMESPACE}" \
-l "app.kubernetes.io/instance=${FORGEJO_RELEASE}" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
if [[ -z "${PROD_POD}" ]]; then
bad "preflight" "Forgejo app pod not found in ${FORGEJO_NAMESPACE}"
exit 1
fi
mkdir -p "${BACKUP_DIR}"
print_hdr "Forgejo backup — ${TS}"
ok "kubeconfig" "${KUBECONFIG}"
ok "forgejo pod" "${FORGEJO_NAMESPACE}/${PROD_POD}"
ok "forgejo-db" "${FORGEJO_DB_NAMESPACE}/${FORGEJO_DB_POD}"
# 1. Application dump (repos, packages, attachments, LFS, avatars)
ok "forgejo dump" "running in pod…"
DUMP_REMOTE="/tmp/forgejo-backup-${TS}.zip"
kubectl exec -n "${FORGEJO_NAMESPACE}" "${PROD_POD}" -c gitea -- \
forgejo dump -f "${DUMP_REMOTE}"
# cat stream avoids kubectl cp websocket drops on larger archives
kubectl exec -n "${FORGEJO_NAMESPACE}" "${PROD_POD}" -c gitea -- \
cat "${DUMP_REMOTE}" > "${DUMP_PLAIN}"
kubectl exec -n "${FORGEJO_NAMESPACE}" "${PROD_POD}" -c gitea -- \
rm -f "${DUMP_REMOTE}"
ok "forgejo dump" "$(du -h "${DUMP_PLAIN}" | awk '{print $1}') → $(basename "${DUMP_PLAIN}")"
# 2. PostgreSQL logical dump (stdout; CNPG data dir is read-only for ad-hoc paths)
ok "pg_dump" "database forgejo…"
kubectl exec -n "${FORGEJO_DB_NAMESPACE}" "${FORGEJO_DB_POD}" -c postgres -- \
pg_dump -U postgres -d forgejo -Fc --no-owner --no-acl -f - > "${DB_PLAIN}"
ok "pg_dump" "$(du -h "${DB_PLAIN}" | awk '{print $1}') → $(basename "${DB_PLAIN}")"
# 3. Encrypt
railiance_backup_encrypt "${DUMP_PLAIN}" "${DUMP_AGE}"
railiance_backup_encrypt "${DB_PLAIN}" "${DB_AGE}"
ok "age" "encrypted dump + db"
if [[ "${DRY_RUN}" == "1" ]]; then
warn "dry-run" "skipping Nextcloud upload"
else
railiance_backup_nc_upload "${DUMP_AGE}" "forgejo-dump-${TS}.zip.age"
ok "upload" "forgejo-dump-${TS}.zip.age"
railiance_backup_nc_upload "${DB_AGE}" "forgejo-db-${TS}.sql.age"
ok "upload" "forgejo-db-${TS}.sql.age"
if [[ "${DOW}" == "7" ]]; then
railiance_backup_nc_upload "${DUMP_AGE}" "forgejo-dump-weekly-${TS}.zip.age"
railiance_backup_nc_upload "${DB_AGE}" "forgejo-db-weekly-${TS}.sql.age"
ok "weekly" "Sunday copies uploaded"
fi
railiance_backup_record_success "${BACKUP_DIR}" "${TS}"
fi
# 4. Local cache prune (keep 7 per type)
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-dump-*.zip.age' 7
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-db-*.sql.age' 7
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-dump-*.zip' 3
railiance_backup_prune_local "${BACKUP_DIR}" 'forgejo-db-*.sql' 3
ok "cache" "local copies pruned"
echo
ok "done" "Forgejo backup complete — ${TS}"
if [[ "${DRY_RUN}" != "1" ]]; then
echo " Dump: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-dump-${TS}.zip.age"
echo " DB: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-db-${TS}.sql.age"
echo
echo " Decrypt with ~/.config/age/railiance-backup.key"
echo " Restore drill: railiance-infra tools/forgejo-restore-drill.sh"
fi