Extern geliefertes Zertifikat verwenden, statt es zu kopieren

Liefert eine fremde Stelle das Zertifikat in ein Verzeichnis und
ersetzt es dort beim Erneuern - gleicher Dateiname, neuer Inhalt -,
passt der uebliche Weg nicht: der Proxy Manager kopiert hochgeladene
Zertifikate nach /data/custom_ssl/npm-<id>/ und bekommt von einer
ersetzten Datei nichts mit.

Das Verzeichnis wird deshalb schreibgeschuetzt als /certs in den
Container gehaengt (neuer Mount in der compose-Datei, Vorgabe
./data/npm/external-certs), und "cert external" ersetzt die beiden
Dateien unter custom_ssl durch Verweise darauf. Am Abbild
jc21/nginx-proxy-manager:2 nachgemessen: nginx liefert danach das
externe Zertifikat, und ein Austausch der Datei wirkt - allerdings erst
nach einem Neuladen.

Genau das ist der Haken, und er ist belegt: ohne Neuladen liefert nginx
weiterhin das alte Zertifikat. Dafuer gibt es

  cert external --refresh      vergleicht den SHA256-Fingerabdruck mit
                               dem zuletzt gesehenen und laedt nur bei
                               Aenderung neu
  cert external --watch on     traegt das als taeglichen cron-Eintrag ein

Vor dem Umstellen wird geprueft, ob beide Dateien lesbar sind, ob
Zertifikat und Schluessel zusammengehoeren (Vergleich der oeffentlichen
Schluessel - das faengt auch EC-Schluessel) und wie lange es noch gilt.
Beide Faelle sind durchgespielt: ein fremder Schluessel wird abgelehnt,
ein in fuenf Tagen ablaufendes Zertifikat gemeldet.

