Add Forgejo T05 verify, operator bootstrap, and security hardening
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 3s

Introduce forgejo-verify acceptance checks, idempotent operator bootstrap,
runner registration SOPS capture helper, and session/security Helm values.
This commit is contained in:
tegwick 2026-07-07 22:09:53 +02:00
parent 9b31f229e8
commit 3a9236148a
8 changed files with 436 additions and 1 deletions

34
tools/forgejo-npm-smoke.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# T07 npm registry smoke — install published @coulomb/forgejo-npm-probe (no publish).
set -euo pipefail
REGISTRY="${FORGEJO_NPM_REGISTRY:-https://forgejo.coulomb.social/api/packages/coulomb/npm/}"
TOKEN_FILE="${FORGEJO_NPM_TOKEN_FILE:-${FORGEJO_TOKEN_FILE:-/tmp/forgejo-tegwick-api-token}}"
PROBE_PKG="${FORGEJO_NPM_PROBE_PKG:-@coulomb/forgejo-npm-probe@0.0.1}"
if [[ -n "${FORGEJO_NPM_TOKEN:-}" ]]; then
TOKEN="${FORGEJO_NPM_TOKEN}"
elif [[ -f "${TOKEN_FILE}" ]]; then
TOKEN="$(cat "${TOKEN_FILE}")"
else
echo "Set FORGEJO_NPM_TOKEN or create ${TOKEN_FILE}" >&2
exit 1
fi
command -v npm >/dev/null 2>&1 || { echo "npm required" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "${work}"' EXIT
cd "${work}"
npm config set @coulomb:registry "${REGISTRY}"
npm config set -- '//forgejo.coulomb.social/api/packages/coulomb/npm/:_authToken' "${TOKEN}"
npm install "${PROBE_PKG}" >/dev/null
out="$(node -e "console.log(require('@coulomb/forgejo-npm-probe')())")"
if [[ "${out}" == "forgejo-npm-probe-ok" ]]; then
echo "npm smoke ok: ${PROBE_PKG}"
else
echo "npm smoke fail: unexpected output '${out}'" >&2
exit 1
fi

View file

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Idempotent operator account bootstrap for Forgejo (RAIL-HO-WP-0005 T05).
# Does not commit secrets — uses API token from file or env.
set -euo pipefail
FORGEJO_API="${FORGEJO_API:-https://forgejo.coulomb.social/api/v1}"
FORGEJO_ORG="${FORGEJO_ORG:-coulomb}"
FORGEJO_OPERATOR_USER="${FORGEJO_OPERATOR_USER:-tegwick}"
FORGEJO_OPERATOR_EMAIL="${FORGEJO_OPERATOR_EMAIL:-bernd.worsch@gmail.com}"
FORGEJO_OPERATOR_SSH_KEY="${FORGEJO_OPERATOR_SSH_KEY:-$HOME/.ssh/id_gitea.pub}"
FORGEJO_OPERATOR_SSH_TITLE="${FORGEJO_OPERATOR_SSH_TITLE:-workstation-automation}"
TOKEN_FILE="${FORGEJO_TOKEN_FILE:-/tmp/forgejo-tegwick-api-token}"
if [[ -n "${FORGEJO_ADMIN_TOKEN:-}" ]]; then
TOKEN="${FORGEJO_ADMIN_TOKEN}"
elif [[ -f "${TOKEN_FILE}" ]]; then
TOKEN="$(cat "${TOKEN_FILE}")"
else
echo "Set FORGEJO_ADMIN_TOKEN or create ${TOKEN_FILE}" >&2
exit 1
fi
AUTH=(-H "Authorization: token ${TOKEN}")
user_status() {
curl -sS -o /dev/null -w '%{http_code}' "${AUTH[@]}" "${FORGEJO_API}/users/${FORGEJO_OPERATOR_USER}"
}
echo "==> Forgejo operator bootstrap: ${FORGEJO_OPERATOR_USER}"
case "$(user_status)" in
200)
echo " user exists"
;;
404)
echo " creating user ${FORGEJO_OPERATOR_USER}"
FORGEJO_OPERATOR_USER="${FORGEJO_OPERATOR_USER}" \
FORGEJO_OPERATOR_EMAIL="${FORGEJO_OPERATOR_EMAIL}" \
curl -fsS -X POST "${FORGEJO_API}/admin/users" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json, os
print(json.dumps({
'username': os.environ['FORGEJO_OPERATOR_USER'],
'email': os.environ['FORGEJO_OPERATOR_EMAIL'],
'password': os.urandom(18).hex(),
'must_change_password': False,
'send_notify': False,
}))
")" >/dev/null
;;
*)
echo "Unexpected user lookup status for ${FORGEJO_OPERATOR_USER}" >&2
exit 1
;;
esac
# Promote to site admin
curl -fsS -X PATCH "${FORGEJO_API}/admin/users/${FORGEJO_OPERATOR_USER}" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d '{"admin":true,"prohibit_login":false}' >/dev/null
echo " admin=true"
# Org Owners team membership
team_id="$(curl -fsS "${AUTH[@]}" "${FORGEJO_API}/orgs/${FORGEJO_ORG}/teams" \
| python3 -c "import sys,json; teams=json.load(sys.stdin); print(next((t['id'] for t in teams if t.get('name')=='Owners'), ''))")"
if [[ -n "${team_id}" ]]; then
curl -fsS -X PUT "${AUTH[@]}" \
"${FORGEJO_API}/teams/${team_id}/members/${FORGEJO_OPERATOR_USER}" >/dev/null 2>&1 \
&& echo " org ${FORGEJO_ORG} Owners team" \
|| echo " (already in Owners team or API skipped)"
fi
# SSH key for git+ssh
if [[ ! -f "${FORGEJO_OPERATOR_SSH_KEY}" ]]; then
echo "Missing SSH public key: ${FORGEJO_OPERATOR_SSH_KEY}" >&2
exit 1
fi
existing="$(curl -fsS "${AUTH[@]}" "${FORGEJO_API}/users/${FORGEJO_OPERATOR_USER}/keys" \
| FORGEJO_OPERATOR_SSH_KEY="${FORGEJO_OPERATOR_SSH_KEY}" python3 -c "
import json, os, sys
want = open(os.environ['FORGEJO_OPERATOR_SSH_KEY']).read().strip()
keys = json.load(sys.stdin)
print(any((k.get('key') or '').strip() == want for k in keys))
")"
if [[ "${existing}" == "True" ]]; then
echo " SSH key already present"
else
FORGEJO_OPERATOR_SSH_TITLE="${FORGEJO_OPERATOR_SSH_TITLE}" \
FORGEJO_OPERATOR_SSH_KEY="${FORGEJO_OPERATOR_SSH_KEY}" \
curl -fsS -X POST "${FORGEJO_API}/admin/users/${FORGEJO_OPERATOR_USER}/keys" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json, os
print(json.dumps({
'title': os.environ['FORGEJO_OPERATOR_SSH_TITLE'],
'key': open(os.environ['FORGEJO_OPERATOR_SSH_KEY']).read().strip(),
}))
")" >/dev/null
echo " SSH key added (${FORGEJO_OPERATOR_SSH_TITLE})"
fi
echo "==> Operator bootstrap complete: ${FORGEJO_OPERATOR_USER}"
echo " Test: ssh forgejo-remote # expect Hi there, ${FORGEJO_OPERATOR_USER}!"

