From 9b31f229e8792585f17c4bc09f0220578d100dd1 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 7 Jul 2026 21:28:46 +0200 Subject: [PATCH] Document reuse-surface runtime secrets and Forgejo org webhook Add runbook coverage for REUSE_SURFACE_TOKEN and REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET in reuse-surface-env, plus an idempotent make reuse-forgejo-webhook target that wires the coulomb org push webhook to the hub receiver. --- Makefile | 23 +++++++- docs/reuse-surface-on-railiance01.md | 80 ++++++++++++++++++++++++---- helm/reuse-surface-values.yaml | 8 +-- tools/reuse-forgejo-webhook.sh | 80 ++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 14 deletions(-) create mode 100755 tools/reuse-forgejo-webhook.sh diff --git a/Makefile b/Makefile index 5c4f4d1..43de42b 100644 --- a/Makefile +++ b/Makefile @@ -246,6 +246,24 @@ forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, forgejo-smoke: ## Verify Forgejo web and OCI registry challenge FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh +forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels + FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh + +forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git) + FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh + +forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key) + KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh + +forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted + @test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; } + @if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \ + grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \ + else \ + echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \ + fi + @echo "ok: Forgejo SOPS sentinels" + forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50 @@ -292,6 +310,9 @@ reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo li reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01 KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50 +reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03) + bash tools/reuse-forgejo-webhook.sh + ##@ Help help: ## Show this help @@ -299,4 +320,4 @@ help: ## Show this help /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) -.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs help +.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs help diff --git a/docs/reuse-surface-on-railiance01.md b/docs/reuse-surface-on-railiance01.md index a30291a..ff964a2 100644 --- a/docs/reuse-surface-on-railiance01.md +++ b/docs/reuse-surface-on-railiance01.md @@ -44,7 +44,74 @@ export REUSE_SURFACE_URL=http://127.0.0.1:18001 | Values | `helm/reuse-surface-values.yaml` | | Image | `gitea.coulomb.social/coulomb/reuse-surface:` | | Landing image | `nginxinc/nginx-unprivileged:1.27-alpine` | -| Secret | `reuse-surface-env` (`REUSE_SURFACE_TOKEN`) | +| Secret | `reuse-surface-env` (see [Runtime secrets](#runtime-secrets)) | + +## Runtime secrets + +Custody is the Kubernetes Secret `reuse-surface-env` in namespace `reuse` on +Railiance01. This is **not** an OpenBao KV path today — see +**RAILIANCE-WP-0011** for a planned OpenBao + External Secrets migration. + +| Key | Required | Purpose | +| --- | --- | --- | +| `REUSE_SURFACE_TOKEN` | yes | Bearer token for hub write API (`POST /v1/repos`, compose, etc.) | +| `REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET` | for webhooks | HMAC secret for `POST /v1/webhooks/forgejo` (REUSE-WP-0019-T02) | + +The chart injects both keys via `envFrom.secretRef` on the `reuse-surface` +Deployment. After patching the Secret, restart the Deployment so new values +are picked up: + +```bash +kubectl --kubeconfig ~/.kube/config-hosteurope rollout restart deployment/reuse-surface -n reuse +kubectl --kubeconfig ~/.kube/config-hosteurope rollout status deployment/reuse-surface -n reuse +``` + +**Create or rotate** (generate values locally — never commit or paste into chat): + +```bash +kubectl create namespace reuse --dry-run=client -o yaml | kubectl apply -f - +kubectl create secret generic reuse-surface-env \ + --namespace reuse \ + --from-literal=REUSE_SURFACE_TOKEN="$(openssl rand -hex 32)" \ + --from-literal=REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET="$(openssl rand -hex 32)" \ + --dry-run=client -o yaml | kubectl apply -f - +kubectl --kubeconfig ~/.kube/config-hosteurope rollout restart deployment/reuse-surface -n reuse +``` + +**Export for a shell session** (streams to your terminal only): + +```bash +export REUSE_SURFACE_TOKEN=$( + kubectl --kubeconfig ~/.kube/config-hosteurope get secret reuse-surface-env -n reuse \ + -o jsonpath='{.data.REUSE_SURFACE_TOKEN}' | base64 -d +) +``` + +Or proxy via ops-warden: `warden access reuse-surface-hub-write-token --fetch`. + +## Forgejo org webhook + +Org-level push webhook on Forgejo (`coulomb`) triggers hub recompose when any +repo changes paths under `registry/indexes/`. The HMAC secret must match +`REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET` in `reuse-surface-env`. + +Requires a Forgejo admin API token at `/tmp/forgejo-tegwick-api-token` (or +`FORGEJO_ADMIN_TOKEN`). Idempotent setup: + +```bash +make reuse-forgejo-webhook +``` + +Manual verification (unsigned body should return `401`, not `503`): + +```bash +curl -fsS -o /dev/null -w "%{http_code}\n" -X POST \ + https://reuse.coulomb.social/v1/webhooks/forgejo \ + -H "Content-Type: application/json" -d '{}' +``` + +Live status (2026-07-07): org webhook id `1` on `coulomb` → +`https://reuse.coulomb.social/v1/webhooks/forgejo`, push events only. ## Browser landing page @@ -71,15 +138,10 @@ the API service. ```bash # 1. Pin image tag in helm/reuse-surface-values.yaml -# 2. Create secret (example — use SOPS handoff in production) -kubectl create namespace reuse --dry-run=client -o yaml | kubectl apply -f - -kubectl create secret generic reuse-surface-env \ - --namespace reuse \ - --from-literal=REUSE_SURFACE_TOKEN='' \ - --dry-run=client -o yaml | kubectl apply -f - - -# Production (Railiance01, defaults to ~/.kube/config-hosteurope) +# 2. Ensure reuse-surface-env has both keys (see Runtime secrets above) +# 3. Production (Railiance01, defaults to ~/.kube/config-hosteurope) make reuse-deploy +make reuse-forgejo-webhook # after webhook secret is in the cluster Secret make reuse-status # Restore kubeconfig from the node if missing: diff --git a/helm/reuse-surface-values.yaml b/helm/reuse-surface-values.yaml index 439be8a..14c7958 100644 --- a/helm/reuse-surface-values.yaml +++ b/helm/reuse-surface-values.yaml @@ -1,8 +1,8 @@ # Production overrides for reuse-surface federation hub. -# REUSE_SURFACE_TOKEN is supplied via Secret reuse-surface-env. -# REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET (REUSE-WP-0019-T02, POST -# /v1/webhooks/forgejo) is the same Secret's job -- not yet added as of -# this image; the webhook endpoint returns 503 until it is. +# Runtime secrets (Secret reuse-surface-env, namespace reuse): +# REUSE_SURFACE_TOKEN — hub write API bearer token +# REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET — HMAC for POST /v1/webhooks/forgejo +# See docs/reuse-surface-on-railiance01.md#runtime-secrets and make reuse-forgejo-webhook. image: tag: "e3ae22e" diff --git a/tools/reuse-forgejo-webhook.sh b/tools/reuse-forgejo-webhook.sh new file mode 100755 index 0000000..15b7320 --- /dev/null +++ b/tools/reuse-forgejo-webhook.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Idempotent org-level Forgejo webhook for reuse-surface hub recompose (REUSE-WP-0019-T03). +# Reads the HMAC secret from the live cluster Secret — never prints it. +set -euo pipefail + +FORGEJO_API="${FORGEJO_API:-https://forgejo.coulomb.social/api/v1}" +FORGEJO_ORG="${FORGEJO_ORG:-coulomb}" +WEBHOOK_URL="${REUSE_WEBHOOK_URL:-https://reuse.coulomb.social/v1/webhooks/forgejo}" +KUBECONFIG_PATH="${KUBECONFIG:-$HOME/.kube/config-hosteurope}" +SECRET_NS="${REUSE_SECRET_NAMESPACE:-reuse}" +SECRET_NAME="${REUSE_SECRET_NAME:-reuse-surface-env}" +SECRET_KEY="${REUSE_WEBHOOK_SECRET_KEY:-REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET}" +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 + +WEBHOOK_SECRET="$( + kubectl --kubeconfig "${KUBECONFIG_PATH}" get secret "${SECRET_NAME}" -n "${SECRET_NS}" \ + -o "jsonpath={.data.${SECRET_KEY}}" | base64 -d +)" +if [[ -z "${WEBHOOK_SECRET}" ]]; then + echo "Missing ${SECRET_KEY} in ${SECRET_NS}/${SECRET_NAME}" >&2 + exit 1 +fi + +export FORGEJO_API FORGEJO_ORG WEBHOOK_URL TOKEN WEBHOOK_SECRET +python3 <<'PY' +import json +import os +import urllib.error +import urllib.request + +api = os.environ["FORGEJO_API"].rstrip("/") +org = os.environ["FORGEJO_ORG"] +url = os.environ["WEBHOOK_URL"] +token = os.environ["TOKEN"] +secret = os.environ["WEBHOOK_SECRET"] +headers = {"Authorization": f"token {token}"} + +def api_json(method: str, path: str, payload: dict | None = None): + data = None if payload is None else json.dumps(payload).encode() + req = urllib.request.Request( + f"{api}{path}", + data=data, + headers={**headers, **({} if payload is None else {"Content-Type": "application/json"})}, + method=method, + ) + with urllib.request.urlopen(req) as resp: + return json.load(resp) + +hooks = api_json("GET", f"/orgs/{org}/hooks") +for hook in hooks: + cfg = hook.get("config") or {} + if cfg.get("url") == url and hook.get("active") and "push" in (hook.get("events") or []): + print(f"==> Forgejo org webhook already configured (id={hook['id']})") + raise SystemExit(0) + +created = api_json( + "POST", + f"/orgs/{org}/hooks", + { + "type": "gitea", + "config": { + "url": url, + "content_type": "json", + "secret": secret, + }, + "events": ["push"], + "active": True, + }, +) +print(f"==> Forgejo org webhook created (id={created['id']}) -> {url}") +PY \ No newline at end of file