diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 992596c..eafe0d0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,3 +24,85 @@ verify: if grep -q "ENC\[" "$f"; then echo "OK: $f ist verschluesselt"; else echo "WARNUNG: $f ist moeglicherweise NICHT verschluesselt!"; fi done - echo "Flux reconciled die Aenderungen innerhalb ~1 Minute (Quelle Gitea-Mirror)." + +# --------------------------------------------------------------------------- +# TURN-Rotation kanonisieren (laeuft NUR als Pipeline-Schedule) +# +# Der Rotations-CronJob laeuft im Cluster und erreicht git.lab nicht - er pusht +# seinen Branch deshalb nach Gitea. Von dort muss die Rotation ueber git.lab +# zurueck, sonst ueberschreibt der naechste Mirror-Push sie und Flux spielt still +# das ALTE Shared Secret wieder ein. Frueher war das ein monatlicher Handgriff; +# dieser Job erledigt ihn. +# +# Einrichtung (einmalig, braucht Rechte - siehe CLAUDE.md): +# 1. Project Access Token, Rolle Maintainer, Scope write_repository +# -> CI/CD-Variable CANONIZE_TOKEN (masked + protected) +# 2. Pipeline-Schedule anlegen, taeglich, z. B. "17 5 * * *" +# +# Warum taeglich statt monatlich zum Rotationstermin: ein monatlicher Schedule +# hat genau einen Versuch. Faellt der Runner an dem Tag aus, faellt die +# Kanonisierung ein Monat lang aus. Taeglich holt der Job jede offene Rotation +# spaetestens am Folgetag nach und ist ansonsten in Sekunden fertig. +# --------------------------------------------------------------------------- +canonize_rotation: + image: alpine:3.20 + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + variables: + GIT_DEPTH: "0" # volle Historie: wir brauchen merge-base + script: + - apk add --no-cache git ca-certificates >/dev/null + - | + # Lab-CA: der Runner legt sie je nach Version an unterschiedlichen Orten ab. + for c in /etc/gitlab-runner/certs/git.lab.crt /etc/gitlab-runner/certs/ca.crt; do + [ -f "$c" ] && export GIT_SSL_CAINFO="$c" && break + done + echo "CA: ${GIT_SSL_CAINFO:-Systemspeicher}" + - | + if [ -z "$CANONIZE_TOKEN" ]; then + echo "CANONIZE_TOKEN fehlt - siehe Kopf dieser Datei, Schritt 1." + exit 1 + fi + - git config --global user.email "ci@axion1337.chat" + - git config --global user.name "TURN-Rotation (automatische Kanonisierung)" + - git remote add gitea https://rohana.axion1337.de/sorb/axion1337.chat-gitops.git + - git fetch --quiet gitea + - git fetch --quiet origin main + - git checkout -B main origin/main + - | + MERGED=0 + for ref in $(git for-each-ref --format='%(refname:short)' 'refs/remotes/gitea/turn-secret-rotation-*'); do + SHA=$(git rev-parse "$ref") + if git merge-base --is-ancestor "$SHA" HEAD; then + echo "bereits kanonisiert: $ref" + continue + fi + echo "kanonisiere: $ref ($SHA)" + SUBJ="chore(coturn): TURN-Rotation aus ${ref#gitea/} uebernommen" + BODY="Automatisch kanonisiert: der Rotations-CronJob im Cluster erreicht git.lab nicht und pusht nach Gitea; dieser Commit bringt die Rotation auf den kanonischen Weg zurueck, bevor der Mirror sie ueberschreiben kann." + if ! git merge --no-ff -m "$SUBJ" -m "$BODY" "$SHA"; then + echo "MERGE-KONFLIKT in $ref - es wird nichts gepusht." + echo "Von Hand aufloesen, siehe CLAUDE.md (Abschnitt Repo Topology)." + exit 1 + fi + MERGED=1 + done + echo "MERGED=$MERGED" > .canonize_state + - | + . ./.canonize_state + if [ "$MERGED" = "0" ]; then + echo "Keine offene Rotation - nichts zu tun." + exit 0 + fi + # Sicherheitsnetz: die Rotation fasst genau diese Secrets an. Kaeme hier + # etwas Unverschluesseltes durch, waere das Shared Secret im Klartext in Git. + for f in apps/production/coturn-secret.yaml apps/production/synapse-turn-secret.yaml; do + if ! grep -q "ENC\[" "$f"; then + echo "ABBRUCH: $f ist nicht SOPS-verschluesselt - es wird nichts gepusht." + exit 1 + fi + echo "OK: $f ist verschluesselt" + done + git push "https://oauth2:${CANONIZE_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:main + echo "Gepusht. Der Mirror traegt es nach Gitea zurueck, der PR schliesst sich dort selbst;" + echo "der Rotations-Branch existiert dann nur noch auf Gitea und wird vom Mirror entfernt." diff --git a/CLAUDE.md b/CLAUDE.md index 6f24ff4..f488e74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,8 +39,17 @@ Gitea; the mirror delivers). **Never push directly to Gitea** for this repo — force-overwrites divergent state. The same rule applies to ThreadNet-Web, threadnet-call, thread-net-git, threadnet-operating and (since 2026-08-01) `management` (the former `Backlogs` repo, renamed in the PM-framework restructuring — ADRs/vision/roadmap live -there). **Since 2026-08-02 the wiki lives on git.lab too** — there is no direct-to-Gitea -exception left. +there). Since 2026-08-02 the wiki lives on git.lab too, so **no repo is authored on +Gitea any more**; the one process that still *writes* there is the TURN rotation +(see below). + +**Why Gitea is the Flux source, and why that is not a leftover.** git.lab holds the +blueprints; Gitea holds a copy the cluster can reach without the lab being up. That +separation is deliberate: the Hetzner cluster must be buildable and re-deployable when +the homelab is offline, on holiday, or mid-rebuild — it therefore must not depend on a +host that only answers inside the lab. **Do not "fix" the Flux source to point at +git.lab**: it would look tidier and would couple production availability to the lab, +which is exactly what this split avoids. **Issues live on git.lab** (migrated 2026-08-01, gitops#48): the old Gitea issues are closed with a pointer to their GitLab counterpart. ⚠️ gitops issue numbers **shifted** @@ -58,11 +67,22 @@ it is a stale May snapshot of `docs/`; don't edit or trust it. All doc sources Docusaurus site at **wiki.lab**, configured in `git.lab/homelab/wiki` — content is pulled at build time, so edits always belong in the source repo. -**Exception that needs manual handling**: the monthly TURN-rotation CronJob runs in the -cluster (no route to git.lab) and still opens its PR on Gitea. Never merge that PR on -Gitea — instead fetch the rotation branch, merge it into main locally, push to git.lab; -the mirror carries it back, Gitea auto-closes the PR (worked example: 2026-08-01, -commit 640c934). If the mirror lags, force it: GitLab API +**The one write that still lands on Gitea**: the monthly TURN-rotation CronJob runs in +the cluster (no route to git.lab) and pushes its rotation branch to Gitea. Never merge +that PR on Gitea — the rotation has to travel back through git.lab, otherwise the next +mirror push overwrites it and Flux silently re-applies the *old* shared secret. + +**This is automated — do not do it by hand.** The scheduled job `canonize_rotation` in +`.gitlab-ci.yml` runs daily on git.lab, picks up any `turn-secret-rotation-*` branch +from Gitea that is not yet in `main`, merges it, and pushes to git.lab; the mirror +carries it back and Gitea auto-closes the PR. Once merged, the branch exists only on +Gitea, so the next mirror run deletes it — no cleanup needed. + +If the job fails (merge conflict, or the SOPS check finds an unencrypted secret file), +it fails **loudly and changes nothing** — the pipeline stays red until someone looks. +That red pipeline is the alarm; there is no separate reminder. Manual fallback, should +it ever be needed: fetch the branch, merge into main locally, push to git.lab (worked +example: 2026-08-01, commit `640c934`). If the mirror lags, force it via the GitLab API `POST /projects//remote_mirrors//sync`. ## Repository Structure