deploy/caddy-guard.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/bash
# caddy-guard — gardien du Caddyfile du VPS.
# Chaque minute : caddy validate. Si KO -> restauration du dernier snapshot
# valide (écriture in-place, inode préservée) + reload + alerte Vektor.
# Jamais de mv/rename sur le Caddyfile (bind-mount fichier, piège inode).
set -u
BASE=/opt/caddy
CF="$BASE/Caddyfile"
SNAPDIR="$BASE/guard-snapshots"
LOG=/var/log/caddy-guard.log
INCIDENT=/var/lib/caddy-guard/incident
ts() { date '+%F %T'; }
log() { echo "$(ts) $*" >> "$LOG"; }
validate() { docker exec caddy-caddy-1 caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>&1; }
post_vektor_alert() {
ip=$(docker inspect vektor-api --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null)
secret=$(grep -E '^VEKTOR_SELFHEAL_SECRET=' /opt/vektor/.env 2>/dev/null | head -1 | cut -d= -f2-)
[ -n "$ip" ] && [ -n "$secret" ] || return 0
curl -s -m 30 -X POST "http://$ip:8000/api/webhook/grafana" \
-H "Content-Type: application/json" -H "X-Vektor-Selfheal: $secret" \
-d "{\"alerts\":[{\"fingerprint\":\"caddy-guard-restore\",\"title\":\"Caddyfile corrompu restauré automatiquement par caddy-guard\",\"status\":\"firing\",\"labels\":{\"severity\":\"critical\"}}]}" \
>/dev/null 2>&1
}
if validate; then
mkdir -p "$SNAPDIR"
latest=$(ls -1t "$SNAPDIR"/snap-*.conf 2>/dev/null | head -1)
# snapshot si aucun, ou si le fichier a changé depuis le dernier snapshot
if [ -z "$latest" ] || ! cmp -s "$CF" "$latest"; then
cp "$CF" "$SNAPDIR/snap-$(date +%Y%m%d%H%M%S).conf"
ls -1t "$SNAPDIR"/snap-*.conf | tail -n +13 | xargs -r rm -f
log "snapshot de config valide pris"
fi
if [ -f "$INCIDENT" ]; then
log "config redevenue valide — incident caddy-guard clos"
rm -f "$INCIDENT"
printf '{"active": false}\n' > "$BASE/config/maintenance/incident.json"
# résolution côté Vektor
ip=$(docker inspect vektor-api --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null)
secret=$(grep -E '^VEKTOR_SELFHEAL_SECRET=' /opt/vektor/.env 2>/dev/null | head -1 | cut -d= -f2-)
if [ -n "$ip" ] && [ -n "$secret" ]; then
curl -s -m 30 -X POST "http://$ip:8000/api/webhook/grafana" \
-H "Content-Type: application/json" -H "X-Vektor-Selfheal: $secret" \
-d '{"alerts":[{"fingerprint":"caddy-guard-restore","title":"Caddyfile corrompu restauré automatiquement par caddy-guard","status":"resolved","labels":{"severity":"critical"}}]}' \
>/dev/null 2>&1
fi
fi
exit 0
fi
# --- validate KO : restauration ---
latest=$(ls -1t "$SNAPDIR"/snap-*.conf 2>/dev/null | head -1)
if [ -z "$latest" ]; then
log "VALIDATE KO et AUCUN snapshot — intervention manuelle requise"
exit 1
fi
log "VALIDATE KO — restauration depuis $latest"
cat "$latest" > "$CF"
if ! validate; then
log "le snapshot restauré est lui aussi KO — NON reload, intervention manuelle"
exit 1
fi
docker exec caddy-caddy-1 caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 \
&& log "reload OK après restauration" || log "reload en échec après restauration"
printf '{"active": true, "title": "Config reverse-proxy restaurée automatiquement", "message": "Le Caddyfile est devenu invalide ; un snapshot valide a été remis en place par le gardien.", "since": "%s"}\n' "$(ts)" \
> "$BASE/config/maintenance/incident.json"
touch "$INCIDENT"
post_vektor_alert