View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# One-time: capture cluster runner registration token into SOPS (operator host with age key).
# RAIL-HO-WP-0005 T05 — closes runner secret gap when bootstrap used kubectl create secret.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="${ROOT}/helm/forgejo-runner-registration.sops.yaml"
KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config-hosteurope}"
FORGEJO_NAMESPACE="${FORGEJO_NAMESPACE:-forgejo}"
export KUBECONFIG
if ! command -v sops >/dev/null 2>&1; then
echo "ERROR: sops required" >&2
exit 1
fi
if [[ -f "${OUT}" ]]; then
echo "Already exists: ${OUT}" >&2
echo "Remove it first if you intend to re-capture from cluster." >&2
exit 1
fi
token="$(kubectl get secret forgejo-runner-registration -n "${FORGEJO_NAMESPACE}" \
-o jsonpath='{.data.token}' | base64 -d)"
if [[ -z "${token}" ]]; then
echo "ERROR: forgejo-runner-registration secret missing or empty in ${FORGEJO_NAMESPACE}" >&2
exit 1
fi
cat > "${OUT}" <<EOF
apiVersion: v1
kind: Secret
metadata:
name: forgejo-runner-registration
namespace: ${FORGEJO_NAMESPACE}
type: Opaque
stringData:
token: ${token}
EOF
sops --encrypt --in-place "${OUT}"
echo "Created ${OUT}"
echo "Verify: SOPS_SENTINEL=${OUT} make check-sops"

