commit d3638d49ae993a8a65d1f9d86c2f41fb2f4b7150 Author: maierch Date: Tue Jul 28 16:39:13 2026 +0200 SquidGuard-Listen self-managed via GitLab (Shell-Runner) Beispiel-Setup: ein Repo je AG fuer Zugriffskontrolle, zentrales Deploy-Repo mit Validierung, Rollout auf beide Proxys, DB-Rebuild je Gruppe und Auto-Rollback auf den letzten guten Stand. Co-Authored-By: Claude Opus 4.8 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d840534 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# SquidGuard-Listen per GitLab self-managed (Shell-Runner) + +## Idee / Sicherheitsmodell + +- **Ein Repo pro Arbeitsgruppe** (`squidguard/ag01` .. `squidguard/ag08`). + GitLab-Rechte am Projekt = wer die beiden Dateien `AGdomain` / + `AGurl` aendern darf. Nur AG01-Mitglieder haben Developer im Repo + `ag01`. Das ist die eigentliche Zugriffskontrolle. +- **Ein zentrales Deploy-Repo** (`squidguard/deploy`), nur Ops beschreibbar. + Hier liegen Deploy-Skript und Secrets. Die AG-Repos enthalten KEINE + Secrets -> auch wenn eine AG ihre `.gitlab-ci.yml` frei bearbeitet, kann + sie damit nur die zentrale Pipeline anstossen, nichts ausspaehen. +- **Dedizierter Shell-Runner** nur fuer das Deploy-Repo (nicht mit den + AG-Repos teilen!), Tag `squidguard-shell`. + +## Dateibaum + + deploy-repo/ -> Inhalt des Repos squidguard/deploy + .gitlab-ci.yml + scripts/validate.sh + scripts/deploy.sh + config/proxies.env + ag-repo/ -> Vorlage fuer jedes Repo squidguard/agNN + .gitlab-ci.yml (optional, nur fuer Sofort-Deploy) + AG01domain + AG01url + proxy/ -> auf BEIDE Squid-Proxies installieren + usr-local-sbin-squidguard-rebuild.sh + usr-local-sbin-squidguard-reload.sh + sudoers.d-squidguard-deploy + squidGuard.conf.snippet + +## Einrichtung Proxys (proxy1 + proxy2, je identisch) + + # Deploy-User anlegen + useradd -m -s /bin/bash deploy + install -d -o deploy -g deploy /var/lib/squidguard/incoming + install -d -o proxy -g proxy /var/lib/squidguard/db + install -d -o proxy -g proxy /var/lib/squidguard/backup # fuer Auto-Rollback + + # Wrapper-Skripte installieren + install -o root -g root -m 0755 usr-local-sbin-squidguard-rebuild.sh /usr/local/sbin/squidguard-rebuild.sh + install -o root -g root -m 0755 usr-local-sbin-squidguard-reload.sh /usr/local/sbin/squidguard-reload.sh + + # sudoers (WICHTIG: 0440, vorher pruefen) + install -o root -g root -m 0440 sudoers.d-squidguard-deploy /etc/sudoers.d/squidguard-deploy + visudo -c + + # SSH-Key des Runner-Users in ~deploy/.ssh/authorized_keys eintragen + # squidGuard.conf.snippet in /etc/squidguard/squidGuard.conf einbauen + +Hinweis: `PROXY_USER` in `squidguard-rebuild.sh` ist unter Debian/Ubuntu +`proxy`, unter RHEL/CentOS `squid` -> ggf. anpassen. + +## Einrichtung Shell-Runner (auf dem Runner-Host) + + # Runner registrieren, an Projekt squidguard/deploy gebunden + gitlab-runner register \ + --url https://gitlab.intern/ \ + --token \ + --executor shell \ + --tag-list squidguard-shell + + # Als Runner-User (meist gitlab-runner): SSH-Key + known_hosts anlegen + sudo -u gitlab-runner ssh-keygen -t ed25519 -N '' -f ~gitlab-runner/.ssh/id_ed25519 + sudo -u gitlab-runner ssh-keyscan proxy1.intern proxy2.intern >> ~gitlab-runner/.ssh/known_hosts + # der oeffentliche Key gehoert in ~deploy/.ssh/authorized_keys beider Proxys + +## Einrichtung GitLab + +1. Gruppe `squidguard` anlegen, darin Projekte `ag01`..`ag08` und `deploy`. +2. Je AG-Repo: nur die AG-Mitglieder als **Developer** hinzufuegen. + Optional `main` als Protected Branch (Maintainer merged). +3. Group-Deploy-Token mit Scope `read_repository` erstellen -> + im Deploy-Repo als CI/CD-Variablen `GIT_USER` / `GIT_TOKEN` hinterlegen + (protected + masked). +4. Deploy-Repo: Pipeline-Zeitplan (Schedule) z.B. alle 5 Minuten anlegen. +5. Optional Sofort-Deploy: im Deploy-Repo ein Trigger-Token erzeugen und in + jedem AG-Repo als `DEPLOY_TRIGGER_TOKEN` + `DEPLOY_PROJECT_ID` + hinterlegen, dann `ag-repo/.gitlab-ci.yml` verwenden. + +## Ablauf + +Push in `ag01/main` -> (optional Trigger) -> zentrale Pipeline auf dem +Shell-Runner -> `deploy.sh`: klont alle AG-Repos, validiert, rsynct auf +beide Proxys, `squidguard-rebuild.sh AG` (DB je Gruppe neu), +am Ende `squidguard-reload.sh` (squid -k reconfigure). + +Eine ungueltige Zeile wird schon in der CI-Validierung abgefangen und +gelangt gar nicht erst auf die Proxys. + +## Auto-Rollback auf dem Proxy + +`squidguard-rebuild.sh` sichert vor jeder Uebernahme den letzten +funktionierenden Stand einer Gruppe (Quelllisten + `.db`) nach +`/var/lib/squidguard/backup/AG/`. Schlaegt der Rebuild fehl - +erkannt an Exit-Code, Fehlermeldungen in der Ausgabe oder einer leeren +`.db` -, wird dieser Stand automatisch zurueckgespielt. Die Gruppe +laeuft damit unveraendert weiter, statt offline zu gehen. + +`deploy.sh` behandelt das pro Gruppe isoliert: eine fehlgeschlagene +Gruppe wird uebersprungen (und zurueckgerollt), alle anderen Gruppen +gehen normal live, und Squid wird trotzdem neu geladen. Am Ende endet +die Pipeline mit Exit-Code 1 und listet die betroffenen `proxy/AG` +auf, damit der Fehler sichtbar bleibt. + +Da die CI-Validierung Formatfehler bereits vorher abfaengt, ist der +Rollback vor allem die zweite Sicherung gegen Proxy-seitige Probleme +(z.B. squidGuard-Version, Rechte, kaputte Bestands-.db). diff --git a/ag-repo/.gitlab-ci.yml b/ag-repo/.gitlab-ci.yml new file mode 100644 index 0000000..e51131d --- /dev/null +++ b/ag-repo/.gitlab-ci.yml @@ -0,0 +1,21 @@ +# Liegt in JEDEM AG-Repo (ag01 .. ag08). Optional - nur fuer sofortigen Deploy. +# Enthaelt BEWUSST keine Deploy-Secrets, sondern loest nur die zentrale +# Pipeline aus. Ein Trigger-Token kann nur ausloesen, nichts ausspaehen. +# +# CI/CD-Variablen im AG-Repo: +# DEPLOY_TRIGGER_TOKEN - Pipeline-Trigger-Token des Deploy-Repos +# DEPLOY_PROJECT_ID - Projekt-ID des Deploy-Repos + +stages: + - trigger + +trigger-deploy: + stage: trigger + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + script: + - > + curl -sf -X POST + -F token=$DEPLOY_TRIGGER_TOKEN + -F ref=main + "https://$CI_SERVER_HOST/api/v4/projects/$DEPLOY_PROJECT_ID/trigger/pipeline" diff --git a/ag-repo/AG01domain b/ag-repo/AG01domain new file mode 100644 index 0000000..ae8da96 --- /dev/null +++ b/ag-repo/AG01domain @@ -0,0 +1,3 @@ +beispiel-erlaubt.de +docs.example.org +intranet.firma.local diff --git a/ag-repo/AG01url b/ag-repo/AG01url new file mode 100644 index 0000000..e12dacd --- /dev/null +++ b/ag-repo/AG01url @@ -0,0 +1,2 @@ +example.com/team/ag01 +downloads.example.org/software/freigegeben diff --git a/deploy-repo/.gitlab-ci.yml b/deploy-repo/.gitlab-ci.yml new file mode 100644 index 0000000..d951bc8 --- /dev/null +++ b/deploy-repo/.gitlab-ci.yml @@ -0,0 +1,23 @@ +# Zentrale Deploy-Pipeline. Liegt im NUR fuer Ops beschreibbaren Repo. +# Laeuft auf einem dedizierten Shell-Runner (Tag: squidguard-shell), +# der ausschliesslich diesem Projekt zugeordnet ist. + +stages: + - deploy + +deploy: + stage: deploy + tags: + - squidguard-shell + rules: + # Zeitplan (z.B. alle 5 Min) -> regelmaessiger Abgleich + - if: '$CI_PIPELINE_SOURCE == "schedule"' + # von einem AG-Repo per Trigger ausgeloest -> sofortiger Abgleich + - if: '$CI_PIPELINE_SOURCE == "trigger"' + # manuell ueber die GitLab-Oberflaeche + - if: '$CI_PIPELINE_SOURCE == "web"' + # Aenderung an der Deploy-Logik selbst + - if: '$CI_COMMIT_BRANCH == "main"' + script: + - chmod +x scripts/*.sh + - ./scripts/deploy.sh diff --git a/deploy-repo/config/proxies.env b/deploy-repo/config/proxies.env new file mode 100644 index 0000000..e29e57e --- /dev/null +++ b/deploy-repo/config/proxies.env @@ -0,0 +1,7 @@ +# Ziel-Proxies (SSH-Hostnamen) und der Deploy-User auf den Proxies. +# Wird von deploy.sh eingelesen. +PROXIES="proxy1.intern proxy2.intern" +SSH_USER="deploy" + +# GitLab-Gruppe/Namespace, unter der die AG-Repos liegen (ag01 .. ag08) +NAMESPACE="squidguard" diff --git a/deploy-repo/scripts/deploy.sh b/deploy-repo/scripts/deploy.sh new file mode 100755 index 0000000..b729fd5 --- /dev/null +++ b/deploy-repo/scripts/deploy.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# deploy.sh +# Laeuft im zentralen Deploy-Repo auf dem Shell-Runner: +# 1. klont alle AG-Repos (read-only Deploy-Token) +# 2. validiert alle Listen +# 3. rollt sie NUR bei fehlerfreier Validierung auf beide Proxies aus +# 4. baut je Gruppe die .db neu und laedt Squid einmal pro Proxy neu +# +# Erwartete CI/CD-Variablen (im Deploy-Repo, maskiert/protected): +# GIT_USER - Name des Group-Deploy-Tokens (read_repository) +# GIT_TOKEN - Wert des Group-Deploy-Tokens +# SSH-Key + known_hosts liegen im Home des Runner-Users (nicht als CI-Variable). +set -euo pipefail + +here=$(cd "$(dirname "$0")/.." && pwd) +# shellcheck source=/dev/null +source "$here/config/proxies.env" + +: "${GIT_USER:?GIT_USER fehlt}" +: "${GIT_TOKEN:?GIT_TOKEN fehlt}" +: "${CI_SERVER_HOST:?CI_SERVER_HOST fehlt (von GitLab CI gesetzt)}" + +groups="AG01 AG02 AG03 AG04 AG05 AG06 AG07 AG08" + +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT + +# --- 1) + 2) Klonen und validieren -------------------------------------- +for ag in $groups; do + echo "== Pruefe $ag ==" + repo="https://${GIT_USER}:${GIT_TOKEN}@${CI_SERVER_HOST}/${NAMESPACE}/${ag,,}.git" + git clone --depth 1 -q "$repo" "$workdir/$ag" + + "$here/scripts/validate.sh" domain "$workdir/$ag/${ag}domain" + "$here/scripts/validate.sh" url "$workdir/$ag/${ag}url" +done + +# --- 3) + 4) Ausrollen --------------------------------------------------- +SSH="ssh -o BatchMode=yes -o StrictHostKeyChecking=yes" +failed="" # sammelt "proxy/AG" der fehlgeschlagenen Rebuilds + +for proxy in $PROXIES; do + echo "== Deploy nach $proxy ==" + + for ag in $groups; do + # Dateien in den Staging-Bereich des Proxys legen + $SSH "${SSH_USER}@${proxy}" "mkdir -p /var/lib/squidguard/incoming/${ag}" + rsync -e "$SSH" -a --checksum \ + "$workdir/$ag/${ag}domain" "$workdir/$ag/${ag}url" \ + "${SSH_USER}@${proxy}:/var/lib/squidguard/incoming/${ag}/" + + # Uebernahme + DB-Rebuild dieser Gruppe (privilegiert, aber eng begrenzt). + # Schlaegt der Rebuild fehl, rollt das Proxy-Skript die Gruppe auf ihren + # letzten guten Stand zurueck. Wir merken uns den Fehler, machen aber + # mit den restlichen Gruppen weiter, damit deren gute Aenderungen live gehen. + if $SSH "${SSH_USER}@${proxy}" "sudo /usr/local/sbin/squidguard-rebuild.sh ${ag}"; then + : + else + echo "WARNUNG: Rebuild ${ag} auf ${proxy} fehlgeschlagen (Rollback aktiv)" >&2 + failed="${failed} ${proxy}/${ag}" + fi + done + + # Squid einmal pro Proxy neu laden, damit die neuen .db aktiv werden. + # Auch bei Einzelfehlern: die erfolgreich gebauten Gruppen sollen live gehen. + $SSH "${SSH_USER}@${proxy}" "sudo /usr/local/sbin/squidguard-reload.sh" +done + +if [ -n "$failed" ]; then + echo "Deploy mit Fehlern abgeschlossen. Zurueckgerollt:${failed}" >&2 + exit 1 +fi + +echo "Deploy abgeschlossen." diff --git a/deploy-repo/scripts/validate.sh b/deploy-repo/scripts/validate.sh new file mode 100755 index 0000000..a62fb06 --- /dev/null +++ b/deploy-repo/scripts/validate.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# validate.sh +# Prueft eine squidGuard-Domain- bzw. URL-Liste auf gueltiges Format, +# damit eine kaputte Zeile nicht den DB-Rebuild auf dem Proxy stoert. +set -euo pipefail + +type=${1:?Typ fehlt (domain|url)} +file=${2:?Datei fehlt} + +[ -f "$file" ] || { echo "Datei fehlt: $file" >&2; exit 1; } + +domain_re='^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$' +url_re='^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(/[^[:space:]]*)?$' + +errors=0 +lineno=0 +while IFS= read -r line || [ -n "$line" ]; do + lineno=$((lineno + 1)) + + # Leerzeilen sind erlaubt und werden ignoriert + [ -z "${line//[[:space:]]/}" ] && continue + + # squidGuard-DB-Listen kennen KEINE Kommentare -> '#' waere eine echte Domain + case "$line" in + \#*) echo "$file:$lineno: Kommentare (#) sind in squidGuard-Listen nicht erlaubt" >&2 + errors=$((errors + 1)); continue ;; + esac + + # kein Schema + if printf '%s' "$line" | grep -qiE '://'; then + echo "$file:$lineno: Schema (z.B. http://) nicht erlaubt: $line" >&2 + errors=$((errors + 1)); continue + fi + + # keine Grossbuchstaben (squidGuard matcht case-sensitiv auf lowercase) + lower=$(printf '%s' "$line" | tr 'A-Z' 'a-z') + if [ "$lower" != "$line" ]; then + echo "$file:$lineno: Grossbuchstaben nicht erlaubt: $line" >&2 + errors=$((errors + 1)); continue + fi + + case "$type" in + domain) + printf '%s' "$line" | grep -qE "$domain_re" \ + || { echo "$file:$lineno: keine gueltige Domain: $line" >&2; errors=$((errors + 1)); } ;; + url) + printf '%s' "$line" | grep -qE "$url_re" \ + || { echo "$file:$lineno: keine gueltige URL: $line" >&2; errors=$((errors + 1)); } ;; + *) echo "Unbekannter Typ: $type" >&2; exit 2 ;; + esac +done < "$file" + +if [ "$errors" -gt 0 ]; then + echo "Validierung fehlgeschlagen: $errors Fehler in $file" >&2 + exit 1 +fi +echo "OK: $file" diff --git a/proxy/squidGuard.conf.snippet b/proxy/squidGuard.conf.snippet new file mode 100644 index 0000000..fc5b78d --- /dev/null +++ b/proxy/squidGuard.conf.snippet @@ -0,0 +1,31 @@ +# Ausschnitt fuer /etc/squidguard/squidGuard.conf +# dbhome muss zu squidguard-rebuild.sh (DB=/var/lib/squidguard/db) passen. + +dbhome /var/lib/squidguard/db +logdir /var/log/squidguard + +# --- IP-Zuordnung der Rechner je Arbeitsgruppe --------------------------- +src ag01 { ip 10.0.1.11 10.0.1.12 } # die 8 Rechner der AG01 ... +src ag02 { ip 10.0.2.11 10.0.2.12 } +# ... ag03 .. ag08 analog + +# --- Ziel-Listen je Gruppe (self-managed via GitLab) --------------------- +dest ag01 { + domainlist AG01/AG01domain + urllist AG01/AG01url +} +dest ag02 { + domainlist AG02/AG02domain + urllist AG02/AG02url +} +# ... dest ag03 .. ag08 analog + +acl { + ag01 { pass ag01 none } # AG01 darf nur ihre eigenen Ziele + ag02 { pass ag02 none } + # ... ag03 .. ag08 analog + default { + pass none + redirect http://intranet.firma.local/blocked?url=%u + } +} diff --git a/proxy/sudoers.d-squidguard-deploy b/proxy/sudoers.d-squidguard-deploy new file mode 100644 index 0000000..0af5795 --- /dev/null +++ b/proxy/sudoers.d-squidguard-deploy @@ -0,0 +1,14 @@ +# /etc/sudoers.d/squidguard-deploy (chmod 0440, chown root:root) +# Der Deploy-User darf NUR die beiden Wrapper-Skripte aufrufen, mit exakt +# diesen Argumenten. Kein Shell-Zugriff, keine anderen Kommandos. +# Pruefen mit: visudo -c -f /etc/sudoers.d/squidguard-deploy + +deploy ALL=(root) NOPASSWD: /usr/local/sbin/squidguard-rebuild.sh AG01, \ + /usr/local/sbin/squidguard-rebuild.sh AG02, \ + /usr/local/sbin/squidguard-rebuild.sh AG03, \ + /usr/local/sbin/squidguard-rebuild.sh AG04, \ + /usr/local/sbin/squidguard-rebuild.sh AG05, \ + /usr/local/sbin/squidguard-rebuild.sh AG06, \ + /usr/local/sbin/squidguard-rebuild.sh AG07, \ + /usr/local/sbin/squidguard-rebuild.sh AG08, \ + /usr/local/sbin/squidguard-reload.sh diff --git a/proxy/usr-local-sbin-squidguard-rebuild.sh b/proxy/usr-local-sbin-squidguard-rebuild.sh new file mode 100755 index 0000000..00d0307 --- /dev/null +++ b/proxy/usr-local-sbin-squidguard-rebuild.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# /usr/local/sbin/squidguard-rebuild.sh +# Root-owned (0755). Wird vom Deploy-User via sudo aufgerufen. +# Uebernimmt die Staging-Dateien einer Gruppe und baut NUR deren .db neu. +# Bei einem Fehler wird der letzte funktionierende Stand automatisch +# zurueckgerollt, damit die Gruppe nicht offline geht. +set -euo pipefail + +ag=${1:-} +case "$ag" in + AG0[1-8]) ;; # nur die erlaubten Gruppen + *) echo "Ungueltige Gruppe: '$ag'" >&2; exit 2 ;; +esac + +DB=/var/lib/squidguard/db +IN=/var/lib/squidguard/incoming +BK=/var/lib/squidguard/backup +CONF=/etc/squidguard/squidGuard.conf +PROXY_USER=proxy # Debian/Ubuntu: proxy RHEL: squid + +for f in "${ag}domain" "${ag}url"; do + [ -f "$IN/$ag/$f" ] || { echo "Staging-Datei fehlt: $IN/$ag/$f" >&2; exit 3; } +done + +install -d -o "$PROXY_USER" -g "$PROXY_USER" -m 0755 "$DB/$ag" "$BK/$ag" + +# --- 1) Letzten funktionierenden Stand sichern (Quelllisten + .db) -------- +# rsync spiegelt exakt (auch Loeschungen), sodass BK immer der letzte +# erfolgreich uebernommene Stand dieser Gruppe ist. +rsync -a --delete "$DB/$ag/" "$BK/$ag/" + +restore() { + echo "Rebuild fuer $ag fehlgeschlagen -> Rollback auf letzten guten Stand" >&2 + rsync -a --delete "$BK/$ag/" "$DB/$ag/" +} + +# --- 2) Neue Quelllisten uebernehmen ------------------------------------- +install -o "$PROXY_USER" -g "$PROXY_USER" -m 0644 "$IN/$ag/${ag}domain" "$DB/$ag/${ag}domain" +install -o "$PROXY_USER" -g "$PROXY_USER" -m 0644 "$IN/$ag/${ag}url" "$DB/$ag/${ag}url" + +# --- 3) .db je Liste neu bauen, als Squid-User, mit Fehlererkennung ------ +rebuild_one() { + local list=$1 out rc + out=$(sudo -u "$PROXY_USER" squidGuard -c "$CONF" -C "$list" 2>&1) || rc=$? + rc=${rc:-0} + echo "$out" + # squidGuard liefert nicht immer einen aussagekraeftigen Exit-Code: + # zusaetzlich stderr und Existenz/Groesse der .db pruefen. + if [ "$rc" -ne 0 ] \ + || printf '%s' "$out" | grep -qiE 'error|fatal|went not through' \ + || [ ! -s "$DB/$list.db" ]; then + return 1 + fi + return 0 +} + +if ! rebuild_one "$ag/${ag}domain" || ! rebuild_one "$ag/${ag}url"; then + restore + exit 4 +fi + +echo "rebuilt $ag" diff --git a/proxy/usr-local-sbin-squidguard-reload.sh b/proxy/usr-local-sbin-squidguard-reload.sh new file mode 100755 index 0000000..ee26e61 --- /dev/null +++ b/proxy/usr-local-sbin-squidguard-reload.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# /usr/local/sbin/squidguard-reload.sh +# Root-owned (0755). Laedt Squid neu, damit die frisch gebauten .db aktiv werden. +set -euo pipefail +squid -k reconfigure +echo "squid reloaded"