New setup of Traefik with crowdsec
This commit is contained in:
13
crowdsec/acquis.yaml
Normal file
13
crowdsec/acquis.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
# CrowdSec acquisition for Traefik access logs
|
||||
# Gemountet nach /etc/crowdsec/acquis.d/traefik.yaml (siehe docker-compose.yml)
|
||||
# Liest das Access-Log, das Traefik in das geteilte Named Volume traefik-logs
|
||||
# schreibt (accessLog.filePath: /var/log/traefik/access.log in traefik.yml).
|
||||
# Das Label "traefik" aktiviert die Parser der crowdsecurity/traefik-Collection.
|
||||
filenames:
|
||||
- /var/log/traefik/access.log
|
||||
# force_inotify: CrowdSec ueberwacht das Verzeichnis per inotify, auch wenn die
|
||||
# Datei beim Start noch nicht existiert. Loest das Cold-Boot-Problem: CrowdSec
|
||||
# startet (depends_on) vor Traefik, das Access-Log entsteht erst danach.
|
||||
force_inotify: true
|
||||
labels:
|
||||
type: traefik
|
||||
61
docker-compose.yml
Normal file
61
docker-compose.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
# docker-compose.yml
|
||||
# Traefik + CrowdSec - migriert von CreateTraefikPod.sh
|
||||
#
|
||||
# Laeuft mit `docker compose` UND `podman compose` (Podman >= 4.1).
|
||||
# Wegen Bind auf :443 und :1194 ist ROOTFUL noetig (rootless kann <1024 nur
|
||||
# mit net.ipv4.ip_unprivileged_port_start-Tweak).
|
||||
# Die Volume-Mounts behalten dein SELinux-Relabel (:Z) bei.
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.7 # aktuell v3.7.5 (Jun 2026), enthaelt Fix fuer CVE-2026-22045 (TLS-ALPN)
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- crowdsec
|
||||
networks:
|
||||
- proxy
|
||||
volumes:
|
||||
# deine bestehende statische + dynamische Config (unveraendert uebernommen)
|
||||
- /srv/TRAEFIK/etc/traefik:/etc/traefik:Z
|
||||
# Access-Log -> geteiltes Named Volume, das CrowdSec read-only mountet
|
||||
- traefik-logs:/var/log/traefik
|
||||
ports:
|
||||
- "192.168.0.141:1180:1180"
|
||||
- "192.168.0.142:443:443"
|
||||
- "192.168.0.142:8880:8880"
|
||||
- "192.168.0.142:40022:40022"
|
||||
- "192.168.0.142:8888:8888"
|
||||
- "192.168.0.142:54321:54321/udp"
|
||||
- "192.168.0.142:5001:5001"
|
||||
- "192.168.0.142:5000:5000"
|
||||
- "192.168.0.142:8443:8443"
|
||||
- "192.168.0.142:1194:1194"
|
||||
- "192.168.0.142:1194:1194/udp"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.7.8 # v1.7.x benoetigt das /var/lib/crowdsec/data Volume (vorhanden)
|
||||
container_name: crowdsec
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- proxy
|
||||
environment:
|
||||
# Collections, die die Traefik-Logs parsen und Scan-/CVE-/DoS-Muster erkennen
|
||||
COLLECTIONS: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/base-http-scenarios crowdsecurity/http-dos"
|
||||
# GID: "1000" # nur setzen, falls CrowdSec das Access-Log nicht lesen darf (Permissions)
|
||||
volumes:
|
||||
- crowdsec-config:/etc/crowdsec # Named Volume -> Image-Defaults bleiben erhalten
|
||||
- crowdsec-db:/var/lib/crowdsec/data
|
||||
- traefik-logs:/var/log/traefik:ro # liest Traefiks Access-Log
|
||||
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.d/traefik.yaml:ro,Z
|
||||
ports:
|
||||
- "192.168.0.142:6060:6060" # Prometheus-Metriken (CrowdSec) fuer 192.168.0.23
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
traefik-logs:
|
||||
crowdsec-config:
|
||||
crowdsec-db:
|
||||
63
scripts/README-cert-sync.md
Normal file
63
scripts/README-cert-sync.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Synology → Traefik certificate sync
|
||||
|
||||
`sync-synology-certs.sh` pulls the LE certs that the Synologies already manage
|
||||
(for `fids.famfi.dyndns.org` / `fids2.famfi.dyndns.org`) and installs them into
|
||||
Traefik's `tls/` dir, so Traefik terminates TLS with a **valid** cert while the
|
||||
**CrowdSec** bouncer stays in the request path.
|
||||
|
||||
Result: clients get a valid cert, the Synology keeps owning cert *acquisition*,
|
||||
and we keep HTTP-level protection. The `fids`/`fids2` routers stay `tls: {}` —
|
||||
once these certs are loaded, Traefik serves them by SNI automatically.
|
||||
|
||||
## One-time setup
|
||||
|
||||
### 1. SSH key from the Traefik host to each Synology
|
||||
```bash
|
||||
sudo ssh-keygen -t ed25519 -f /root/.ssh/synology_certsync -N "" # if no key yet
|
||||
# DSM: Control Panel → Terminal & SNMP → Enable SSH service
|
||||
# Add the PUBLIC key to each Synology user's authorized_keys:
|
||||
ssh-copy-id -i /root/.ssh/synology_certsync.pub admin@192.168.0.245
|
||||
ssh-copy-id -i /root/.ssh/synology_certsync.pub admin@192.168.0.234
|
||||
```
|
||||
Add to `/root/.ssh/config` so the script's plain `ssh` uses the key:
|
||||
```
|
||||
Host 192.168.0.245 192.168.0.234
|
||||
User admin
|
||||
IdentityFile /root/.ssh/synology_certsync
|
||||
```
|
||||
|
||||
### 2. Allow the SSH user to read the certs without a password
|
||||
The certs live in `/usr/syno/etc/certificate/_archive/` (root-only). On each NAS,
|
||||
DSM → Control Panel → Task Scheduler, or edit sudoers, to grant NOPASSWD:
|
||||
```
|
||||
# /etc/sudoers.d/certsync on the Synology
|
||||
admin ALL=(root) NOPASSWD: /bin/sh
|
||||
```
|
||||
(Scope this tighter if you prefer; the script calls `sudo sh -c '…cat…'`.)
|
||||
|
||||
### 3. Test manually
|
||||
```bash
|
||||
sudo /home/lutz/Projects/Traefik/scripts/sync-synology-certs.sh
|
||||
```
|
||||
Expect "updated cert for fids …". Then verify Traefik serves it:
|
||||
```bash
|
||||
echo | openssl s_client -connect 192.168.0.142:5001 \
|
||||
-servername fids.famfi.dyndns.org 2>/dev/null | openssl x509 -noout -subject -issuer
|
||||
# subject should be CN=fids.famfi.dyndns.org, issuer Let's Encrypt (not TRAEFIK DEFAULT CERT)
|
||||
```
|
||||
|
||||
### 4. Schedule (root cron — daily is plenty; LE renews ~monthly)
|
||||
```
|
||||
# /etc/cron.d/synology-certsync
|
||||
17 4 * * * root /home/lutz/Projects/Traefik/scripts/sync-synology-certs.sh >> /var/log/synology-certsync.log 2>&1
|
||||
```
|
||||
|
||||
## How reload works
|
||||
The script regenerates `traefik.d/tls-synology.yml` on every run. That file is in
|
||||
Traefik's watched config dir, so writing it triggers a hot-reload — **no restart
|
||||
needed**. Until the first successful run, `tls-synology.yml` does not exist and
|
||||
Traefik keeps serving its self-signed default for fids/fids2 (no errors).
|
||||
|
||||
## Adding more hosts
|
||||
Append a line to the `TARGETS=( … )` array in the script:
|
||||
`"name|user@host|port|domain.to.match"`.
|
||||
113
scripts/sync-synology-certs.sh
Executable file
113
scripts/sync-synology-certs.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sync-synology-certs.sh
|
||||
# -----------------------
|
||||
# Pulls the current Let's Encrypt certificate (fullchain + private key) for a
|
||||
# given hostname FROM a Synology NAS and installs it into Traefik's tls/ dir,
|
||||
# then triggers a Traefik hot-reload (via the file provider watch).
|
||||
#
|
||||
# WHY: fids/fids2 are Synology backends that obtain their own LE certs. We want
|
||||
# Traefik to TERMINATE TLS with a VALID cert (so CrowdSec can still inspect the
|
||||
# request) instead of its self-signed default. Rather than have Traefik fetch
|
||||
# its own certs, we copy the ones the Synology already manages.
|
||||
#
|
||||
# RUN AS ROOT (writes under /srv/TRAEFIK, root-owned). Typically via cron.
|
||||
#
|
||||
# Prereqsuites (see README-cert-sync.md):
|
||||
# * passwordless SSH key from this host -> each Synology
|
||||
# * the SSH user may sudo-read /usr/syno/etc/certificate (NOPASSWD recommended)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CONFIG ── one line per certificate to sync.
|
||||
# Format: "name|ssh_user@host|ssh_port|domain_to_match"
|
||||
# name : subdir under $TLS_DIR (and a label)
|
||||
# domain : the cert whose SAN contains this DNS name is selected on the NAS
|
||||
# ---------------------------------------------------------------------------
|
||||
TARGETS=(
|
||||
"fids|admin@192.168.0.245|22|fids.famfi.dyndns.org"
|
||||
"fids2|admin@192.168.0.234|22|fids2.famfi.dyndns.org"
|
||||
)
|
||||
|
||||
TLS_DIR="/srv/TRAEFIK/etc/traefik/tls/synology" # where certs land (host path)
|
||||
TLS_DIR_IN_CONTAINER="/etc/traefik/tls/synology" # same dir as seen by Traefik
|
||||
DYN_CONF="/srv/TRAEFIK/etc/traefik/traefik.d/tls-synology.yml" # generated, watched by Traefik
|
||||
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||
|
||||
log() { printf '%s [sync-certs] %s\n' "$(date '+%F %T')" "$*"; }
|
||||
die() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
changed=0
|
||||
|
||||
for entry in "${TARGETS[@]}"; do
|
||||
IFS='|' read -r name dest port domain <<<"$entry"
|
||||
log "=== $name ($domain via $dest:$port) ==="
|
||||
|
||||
# 1) Find, on the NAS, the _archive folder whose cert covers $domain, and
|
||||
# stream fullchain + privkey back through one sudo'd SSH call (avoids
|
||||
# scp-ing root-only files). Delimiters let us split locally.
|
||||
remote_cmd='
|
||||
set -e
|
||||
for d in /usr/syno/etc/certificate/_archive/*/; do
|
||||
[ -f "$d/cert.pem" ] || continue
|
||||
if openssl x509 -in "$d/cert.pem" -noout -ext subjectAltName 2>/dev/null \
|
||||
| grep -q "DNS:'"$domain"'"; then
|
||||
echo "===FULLCHAIN==="; cat "$d/fullchain.pem"
|
||||
echo "===PRIVKEY==="; cat "$d/privkey.pem"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
echo "NO_CERT_FOUND_FOR_'"$domain"'" >&2; exit 3'
|
||||
|
||||
blob="$(ssh $SSH_OPTS -p "$port" "$dest" "sudo sh -c '$remote_cmd'")" \
|
||||
|| die "SSH/cert fetch failed for $name (check key, sudo, domain)"
|
||||
|
||||
# 2) Split the blob into the two PEMs.
|
||||
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
||||
printf '%s\n' "$blob" | awk '
|
||||
/===FULLCHAIN===/ {dst="'"$tmp"'/fullchain.pem"; next}
|
||||
/===PRIVKEY===/ {dst="'"$tmp"'/privkey.pem"; next}
|
||||
dst {print > dst}'
|
||||
[ -s "$tmp/fullchain.pem" ] && [ -s "$tmp/privkey.pem" ] || die "empty cert/key for $name"
|
||||
|
||||
# 3) Validate: cert/key modulus match + cert not expired.
|
||||
cmod="$(openssl x509 -in "$tmp/fullchain.pem" -noout -modulus | openssl md5)"
|
||||
kmod="$(openssl rsa -in "$tmp/privkey.pem" -noout -modulus | openssl md5)"
|
||||
[ "$cmod" = "$kmod" ] || die "cert/key mismatch for $name"
|
||||
openssl x509 -in "$tmp/fullchain.pem" -noout -checkend 0 >/dev/null \
|
||||
|| die "fetched cert for $name is EXPIRED — refusing to install"
|
||||
|
||||
# 4) Install only if different from what's already there.
|
||||
d_out="$TLS_DIR/$name"; mkdir -p "$d_out"
|
||||
if ! cmp -s "$tmp/fullchain.pem" "$d_out/fullchain.pem" 2>/dev/null \
|
||||
|| ! cmp -s "$tmp/privkey.pem" "$d_out/privkey.pem" 2>/dev/null; then
|
||||
install -m 0644 "$tmp/fullchain.pem" "$d_out/fullchain.pem"
|
||||
install -m 0600 "$tmp/privkey.pem" "$d_out/privkey.pem"
|
||||
log "updated cert for $name ($(openssl x509 -in "$d_out/fullchain.pem" -noout -enddate))"
|
||||
changed=1
|
||||
else
|
||||
log "cert for $name already up to date"
|
||||
fi
|
||||
rm -rf "$tmp"; trap - EXIT
|
||||
done
|
||||
|
||||
# 5) (Re)generate the Traefik dynamic TLS config listing every cert we have.
|
||||
# Writing this file (in the watched traefik.d dir) triggers a hot-reload.
|
||||
{
|
||||
echo "# AUTO-GENERATED by sync-synology-certs.sh — do not edit by hand."
|
||||
echo "# Loads the Synology-managed certs so Traefik serves valid certs while"
|
||||
echo "# still terminating TLS (CrowdSec stays in the request path)."
|
||||
echo "tls:"
|
||||
echo " certificates:"
|
||||
for entry in "${TARGETS[@]}"; do
|
||||
IFS='|' read -r name _ _ _ <<<"$entry"
|
||||
if [ -s "$TLS_DIR/$name/fullchain.pem" ]; then
|
||||
echo " - certFile: $TLS_DIR_IN_CONTAINER/$name/fullchain.pem"
|
||||
echo " keyFile: $TLS_DIR_IN_CONTAINER/$name/privkey.pem"
|
||||
fi
|
||||
done
|
||||
} > "$DYN_CONF"
|
||||
log "wrote $DYN_CONF (Traefik will hot-reload)"
|
||||
|
||||
[ "$changed" -eq 1 ] && log "DONE (certs changed)" || log "DONE (no changes)"
|
||||
Reference in New Issue
Block a user