#!/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)"