Add Forgejo break-glass seal/verify tooling and recovery runbook
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

RAPPS-WP-0016-T01: founder-run targets rotate the forgejo_admin password
into OpenBao (platform/workloads/forgejo/break-glass-admin) and record a
value-free sign-in. T01 stays in progress until the founder runs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1031639@bnt-lap001
Assistant-Session: e5073fee-f4ae-4fc3-a75a-6c1fc83e0576
This commit is contained in:
tegwick 2026-10-17 11:32:09 +02:00
parent fbe012789e
commit fe5cabecd6
5 changed files with 166 additions and 1 deletions

View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Seal the Forgejo break-glass administrator (RAPPS-WP-0016-T01).
# Founder-run, interactive. Generates a fresh password for the local site admin,
# stores it only in OpenBao (coulombcore), then sets it in Forgejo. The value is
# never printed, logged or written to disk.
set -euo pipefail
BREAKGLASS_USER="${FORGEJO_BREAKGLASS_USER:-forgejo_admin}"
BREAKGLASS_PATH="${FORGEJO_BREAKGLASS_PATH:-platform/workloads/forgejo/break-glass-admin}"
BAO_ADDR="${BAO_ADDR:-https://bao.coulomb.social}"
FORGEJO_KUBECONFIG="${FORGEJO_KUBECONFIG:-$HOME/.kube/config-hosteurope}"
FORGEJO_NAMESPACE="${FORGEJO_NAMESPACE:-forgejo}"
for cmd in bao kubectl openssl; do
command -v "$cmd" >/dev/null 2>&1 || { echo "ERROR: $cmd not found" >&2; exit 1; }
done
if [[ -z "${BAO_TOKEN:-}" ]]; then
read -r -s -p "OpenBao operator token (coulombcore / bao.coulomb.social): " BAO_TOKEN
echo >&2
fi
[[ -n "${BAO_TOKEN:-}" ]] || { echo "ERROR: empty OpenBao token" >&2; exit 1; }
export BAO_ADDR BAO_TOKEN
if curl -fsS "$BAO_ADDR/v1/sys/health" | grep -q '"sealed":true'; then
echo "ERROR: OpenBao at $BAO_ADDR reports sealed" >&2
exit 1
fi
pod="$(KUBECONFIG="$FORGEJO_KUBECONFIG" kubectl -n "$FORGEJO_NAMESPACE" get pod \
-l app.kubernetes.io/name=gitea -o jsonpath='{.items[0].metadata.name}')"
[[ -n "$pod" ]] || { echo "ERROR: no Forgejo pod in $FORGEJO_NAMESPACE" >&2; exit 1; }
echo "Break-glass user: $BREAKGLASS_USER (site admin, local sign-in)"
echo "Custody path: $BREAKGLASS_PATH (field PASSWORD) on $BAO_ADDR"
echo "Forgejo pod: $pod"
read -r -p "Rotate the break-glass password now? [y/N] " answer
[[ "$answer" == "y" ]] || { echo "Aborted; nothing changed."; exit 1; }
password="$(openssl rand -base64 48 | tr -d '\n/+=' | cut -c1-40)"
# Store first: if this fails, Forgejo still has the old password.
printf '%s' "$password" | bao kv put "$BREAKGLASS_PATH" \
PASSWORD=- USERNAME="$BREAKGLASS_USER" ROTATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" >/dev/null
stored="$(bao kv get -field=PASSWORD "$BREAKGLASS_PATH")"
[[ "$stored" == "$password" ]] || { echo "ERROR: OpenBao read-back mismatch; Forgejo unchanged" >&2; exit 1; }
unset stored
# Password goes to the pod on stdin so it never appears in process arguments
# on the workstation.
printf '%s\n' "$password" | KUBECONFIG="$FORGEJO_KUBECONFIG" kubectl -n "$FORGEJO_NAMESPACE" \
exec -i "$pod" -c gitea -- sh -c \
'read -r p; forgejo admin user change-password --username "$0" --password "$p" --must-change-password=false >/dev/null' \
"$BREAKGLASS_USER"
unset password
echo "Sealed: $BREAKGLASS_USER password rotated and stored at $BREAKGLASS_PATH."
echo "Next: make forgejo-breakglass-verify"

View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Verify one break-glass sign-in (RAPPS-WP-0016-T01) and write value-free evidence.
# Founder-run. Reads the password from OpenBao into memory, authenticates against
# the Forgejo API with local credentials, and records only the outcome.
set -euo pipefail
BREAKGLASS_USER="${FORGEJO_BREAKGLASS_USER:-forgejo_admin}"
BREAKGLASS_PATH="${FORGEJO_BREAKGLASS_PATH:-platform/workloads/forgejo/break-glass-admin}"
BAO_ADDR="${BAO_ADDR:-https://bao.coulomb.social}"
FORGEJO_BASE_URL="${FORGEJO_BASE_URL:-https://forgejo.coulomb.social}"
EVIDENCE="${FORGEJO_BREAKGLASS_EVIDENCE:-docs/evidence/$(date -u +%Y-%m-%d)-forgejo-breakglass-signin.json}"
command -v bao >/dev/null 2>&1 || { echo "ERROR: bao CLI not found" >&2; exit 1; }
if [[ -z "${BAO_TOKEN:-}" ]]; then
read -r -s -p "OpenBao operator token (coulombcore / bao.coulomb.social): " BAO_TOKEN
echo >&2
fi
export BAO_ADDR BAO_TOKEN
password="$(bao kv get -field=PASSWORD "$BREAKGLASS_PATH")"
version="$(bao kv metadata get -format=json "$BREAKGLASS_PATH" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["current_version"])')"
# Credentials reach curl through a config on stdin, not argv.
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
code="$(printf 'user = "%s:%s"\n' "$BREAKGLASS_USER" "$password" \
| curl -sS -K - -o "$body_file" -w '%{http_code}' "$FORGEJO_BASE_URL/api/v1/user")"
unset password
python3 - "$code" "$BREAKGLASS_USER" "$BREAKGLASS_PATH" "$version" "$FORGEJO_BASE_URL" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$EVIDENCE" "$body_file" <<'EOF'
import json, sys
code, user, path, version, base, at, out, body = sys.argv[1:]
try:
me = json.load(open(body))
except ValueError:
me = {}
ok = code == "200" and me.get("login") == user and me.get("is_admin") is True
record = {
"schema": "railiance-apps.forgejo-breakglass-signin.v1",
"task": "RAPPS-WP-0016-T01",
"forgejo": base,
"account": user,
"auth": "local password (API basic auth)",
"http_status": int(code) if code.isdigit() else code,
"login_matches": me.get("login") == user,
"is_admin": me.get("is_admin"),
"custody": {"openbao_path": path, "field": "PASSWORD", "version": version},
"verified_at": at,
"result": "pass" if ok else "fail",
"values_emitted": False,
}
with open(out, "w") as f:
json.dump(record, f, indent=2)
f.write("\n")
print(f"{record['result']}: {user} sign-in (HTTP {code}); evidence {out}")
sys.exit(0 if ok else 1)
EOF