From 0055e8f3f7265e068bac4b385dc391e7da7f5759 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 7 Jul 2026 17:16:30 +0200 Subject: [PATCH] Establish railiance backup credentials in OpenBao (CCR-2026-0004). 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. --- Makefile | 18 ++- ...26-0004-railiance-backup-offsite-lane.yaml | 116 +++++++++++++++++ docs/forgejo-backup.md | 61 +++++++++ docs/workload-kv-access-lanes.md | 41 ++++++ helm/forgejo-db-cluster.yaml | 1 + lib/railiance-backup-common.sh | 58 +++++++++ ...-kv-read-railiance-backup-offsite-lane.hcl | 13 ++ tools/cmd/forgejo-backup | 123 ++++++++++++++++++ 8 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 credential-change-requests/CCR-2026-0004-railiance-backup-offsite-lane.yaml create mode 100644 docs/forgejo-backup.md create mode 100644 lib/railiance-backup-common.sh create mode 100644 openbao/policies/workload-kv-read-railiance-backup-offsite-lane.hcl create mode 100755 tools/cmd/forgejo-backup diff --git a/Makefile b/Makefile index bc58aa5..72087ee 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/credential-change-requests/CCR-2026-0004-railiance-backup-offsite-lane.yaml b/credential-change-requests/CCR-2026-0004-railiance-backup-offsite-lane.yaml new file mode 100644 index 0000000..8482d78 --- /dev/null +++ b/credential-change-requests/CCR-2026-0004-railiance-backup-offsite-lane.yaml @@ -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 diff --git a/docs/forgejo-backup.md b/docs/forgejo-backup.md new file mode 100644 index 0000000..2660879 --- /dev/null +++ b/docs/forgejo-backup.md @@ -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-.zip.age +forgejo/forgejo-db-.sql.age +forgejo/forgejo-dump-weekly-.zip.age # Sundays only +forgejo/forgejo-db-weekly-.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. \ No newline at end of file diff --git a/docs/workload-kv-access-lanes.md b/docs/workload-kv-access-lanes.md index 6a48b66..b82251e 100644 --- a/docs/workload-kv-access-lanes.md +++ b/docs/workload-kv-access-lanes.md @@ -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. diff --git a/helm/forgejo-db-cluster.yaml b/helm/forgejo-db-cluster.yaml index 7900d32..a02aab9 100644 --- a/helm/forgejo-db-cluster.yaml +++ b/helm/forgejo-db-cluster.yaml @@ -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 diff --git a/lib/railiance-backup-common.sh b/lib/railiance-backup-common.sh new file mode 100644 index 0000000..3c02564 --- /dev/null +++ b/lib/railiance-backup-common.sh @@ -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" +} \ No newline at end of file diff --git a/openbao/policies/workload-kv-read-railiance-backup-offsite-lane.hcl b/openbao/policies/workload-kv-read-railiance-backup-offsite-lane.hcl new file mode 100644 index 0000000..cf0a250 --- /dev/null +++ b/openbao/policies/workload-kv-read-railiance-backup-offsite-lane.hcl @@ -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"] +} \ No newline at end of file diff --git a/tools/cmd/forgejo-backup b/tools/cmd/forgejo-backup new file mode 100755 index 0000000..f5bc0a6 --- /dev/null +++ b/tools/cmd/forgejo-backup @@ -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 \ No newline at end of file