"cert external --off --id N" nimmt es zurueck und legt wieder eigene
Kopien an. In der Oberflaeche ist aus dem Menuepunkt "Zertifikat" ein
kleines Menue geworden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-20 22:05:20 +02:00
co-authored by Claude Opus 5
parent 81ff194bd1
commit 4456f8c3ea
7 changed files with 522 additions and 0 deletions
+351
View File
@@ -221,6 +221,9 @@ KOMMANDOS - Proxy-Hosts (ueber die API des Proxy Managers)
KOMMANDOS - Zertifikat
cert [Optionen] selbstsigniertes Zertifikat fuer den Proxy erzeugen
cert external [...] ein von aussen geliefertes Zertifikat verwenden
(Verzeichnis wird als /certs eingehaengt, die
Dateien werden dorthin verwiesen statt kopiert)
--domain FQDN Common Name / SAN
--alt NAME zusaetzlicher SAN (mehrfach; DNS-Name oder IP)
--days N Gueltigkeit in Tagen (Vorgabe 3650)
@@ -2027,6 +2030,7 @@ EOF
FW_STATE_FILE_NAME="firewall-opened.conf"
JOBS_FILE_NAME="backup-jobs.conf"
CRON_FILE="/etc/cron.d/checkmk-backup"
CERT_CRON_FILE="/etc/cron.d/checkmk-cert-external"
jobs_file() { printf '%s/%s\n' "$PROJECT_DIR" "$JOBS_FILE_NAME"; }
@@ -3090,6 +3094,10 @@ cert|CERT_DOMAIN|text|FQDN fuer selbstsignierte Zertifikate||
cert|CERT_DAYS|int|Gueltigkeit in Tagen||
cert|CERT_COUNTRY|text|Laenderkuerzel||
cert|CERT_ORG|text|Organisation||
cert|CERT_EXTERNAL_DIR|path|Verzeichnis mit geliefertem Zertifikat||1
cert|CERT_EXTERNAL_FULLCHAIN|text|Dateiname Zertifikatskette||1
cert|CERT_EXTERNAL_KEY|text|Dateiname privater Schluessel||1
cert|CERT_EXTERNAL_WATCH|choice|Taeglich auf Austausch pruefen|off:on|1
firewall|FW_ROLE|choice|Rolle im verteilten Monitoring (leer = single)|single:central:remote|1
firewall|FW_EC_SYSLOG|choice|Event Console: Syslog 514 (leer = aus)|off:udp:tcp:beide|1
firewall|FW_EC_SNMPTRAP|choice|Event Console: SNMP-Traps 162/udp (leer = aus)|off:on|1
@@ -3197,6 +3205,10 @@ settings_post_set() {
cmd_npm_enable "$val" ;;
BACKUP_AUTO)
cmd_job auto "$val" ;;
CERT_EXTERNAL_WATCH)
cmd_cert_external --watch "$val" ;;
CERT_EXTERNAL_DIR)
log "Wirksam nach: $SELF up (das Verzeichnis wird erst dann eingehaengt)" ;;
CMK_IMAGE|CMK_SITE_ID|CMK_CONTAINER_NAME|NPM_IMAGE|NPM_CONTAINER_NAME|TZ|CMK_LIVESTATUS_TCP|NPM_DISABLE_IPV6|CMK_DATA_DIR|NPM_DATA_DIR|NPM_LE_DIR)
log "Wirksam nach: $SELF up (Container werden neu erstellt)" ;;
esac
@@ -4828,10 +4840,278 @@ EOF
esac
}
# =====================================================================
# Vorhandenes Zertifikat von aussen verwenden
#
# Bei manchen Kunden liefert eine fremde Stelle das Zertifikat in ein
# Verzeichnis und ersetzt es dort beim Erneuern - gleicher Dateiname,
# neuer Inhalt. Der Proxy Manager kopiert hochgeladene Zertifikate
# aber nach /data/custom_ssl/npm-<id>/ und bekommt von einer ersetzten
# Datei nichts mit.
#
# Deshalb: das Verzeichnis wird schreibgeschuetzt in den Container
# gehaengt (/certs), und die beiden Dateien unter custom_ssl werden
# durch Verweise darauf ersetzt. nginx liest die Datei bei jedem
# Neuladen frisch ein - nachgemessen: ohne Neuladen liefert es
# weiterhin das alte Zertifikat, danach das neue. Genau dafuer gibt es
# "cert external --refresh" und den Waechter per cron.
# =====================================================================
CERT_STATE_FILE_NAME="cert-external.state"
cert_ext_dir() { printf '%s' "${CERT_EXTERNAL_DIR:-$PROJECT_DIR/data/npm/external-certs}"; }
cert_ext_full() { printf '%s' "${CERT_EXTERNAL_FULLCHAIN:-fullchain.pem}"; }
cert_ext_key() { printf '%s' "${CERT_EXTERNAL_KEY:-privkey.pem}"; }
cert_state_file() { printf '%s/%s\n' "$PROJECT_DIR" "$CERT_STATE_FILE_NAME"; }
# Fingerabdruck des Zertifikats - daran haengt, ob neu geladen werden muss
cert_ext_fingerprint() {
local d; d="$(cert_ext_dir)"
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
openssl x509 -in "$d/$(cert_ext_full)" -noout -fingerprint -sha256 2>/dev/null \
| sed 's/.*=//' | tr -d ':'
}
# Passen Zertifikat und Schluessel zusammen, und wie lange gilt es noch?
cert_ext_verify() {
local d; d="$(cert_ext_dir)"
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
local c="$d/$(cert_ext_full)" k="$d/$(cert_ext_key)"
[ -r "$c" ] || { err "Nicht lesbar: $c"; return 1; }
[ -r "$k" ] || { err "Nicht lesbar: $k"; return 1; }
openssl x509 -in "$c" -noout >/dev/null 2>&1 \
|| { err "$c ist kein lesbares Zertifikat (PEM erwartet)."; return 1; }
local pc pk
pc="$(openssl x509 -in "$c" -noout -pubkey 2>/dev/null)"
pk="$(openssl pkey -in "$k" -pubout 2>/dev/null)"
if [ -z "$pk" ]; then
err "$k ist kein lesbarer privater Schluessel."; return 1
fi
if [ "$pc" != "$pk" ]; then
err "Zertifikat und Schluessel gehoeren nicht zusammen."
printf ' %s\n %s\n' "$c" "$k" >&2
return 1
fi
if ! openssl x509 -in "$c" -noout -checkend 0 >/dev/null 2>&1; then
warn "Das Zertifikat ist bereits abgelaufen ($(openssl x509 -in "$c" -noout -enddate | sed 's/notAfter=//'))."
elif ! openssl x509 -in "$c" -noout -checkend 1209600 >/dev/null 2>&1; then
warn "Das Zertifikat laeuft in weniger als 14 Tagen ab ($(openssl x509 -in "$c" -noout -enddate | sed 's/notAfter=//'))."
fi
return 0
}
# Liegt das Verzeichnis im Container? Ohne Mount geht gar nichts.
cert_ext_mounted() {
$RUNTIME exec "$NPM_CONTAINER_NAME" test -r "/certs/$(cert_ext_full)" 2>/dev/null
}
# Zeigen die Dateien des Zertifikats <id> schon nach draussen?
cert_ext_linked() { # id
[ "$($RUNTIME exec "$NPM_CONTAINER_NAME" readlink "/data/custom_ssl/npm-$1/fullchain.pem" 2>/dev/null)" \
= "/certs/$(cert_ext_full)" ]
}
# =====================================================================
# Selbstsigniertes Zertifikat fuer den NGINX Proxy Manager
# =====================================================================
cmd_cert_external() {
local sub="" cid="" dom="" watch=""
while [ $# -gt 0 ]; do
case "$1" in
--id) cid="$2"; shift 2 ;;
--domain) dom="$2"; shift 2 ;;
--off) sub=off; shift ;;
--refresh) sub=refresh; shift ;;
--watch) sub=watch; watch="$2"; shift 2 ;;
--help|-h)
cat <<EOF
Ein von aussen geliefertes Zertifikat verwenden.
$SELF cert external Stand anzeigen
$SELF cert external --domain kunde.de Zertifikat dieses Proxy-Hosts umstellen
$SELF cert external --id 3 dasselbe ueber die Zertifikats-ID
$SELF cert external --off --id 3 zurueck auf eine eigene Kopie
$SELF cert external --refresh bei geaenderter Datei nginx neu laden
$SELF cert external --watch on|off das taeglich per cron erledigen
Das Verzeichnis kommt aus der .env und wird schreibgeschuetzt als
/certs in den Container gehaengt:
CERT_EXTERNAL_DIR=${CERT_EXTERNAL_DIR:-./data/npm/external-certs}
CERT_EXTERNAL_FULLCHAIN=$(cert_ext_full)
CERT_EXTERNAL_KEY=$(cert_ext_key)
Nach dem Umstellen zeigen die Dateien des Zertifikats im Proxy Manager
dorthin. Wird die Datei draussen ersetzt, liefert nginx bis zum
naechsten Neuladen weiterhin das alte Zertifikat - deshalb --refresh.
EOF
return 0 ;;
*) die "Unbekannte Option fuer 'cert external': $1" ;;
esac
done
local d; d="$(cert_ext_dir)"
case "$d" in /*) : ;; *) d="$PROJECT_DIR/${d#./}" ;; esac
# --- Waechter ein-/ausschalten -----------------------------------
if [ "$sub" = watch ]; then
case "$(fw_lc "$watch")" in
on|ein|true|1) : ;;
off|aus|false|0)
if [ -f "$CERT_CRON_FILE" ]; then
[ "$(id -u)" -eq 0 ] || die "Zum Entfernen von $CERT_CRON_FILE werden root-Rechte benoetigt."
rm -f "$CERT_CRON_FILE"; ok "Waechter entfernt."
else
ok "Es war kein Waechter eingetragen."
fi
env_set CERT_EXTERNAL_WATCH off
return 0 ;;
*) die "--watch: on oder off" ;;
esac
[ "$(id -u)" -eq 0 ] || die "Zum Schreiben von $CERT_CRON_FILE werden root-Rechte benoetigt."
local tmp; tmp="$(mktemp)"
{
printf '# Waechter fuer das extern gelieferte Zertifikat\n'
printf '# Erzeugt von cmk-manage.sh cert external --watch on\n'
printf 'SHELL=/bin/sh\n'
printf 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n'
printf '17 4 * * * root %s cert external --refresh >>/var/log/cmk-cert.log 2>&1\n' "$SELF"
} >"$tmp"
install -m 0644 "$tmp" "$CERT_CRON_FILE"; rm -f "$tmp"
env_set CERT_EXTERNAL_WATCH on
ok "Waechter eingetragen: $CERT_CRON_FILE (taeglich 4:17 Uhr)."
log "Er laedt nginx nur dann neu, wenn sich die Datei geaendert hat."
return 0
fi
# --- Nur nachsehen, ob sich etwas geaendert hat -------------------
if [ "$sub" = refresh ]; then
local now old sf; sf="$(cert_state_file)"
now="$(cert_ext_fingerprint)"
if [ -z "$now" ]; then
err "Zertifikat nicht lesbar: $d/$(cert_ext_full)"; return 1
fi
old="$(cat "$sf" 2>/dev/null || true)"
if [ "$now" = "$old" ]; then
log "Zertifikat unveraendert - nichts zu tun."
return 0
fi
log "Das Zertifikat hat sich geaendert."
cert_ext_verify || return 1
if cmd_proxy reload; then
printf '%s\n' "$now" > "$sf"
ok "Neues Zertifikat ist in Betrieb."
else
err "Neuladen fehlgeschlagen - der Stand bleibt vermerkt als offen."
return 1
fi
return 0
fi
# --- Stand anzeigen ----------------------------------------------
if [ -z "$sub" ] && [ -z "$cid" ] && [ -z "$dom" ]; then
printf '\n%sExtern geliefertes Zertifikat%s\n' "$C_BLD" "$C_RST"
printf ' Verzeichnis : %s\n' "$d"
printf ' Dateien : %s , %s\n' "$(cert_ext_full)" "$(cert_ext_key)"
if [ -r "$d/$(cert_ext_full)" ]; then
printf ' Gueltig bis : %s\n' "$(openssl x509 -in "$d/$(cert_ext_full)" -noout -enddate 2>/dev/null | sed 's/notAfter=//')"
printf ' Aussteller : %s\n' "$(openssl x509 -in "$d/$(cert_ext_full)" -noout -subject 2>/dev/null | sed 's/subject=//')"
else
test_result warn "Dort liegt (noch) kein lesbares Zertifikat."
fi
if npm_enabled && container_running "$NPM_CONTAINER_NAME"; then
if cert_ext_mounted; then
test_result ok "Das Verzeichnis ist im Container als /certs eingehaengt."
else
test_result warn "Im Container fehlt /certs"
printf ' Nach dem Setzen von CERT_EXTERNAL_DIR muss der Container
neu erstellt werden: %s up\n' "$SELF"
fi
local i used=""
for i in $(json_ids "$(NPM_TOKEN="$(npm_api_token 2>/dev/null)" npm_api GET /api/nginx/certificates 2>/dev/null)" 2>/dev/null); do
if cert_ext_linked "$i"; then used="$used $i"; fi
done
if [ -n "$used" ]; then
test_result ok "Verwendet von Zertifikat(en):$used"
else
test_result warn "Noch kein Zertifikat zeigt dorthin."
printf ' Umstellen mit: %s cert external --domain <name>\n' "$SELF"
fi
fi
printf ' Waechter : %s\n' \
"$([ -f "$CERT_CRON_FILE" ] && echo "eingetragen ($CERT_CRON_FILE)" || echo 'nicht eingetragen')"
echo
return 0
fi
# --- Umstellen ----------------------------------------------------
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
if [ -z "$cid" ]; then
[ -n "$dom" ] || die "--domain oder --id angeben."
local all i o
all="$(npm_api GET /api/nginx/proxy-hosts)"
for i in $(json_ids "$all"); do
o="$(npm_api GET "/api/nginx/proxy-hosts/$i")"
if ( set +o pipefail; printf '%s' "$o" | grep -qF "\"$dom\"" ); then
cid="$(json_num "$o" certificate_id)"; break
fi
done
[ -n "$cid" ] || die "Kein Proxy-Host fuer '$dom' gefunden."
if [ "$cid" = 0 ]; then
die "Der Proxy-Host '$dom' hat noch kein Zertifikat.
Erst eines zuweisen, dann umstellen:
$SELF proxy edit --domain $dom --cert self
$SELF cert external --domain $dom"
fi
fi
if [ "$sub" = off ]; then
cert_ext_linked "$cid" || die "Zertifikat $cid zeigt nicht nach draussen - nichts zurueckzunehmen."
log "Ersetze die Verweise durch eigene Kopien ..."
$RUNTIME exec "$NPM_CONTAINER_NAME" sh -c "
cd /data/custom_ssl/npm-$cid || exit 1
cp -L fullchain.pem f.tmp && cp -L privkey.pem k.tmp || exit 1
rm -f fullchain.pem privkey.pem
mv f.tmp fullchain.pem && mv k.tmp privkey.pem" \
|| die "Zuruecknehmen fehlgeschlagen."
ok "Zertifikat $cid hat jetzt wieder eigene Dateien."
cmd_proxy reload || true
return 0
fi
cert_ext_verify || return 1
cert_ext_mounted || die "Im Container gibt es /certs/$(cert_ext_full) nicht.
Das Verzeichnis wird erst beim Neuerstellen des Containers eingehaengt:
$SELF config set CERT_EXTERNAL_DIR '$d'
$SELF up"
log "Setze die Dateien von Zertifikat $cid auf /certs ..."
$RUNTIME exec "$NPM_CONTAINER_NAME" sh -c "
d=/data/custom_ssl/npm-$cid
mkdir -p \"\$d\" && cd \"\$d\" || exit 1
rm -f fullchain.pem privkey.pem
ln -s '/certs/$(cert_ext_full)' fullchain.pem
ln -s '/certs/$(cert_ext_key)' privkey.pem" \
|| die "Die Verweise liessen sich nicht anlegen."
cmd_proxy reload || die "nginx hat die Aenderung nicht angenommen."
cert_ext_fingerprint > "$(cert_state_file)" 2>/dev/null || true
ok "Zertifikat $cid kommt jetzt aus $d."
printf ' Wird die Datei dort ersetzt, uebernimmt nginx sie erst beim
naechsten Neuladen. Dafuer gibt es:
%s cert external --refresh einmalig
sudo %s cert external --watch on taeglich per cron\n\n' "$SELF" "$SELF"
return 0
}
cmd_cert() {
if [ "${1:-}" = external ]; then shift; cmd_cert_external "$@"; return $?; fi
local domain="${CERT_DOMAIN:-checkmk.example.local}" days="${CERT_DAYS:-3650}"
local -a alts=()
while [ $# -gt 0 ]; do
@@ -6477,6 +6757,77 @@ gui_proxy_del() {
}
gui_cert() {
local sel
sel="$(ui_menu "Zertifikate" \
"Fuer den Proxy Manager. Ein selbstsigniertes Zertifikat erzeugt dieses
Werkzeug selbst; ein von aussen geliefertes wird nicht kopiert, sondern
aus seinem Verzeichnis heraus verwendet - dann wirkt ein Austausch
dort, ohne dass hier etwas nachgezogen werden muss." \
selbst "Selbstsigniertes Zertifikat erzeugen" \
extern "Extern geliefertes Zertifikat verwenden" \
stand "Stand des externen Zertifikats anzeigen" \
back "zurueck")" || return 0
case "$sel" in
back) return 0 ;;
stand) local fs; fs="$(mktemp)"
( NO_COLOR=1 cmd_cert external ) >"$fs" 2>&1 || true
ui_text "Extern geliefertes Zertifikat" "$fs"; rm -f "$fs"; return 0 ;;
extern) gui_cert_external; return 0 ;;
esac
gui_cert_self
}
gui_cert_external() {
local fs; fs="$(mktemp)"
( NO_COLOR=1 cmd_cert external ) >"$fs" 2>&1 || true
ui_text "Extern geliefertes Zertifikat" "$fs"; rm -f "$fs"
local sel
sel="$(ui_menu "Extern geliefertes Zertifikat" \
"Verzeichnis: $(cert_ext_dir)
Dateien : $(cert_ext_full) , $(cert_ext_key)
Geaendert wird das Verzeichnis unter Einstellungen > Selbstsignierte
Zertifikate. Nach einer Aenderung muss der Container neu erstellt
werden (Stack starten), damit er es sieht." \
umstellen "Zertifikat eines Proxy-Hosts darauf umstellen" \
refresh "Jetzt nachsehen, ob die Datei ausgetauscht wurde" \
watchon "Taeglich nachsehen (cron eintragen)" \
watchoff "Taegliches Nachsehen abschalten" \
zurueck "Zurueck auf eine eigene Kopie" \
back "zurueck")" || return 0
local -a args=()
case "$sel" in
back) return 0 ;;
umstellen) local d
d="$(ui_input "Umstellen" "Name des Proxy-Hosts:" "${PROXY_DOMAIN:-}")" || return 0
[ -n "$d" ] || return 0
args=(--domain "$d") ;;
refresh) args=(--refresh) ;;
watchon) args=(--watch on) ;;
watchoff) args=(--watch off) ;;
zurueck) local i
i="$(ui_input "Zurueck" "Zertifikats-ID:" "")" || return 0
[ -n "$i" ] || return 0
args=(--off --id "$i") ;;
esac
local f2 rc=0
f2="$(mktemp)"
if [ "$sel" = watchon ] || [ "$sel" = watchoff ]; then
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
gui_run_interactive sudo -- "$SELF" ${SELF_ENV_ARR[@]+"${SELF_ENV_ARR[@]}"} cert external "${args[@]}"
rm -f "$f2"; return 0
fi
fi
( NO_COLOR=1 cmd_cert external "${args[@]}" ) >"$f2" 2>&1 || rc=$?
if [ "$rc" -eq 0 ]; then ui_text "Zertifikat" "$f2"; else ui_text "Fehlgeschlagen" "$f2"; fi
rm -f "$f2"
return 0
}
gui_cert_self() {
local domain alt days
domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return 0
[ -n "$domain" ] || return 0