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.
This commit is contained in:
parent
f428940da8
commit
0055e8f3f7
8 changed files with 430 additions and 1 deletions
18
Makefile
18
Makefile
|
|
@ -356,9 +356,25 @@ argocd-status: ## Show Railiance ArgoCD projects, root app, and registered repos
|
||||||
|
|
||||||
##@ Backup
|
##@ Backup
|
||||||
|
|
||||||
|
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-hosteurope
|
||||||
|
|
||||||
backup: ## Backup platform services (PostgreSQL logical dump) — age-encrypted to Nextcloud
|
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
|
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
|
||||||
|
|
||||||
help: ## Show this 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 } \
|
/^[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)
|
/^##@/ { 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
|
||||||
|
|
|
||||||
|
|
@ -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
61
docs/forgejo-backup.md
Normal 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.
|
||||||
|
|
@ -228,3 +228,44 @@ runbook: docs/workload-kv-access-lanes.md
|
||||||
Until positive and negative caller verification are complete, ops-warden should
|
Until positive and negative caller verification are complete, ops-warden should
|
||||||
keep the catalog entry in `applied-pending-verify`/non-active state with
|
keep the catalog entry in `applied-pending-verify`/non-active state with
|
||||||
`resolvable=false`.
|
`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.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#
|
#
|
||||||
# Apply: KUBECONFIG=~/.kube/config-hosteurope make forgejo-db-deploy
|
# Apply: KUBECONFIG=~/.kube/config-hosteurope make forgejo-db-deploy
|
||||||
# Status: make forgejo-db-status
|
# 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.
|
# Pre-condition: forgejo-db-credentials Secret in databases namespace.
|
||||||
# See helm/forgejo-db-secret.sops.yaml.template
|
# See helm/forgejo-db-secret.sops.yaml.template
|
||||||
|
|
|
||||||
58
lib/railiance-backup-common.sh
Normal file
58
lib/railiance-backup-common.sh
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -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
123
tools/cmd/forgejo-backup
Executable 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue