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 <noreply@anthropic.com>
This commit is contained in:
maierch
2026-07-28 16:39:13 +02:00
commit d3638d49ae
12 changed files with 409 additions and 0 deletions
+109
View File
@@ -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 `AG<NN>domain` /
`AG<NN>url` 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 <PROJECT_RUNNER_TOKEN_des_deploy_repos> \
--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<NN>` (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<NN>/`. 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).
+21
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
beispiel-erlaubt.de
docs.example.org
intranet.firma.local
+2
View File
@@ -0,0 +1,2 @@
example.com/team/ag01
downloads.example.org/software/freigegeben
+23
View File
@@ -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
+7
View File
@@ -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"
+74
View File
@@ -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."
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# validate.sh <domain|url> <datei>
# 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"
+31
View File
@@ -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
}
}
+14
View File
@@ -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
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# /usr/local/sbin/squidguard-rebuild.sh <AG0N>
# 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"
+6
View File
@@ -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"