133
tools/forgejo-verify.sh Executable file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env bash
# T05 acceptance checks for production Forgejo on railiance01 (RAIL-HO-WP-0005).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BASE_URL="${FORGEJO_BASE_URL:-https://forgejo.coulomb.social}"
KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config-hosteurope}"
FORGEJO_NAMESPACE="${FORGEJO_NAMESPACE:-forgejo}"
FORGEJO_DB_NAMESPACE="${FORGEJO_DB_NAMESPACE:-databases}"
FORGEJO_DB_CLUSTER="${FORGEJO_DB_CLUSTER:-forgejo-db}"
FORGEJO_OPERATOR_USER="${FORGEJO_OPERATOR_USER:-tegwick}"
TOKEN_FILE="${FORGEJO_TOKEN_FILE:-/tmp/forgejo-tegwick-api-token}"
export KUBECONFIG
pass=0
fail=0
warn=0
ok() { echo " PASS $*"; pass=$((pass + 1)); }
bad() { echo " FAIL $*" >&2; fail=$((fail + 1)); }
note() { echo " WARN $*"; warn=$((warn + 1)); }
echo "Forgejo T05 verify — ${BASE_URL}"
echo "kubeconfig: ${KUBECONFIG}"
echo
# Web + OCI registry smoke
if FORGEJO_BASE_URL="${BASE_URL}" "${ROOT}/tools/forgejo-smoke.sh" >/dev/null 2>&1; then
ok "web + OCI registry smoke"
else
bad "web + OCI registry smoke"
fi
# Kubernetes workload
if kubectl get pod -n "${FORGEJO_NAMESPACE}" -l app.kubernetes.io/instance=forgejo \
-o jsonpath='{.items[0].status.phase}' 2>/dev/null | grep -q Running; then
ok "forgejo app pod Running"
else
bad "forgejo app pod not Running"
fi
if kubectl get deploy forgejo-runner -n "${FORGEJO_NAMESPACE}" >/dev/null 2>&1; then
ready="$(kubectl get deploy forgejo-runner -n "${FORGEJO_NAMESPACE}" \
-o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo 0/0)"
if [[ "${ready}" == "1/1" ]]; then
ok "forgejo-runner ready (${ready})"
else
note "forgejo-runner replicas ${ready}"
fi
else
bad "forgejo-runner deployment missing"
fi
# TLS ingress
if kubectl get certificate forgejo-tls -n "${FORGEJO_NAMESPACE}" \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q True; then
ok "TLS certificate Ready"
else
bad "TLS certificate not Ready"
fi
# Database
if kubectl get cluster "${FORGEJO_DB_CLUSTER}" -n "${FORGEJO_DB_NAMESPACE}" \
-o jsonpath='{.status.phase}' 2>/dev/null | grep -qi healthy; then
ok "forgejo-db cluster healthy"
else
bad "forgejo-db cluster not healthy"
fi
# Mailer via OpenBao/ESO (not SOPS)
if kubectl get externalsecret forgejo-mailer -n "${FORGEJO_NAMESPACE}" \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q True; then
ok "forgejo-mailer ExternalSecret Ready"
else
bad "forgejo-mailer ExternalSecret not Ready"
fi
# SOPS secret files (encrypted sentinel — decrypt optional)
for f in helm/forgejo-secrets.sops.yaml helm/forgejo-runner-registration.sops.yaml; do
if [[ -f "${ROOT}/${f}" ]] && grep -q '^sops:' "${ROOT}/${f}"; then
ok "SOPS file present: ${f}"
if command -v sops >/dev/null 2>&1 && sops -d "${ROOT}/${f}" >/dev/null 2>&1; then
ok "SOPS decrypt: ${f}"
else
note "SOPS decrypt skipped (no sops or age key): ${f}"
fi
elif [[ "${f}" == *runner-registration* ]]; then
if kubectl get secret forgejo-runner-registration -n "${FORGEJO_NAMESPACE}" >/dev/null 2>&1; then
note "runner registration in cluster only — encrypt helm/forgejo-runner-registration.sops.yaml on operator host with age key"
else
bad "missing runner registration secret"
fi
else
bad "missing SOPS file: ${f}"
fi
done
# Operator SSH (tegwick, not forgejo_admin)
if ssh -o BatchMode=yes -o ConnectTimeout=8 forgejo-remote 2>&1 | grep -q "Hi there, ${FORGEJO_OPERATOR_USER}!"; then
ok "SSH forgejo-remote authenticates as ${FORGEJO_OPERATOR_USER}"
else
bad "SSH forgejo-remote does not authenticate as ${FORGEJO_OPERATOR_USER}"
fi
# Operator API account
if [[ -f "${TOKEN_FILE}" ]]; then
token="$(cat "${TOKEN_FILE}")"
admin="$(curl -fsS -H "Authorization: token ${token}" \
"${BASE_URL}/api/v1/users/${FORGEJO_OPERATOR_USER}" | python3 -c \
"import sys,json; print(json.load(sys.stdin).get('is_admin', False))" 2>/dev/null || echo false)"
if [[ "${admin}" == "True" ]]; then
ok "operator user ${FORGEJO_OPERATOR_USER} is admin (API)"
else
bad "operator user ${FORGEJO_OPERATOR_USER} is not admin"
fi
else
note "no API token at ${TOKEN_FILE} — skip operator admin API check"
fi
# Git HTTPS reachability (no creds — expect 401/200 on repo HEAD)
code="$(curl -sS -o /dev/null -w '%{http_code}' "${BASE_URL}/coulomb/glas-harness")"
if [[ "${code}" == "200" ]]; then
ok "public repo HTTPS (${code})"
else
bad "public repo HTTPS unexpected (${code})"
fi
echo
echo "Summary: ${pass} pass, ${fail} fail, ${warn} warn"
if [[ "${fail}" -gt 0 ]]; then
exit 1
fi