diff --git a/README.md b/README.md index abdc4b8..e1581e1 100644 --- a/README.md +++ b/README.md @@ -1224,6 +1224,8 @@ erzwingen, HTTP/2 und Websockets einschalten und den empfohlenen | `proxy add …` | Proxy-Host anlegen (`--update` ersetzt einen vorhandenen) | | `proxy show --domain X` | Einzelheiten samt zusätzlicher Pfade | | `proxy del --id N` | Proxy-Host löschen | +| `proxy default --domain X` | HTTPS über die blanke IP ermöglichen (spiegelt X in einen Standard-Server) | +| `proxy default --off` | diese Spiegelung wieder entfernen | Optionen von `proxy add`: @@ -1459,6 +1461,68 @@ Einbinden: Die ncurses-Oberfläche bietet denselben Ablauf unter *„Selbstsigniertes Zertifikat erzeugen“*. +### 10.8 HTTPS über die blanke IP + +Ruft man den Server als `https://172.0.250.82/cmk/` auf, endet das im Browser +mit **`ERR_SSL_UNRECOGNIZED_NAME_ALERT`** – über `http://` funktioniert +dieselbe Adresse. Das liegt nicht am Zertifikat. + +Der Grund steckt in TLS: Der Browser teilt dem Server beim Verbindungsaufbau +mit, welchen Namen er sprechen will (*SNI*). Laut RFC 6066 dürfen dort **keine +IP-Adressen** stehen, also sendet der Browser bei einer IP gar nichts. nginx +kann den passenden Server-Block dann nicht über den Namen auswählen und fällt +auf seinen Standard-Server zurück. Bei NPM ist das dieser Block aus +`/etc/nginx/conf.d/default.conf`: + +```nginx +server { + listen 443 ssl; + server_name localhost; + ssl_reject_handshake on; # <- genau das meldet der Browser + return 444; +} +``` + +Über HTTP fällt es nicht auf, weil nginx den Server-Block dort am `Host`-Kopf +erkennt – und der trägt die IP. + +Abhilfe ist ein eigener Standard-Server, der diese namenlosen Aufrufe annimmt: + +```bash +sudo ./scripts/cmk-manage.sh proxy default --domain checkmk.example.local --restart +``` + +Der Befehl liest den genannten Proxy-Host über die API aus und spiegelt ihn – +Ziel, Zertifikat und **alle zusätzlichen Pfade** – in einen Block mit +`default_server`, der in die Zusatzkonfiguration geschrieben wird. Aufrufe mit +Namen bleiben unberührt: nginx wählt weiterhin zuerst nach SNI aus, der +Standard-Server greift nur, wenn kein Name passt. Keiner der von NPM erzeugten +Blöcke setzt `default_server` selbst, deshalb gibt es darum keinen Streit. + +| Befehl | Wirkung | +|---|---| +| `proxy default` | zeigt, ob eingerichtet, und welchen Proxy-Host es spiegelt | +| `proxy default --domain X` | Proxy-Host `X` spiegeln | +| `proxy default --off` | Spiegelung entfernen | +| `--restart` | den Proxy Manager gleich neu starten | + +In der Oberfläche liegt das unter **Proxy-Hosts → HTTPS über die blanke IP +(ohne Namen) ermöglichen**. + +Zwei Dinge sind zu beachten: + +- Das **Zertifikat muss die IP als Alternativnamen führen**, sonst meldet der + Browser einen Namensfehler (anklickbar, aber unschön). `cmk-manage.sh cert` + und `proxy add --cert self` tragen die IP des Hosts von sich aus ein; bei + einem Let's-Encrypt-Zertifikat geht das prinzipbedingt nicht – dafür gibt es + keine Zertifikate auf IP-Adressen. Der Befehl prüft das und sagt es. +- Die Zusatzkonfiguration liegt im Datenverzeichnis des Proxy Managers, und das + gehört dem Container. Deshalb **mit `sudo`** aufrufen. + +Die saubere Alternative bleibt ein Name: ein DNS-Eintrag oder eine Zeile in der +`hosts`-Datei der Clients genügt, damit der Browser SNI sendet und der +Proxy-Host ganz normal greift. + --- ## 11. Proxy Manager lokal einrichten (Terminal-Browser) @@ -2628,6 +2692,7 @@ Checkmk-Werksnotizen (Werks/Release Notes) lesen. | Symptom | Ursache / Lösung | |---|---| +| `ERR_SSL_UNRECOGNIZED_NAME_ALERT` beim Aufruf über `https:///` | Der Browser sendet bei einer IP kein SNI, nginx fällt auf seinen Standard-Server zurück und lehnt ab. `cmk-manage.sh proxy default --domain ` – siehe [Kapitel 10.8](#108-https-über-die-blanke-ip) | | `Port 80/443 already in use` | Auf dem Host läuft bereits ein Webserver: `ss -tlnp \| grep ':80'` → `systemctl disable --now apache2 nginx httpd` | | Nach Reboot laufen die Container nicht (Podman) | systemd-Unit fehlt: `./install.sh --with-systemd` bzw. `--quadlet`, danach `systemctl is-enabled checkmk-stack` | | `omd restore`: *version is not installed* | passendes Image holen: `cmk-manage.sh image pull --edition --version `, dann `up` | diff --git a/README_en.md b/README_en.md index 67336bd..2d4276b 100644 --- a/README_en.md +++ b/README_en.md @@ -1223,6 +1223,8 @@ HTTPS, enables HTTP/2 and websockets, and sets the recommended | `proxy add …` | create a proxy host (`--update` replaces an existing one) | | `proxy show --domain X` | details including additional paths | | `proxy del --id N` | delete a proxy host | +| `proxy default --domain X` | enable HTTPS on the bare IP (mirrors X into a default server) | +| `proxy default --off` | remove that mirror again | Options for `proxy add`: @@ -1456,6 +1458,67 @@ Installing it: The ncurses interface offers the same workflow under *"Selbstsigniertes Zertifikat erzeugen"*. +### 10.8 HTTPS on the bare IP + +Opening the server as `https://172.0.250.82/cmk/` ends in +**`ERR_SSL_UNRECOGNIZED_NAME_ALERT`** in the browser – while `http://` on the +very same address works. This is not a certificate problem. + +The reason is in TLS: the browser tells the server which name it wants to talk +to (*SNI*) while the connection is set up. RFC 6066 does not allow **IP +addresses** there, so for an IP the browser sends nothing at all. nginx then +cannot select the matching server block by name and falls back to its default +server. With NPM that is this block from `/etc/nginx/conf.d/default.conf`: + +```nginx +server { + listen 443 ssl; + server_name localhost; + ssl_reject_handshake on; # <- exactly what the browser reports + return 444; +} +``` + +Over HTTP it does not show, because there nginx picks the server block by the +`Host` header – and that carries the IP. + +The remedy is a default server of your own that accepts those nameless calls: + +```bash +sudo ./scripts/cmk-manage.sh proxy default --domain checkmk.example.local --restart +``` + +The command reads the named proxy host through the API and mirrors it – target, +certificate and **all additional paths** – into a block carrying +`default_server`, written to the custom configuration. Calls by name are +untouched: nginx still selects by SNI first, and the default server only +applies when no name matches. None of the blocks NPM generates sets +`default_server` itself, so there is no conflict over it. + +| Command | Effect | +|---|---| +| `proxy default` | shows whether it is set up, and which proxy host it mirrors | +| `proxy default --domain X` | mirror proxy host `X` | +| `proxy default --off` | remove the mirror | +| `--restart` | restart the Proxy Manager right away | + +In the GUI this lives under **Proxy-Hosts → HTTPS über die blanke IP (ohne +Namen) ermöglichen**. + +Two things to keep in mind: + +- The **certificate must carry the IP as a subject alternative name**, otherwise + the browser reports a name mismatch (clickable, but ugly). `cmk-manage.sh + cert` and `proxy add --cert self` add the host's IP by themselves; with a + Let's Encrypt certificate this is impossible by design – there are no + certificates for IP addresses. The command checks this and says so. +- The custom configuration lives in the Proxy Manager's data directory, which + belongs to the container. So run it **with `sudo`**. + +The clean alternative is still a name: a DNS record or one line in the clients' +`hosts` file is enough for the browser to send SNI, and the proxy host applies +as usual. + --- ## 11. Configuring the Proxy Manager locally (terminal browser) @@ -2625,6 +2688,7 @@ release notes beforehand. | Symptom | Cause / solution | |---|---| +| `ERR_SSL_UNRECOGNIZED_NAME_ALERT` when opening `https:///` | browsers send no SNI for an IP, nginx falls back to its default server and rejects. `cmk-manage.sh proxy default --domain ` – see [chapter 10.8](#108-https-on-the-bare-ip) | | `Port 80/443 already in use` | a web server is already running on the host: `ss -tlnp \| grep ':80'` → `systemctl disable --now apache2 nginx httpd` | | Containers do not run after a reboot (Podman) | systemd unit missing: `./install.sh --with-systemd` or `--quadlet`, then `systemctl is-enabled checkmk-stack` | | `omd restore`: *version is not installed* | fetch the matching image: `cmk-manage.sh image pull --edition --version `, then `up` | diff --git a/scripts/cmk-manage.sh b/scripts/cmk-manage.sh index ea5e2ef..14cdcb6 100755 --- a/scripts/cmk-manage.sh +++ b/scripts/cmk-manage.sh @@ -2753,25 +2753,54 @@ NPM_CUSTOM_CONF_REL="nginx/custom/http.conf" NPM_BLOCK_START="# >>> cmk-manage.sh: Admin-Port - nicht von Hand aendern >>>" NPM_BLOCK_END="# <<< cmk-manage.sh: Admin-Port <<<" -npm_custom_conf_path() { - local base="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}" - case "$base" in /*) : ;; *) base="$PROJECT_DIR/${base#./}" ;; esac - printf '%s/%s\n' "$base" "$NPM_CUSTOM_CONF_REL" +# Die Zusatzkonfiguration liegt im Datenverzeichnis des Proxy Managers, +# und das gehoert dem Container - also root. Ohne Schreibrecht scheitert +# sonst erst "touch" mit einer nackten Fehlermeldung. +npm_conf_writable() { # datei + local f="$1" d; d="$(dirname "$f")" + if [ -e "$f" ]; then + [ -w "$f" ] && return 0 + elif [ -d "$d" ]; then + [ -w "$d" ] && return 0 + else + mkdir -p "$d" 2>/dev/null && return 0 + fi + err "Keine Schreibrechte auf $f" + printf ' Das Verzeichnis gehoert dem Proxy-Manager-Container (root). + Bitte mit sudo aufrufen: + sudo %s %s\n' "$SELF" "${CMD_LINE:-}" >&2 + return 1 } +npm_data_dir_abs() { + local base="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}" + case "$base" in /*) : ;; *) base="$PROJECT_DIR/${base#./}" ;; esac + printf '%s\n' "$base" +} + +npm_custom_conf_path() { + printf '%s/%s\n' "$(npm_data_dir_abs)" "$NPM_CUSTOM_CONF_REL" +} + +NPM_DEF_START="# >>> cmk-manage.sh: HTTPS ohne SNI - nicht von Hand aendern >>>" +NPM_DEF_END="# <<< cmk-manage.sh: HTTPS ohne SNI <<<" + # Vorhandenen verwalteten Block entfernen, Fremdinhalte bleiben erhalten -npm_conf_strip_block() { - local f="$1" +npm_conf_strip_marked() { + local f="$1" st="$2" en="$3" [ -f "$f" ] || return 0 - awk -v s="$NPM_BLOCK_START" -v e="$NPM_BLOCK_END" ' + awk -v s="$st" -v e="$en" ' $0 == s { skip=1; next } $0 == e { skip=0; next } !skip { print } ' "$f" > "$f.tmp" && mv "$f.tmp" "$f" } +npm_conf_strip_block() { npm_conf_strip_marked "$1" "$NPM_BLOCK_START" "$NPM_BLOCK_END"; } + npm_conf_write_block() { local f="$1" port="$2" + npm_conf_writable "$f" || return 1 mkdir -p "$(dirname "$f")" touch "$f" npm_conf_strip_block "$f" @@ -3555,6 +3584,144 @@ proxy_location_json() { # spec -> JSON-Objekt auf stdout } # --- Proxy-Hosts --------------------------------------------------------- +# ===================================================================== +# HTTPS ueber die blanke IP +# +# Ruft man den Server mit https:/// auf, sendet der Browser keinen +# SNI-Namen - RFC 6066 laesst dort keine IP-Adressen zu. nginx kann den +# passenden Server-Block dann nicht ueber den Namen auswaehlen und faellt +# auf den Standard-Server zurueck. Bei NPM ist das der Block aus +# /etc/nginx/conf.d/default.conf, und der antwortet mit +# "ssl_reject_handshake on" - im Browser als +# ERR_SSL_UNRECOGNIZED_NAME_ALERT. +# +# Ueber HTTP faellt das nicht auf: dort waehlt nginx den Server-Block +# anhand des Host-Kopfes aus, und der traegt die IP. +# +# Abhilfe ist ein eigener Block mit "default_server" in der +# Zusatzkonfiguration, die nginx nach den Proxy-Hosts einliest. Keiner +# der NPM-Bloecke setzt das Kennzeichen selbst, deshalb gibt es keinen +# Streit darum. Benannte Aufrufe gehen weiterhin an ihren Proxy-Host - +# der Standard-Server greift nur, wenn kein Name passt. +# ===================================================================== + +# Zertifikatspfade IM CONTAINER zu einer NPM-Zertifikats-ID +npm_cert_paths() { # id -> "fullchain|privkey|anbieter" + local cid="$1" o prov + o="$(npm_api GET "/api/nginx/certificates/$cid")" || return 1 + prov="$(json_str "$o" provider)" + if [ "$prov" = letsencrypt ]; then + printf '/etc/letsencrypt/live/npm-%s/fullchain.pem|/etc/letsencrypt/live/npm-%s/privkey.pem|%s\n' \ + "$cid" "$cid" "$prov" + else + printf '/data/custom_ssl/npm-%s/fullchain.pem|/data/custom_ssl/npm-%s/privkey.pem|%s\n' \ + "$cid" "$cid" "${prov:-other}" + fi +} + +# Deckt das Zertifikat die IP ab? Nur pruefbar, wenn die Datei hier liegt. +npm_cert_covers_ip() { # zertifikats-id ip + local cid="$1" ip="$2" f + f="$(npm_data_dir_abs)/custom_ssl/npm-$cid/fullchain.pem" + [ -r "$f" ] || return 0 # nicht pruefbar - dann auch nicht meckern + ( set +o pipefail; openssl x509 -in "$f" -noout -text 2>/dev/null | grep -q "IP Address:$ip" ) +} + +# Einen Proxy-Host in einen Standard-Server-Block spiegeln +npm_default_write() { # conf objekt-json fullchain privkey domain + local f="$1" o="$2" full="$3" key="$4" dom="$5" + npm_conf_writable "$f" || return 1 + local sch host port seg lp lsch lh lpo + sch="$(json_str "$o" forward_scheme)"; [ -n "$sch" ] || sch=http + host="$(json_str "$o" forward_host)" + port="$(json_num "$o" forward_port)" + + mkdir -p "$(dirname "$f")" + touch "$f" + npm_conf_strip_marked "$f" "$NPM_DEF_START" "$NPM_DEF_END" + { + printf '%s\n' "$NPM_DEF_START" + cat <> "$f" + sed -i '/./,$!d' "$f" +} + +npm_reload_hint() { # restart-flag + if [ -n "${1:-}" ] && container_running "$NPM_CONTAINER_NAME"; then + log "Starte '$NPM_CONTAINER_NAME' neu, damit nginx die Konfiguration liest ..." + if $RUNTIME restart "$NPM_CONTAINER_NAME" >/dev/null; then + ok "Proxy Manager neu gestartet." + else + warn "Neustart fehlgeschlagen - bitte manuell: $SELF restart" + fi + else + log "Wirksam nach: $SELF restart (bzw. Neustart des Proxy Managers)" + fi +} + cmd_proxy() { local sub="${1:-list}"; [ $# -gt 0 ] && shift || true @@ -3639,6 +3806,116 @@ cmd_proxy() { echo return 0 ;; + default|ip) + local dom="" off="" restart="" conf + while [ $# -gt 0 ]; do + case "$1" in + --domain) dom="$2"; shift 2 ;; + --off|--aus) off=1; shift ;; + --restart) restart=1; shift ;; + --help|-h) +cat </ auf, schickt der Browser keinen +Namen mit (SNI kennt keine IP-Adressen). nginx kann den Proxy-Host dann +nicht ueber den Namen finden und faellt auf seinen Standard-Server +zurueck - der lehnt den Handshake ab: ERR_SSL_UNRECOGNIZED_NAME_ALERT. +Ueber HTTP faellt das nicht auf, dort entscheidet der Host-Kopf. + +Dieser Befehl legt einen Standard-Server an, der denselben Weg nimmt wie +der genannte Proxy-Host - samt der zusaetzlichen Pfade. Aufrufe mit +Namen gehen weiterhin an ihren eigenen Proxy-Host. +EOF + return 0 ;; + *) dom="$1"; shift ;; + esac + done + conf="$(npm_custom_conf_path)" + + if [ -n "$off" ]; then + if ! grep -qsF "$NPM_DEF_START" "$conf"; then + ok "Es ist kein Standard-Server eingetragen." + return 0 + fi + npm_conf_strip_marked "$conf" "$NPM_DEF_START" "$NPM_DEF_END" + [ -s "$conf" ] || rm -f "$conf" + ok "Standard-Server entfernt: $conf" + npm_reload_hint "$restart" + return 0 + fi + + if [ -z "$dom" ]; then + printf '\n%sHTTPS ueber die blanke IP%s\n' "$C_BLD" "$C_RST" + if grep -qsF "$NPM_DEF_START" "$conf"; then + test_result ok "eingerichtet" + printf ' Datei : %s\n' "$conf" + printf ' Spiegelt: %s\n' \ + "$(sed -n 's/^# Spiegelt den Proxy-Host "\(.*\)"\.$/\1/p' "$conf" | head -1)" + printf '\n Entfernen mit: %s proxy default --off\n\n' "$SELF" + else + test_result warn "nicht eingerichtet" + printf ' https:/// endet dann in + ERR_SSL_UNRECOGNIZED_NAME_ALERT, http:/// geht.\n' + printf '\n Einrichten mit: %s proxy default --domain \n' "$SELF" + printf ' Vorhandene : %s proxy list\n\n' "$SELF" + fi + return 0 + fi + + npm_require_running || return 1 + NPM_TOKEN="$(npm_api_token)" || return 1 + + # Proxy-Host zum Namen suchen + local all pid="" i + all="$(npm_api GET /api/nginx/proxy-hosts)" + for i in $(json_ids "$all"); do + if ( set +o pipefail; npm_api GET "/api/nginx/proxy-hosts/$i" | grep -qF "\"$dom\"" ); then + pid="$i"; break + fi + done + [ -n "$pid" ] || die "Kein Proxy-Host fuer '$dom' gefunden - '$SELF proxy list' zeigt die vorhandenen." + + local o cid + o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")" + cid="$(json_num "$o" certificate_id)" + if [ -z "$cid" ] || [ "$cid" = 0 ]; then + die "Der Proxy-Host '$dom' hat kein Zertifikat. + Ohne Zertifikat gibt es nichts, was ohne SNI ausgeliefert werden koennte. + Erst eines zuweisen: $SELF proxy add --domain $dom --cert ... --update" + fi + + local paths full key prov + paths="$(npm_cert_paths "$cid")" || die "Zertifikat $cid nicht abrufbar." + full="${paths%%|*}"; paths="${paths#*|}" + key="${paths%%|*}"; prov="${paths##*|}" + + local ip; ip="$(primary_ip)" + if [ "$prov" = letsencrypt ]; then + warn "Das Zertifikat stammt von Lets Encrypt und lautet auf '$dom'. + Beim Aufruf ueber die IP meldet der Browser deshalb einen + Namensfehler (anklickbar, aber unschoen). Fuer den IP-Zugang ist + ein selbstsigniertes Zertifikat mit der IP als Alternativname + besser: $SELF cert --domain $dom --alt $ip" + elif [ -n "$ip" ] && ! npm_cert_covers_ip "$cid" "$ip"; then + warn "Das Zertifikat fuehrt die IP $ip nicht als Alternativnamen. + Der Browser meldet dann einen Namensfehler. Neu erzeugen mit: + $SELF cert --domain $dom --alt $ip + und dem Proxy-Host zuweisen." + fi + + CMD_LINE="proxy default --domain $dom" npm_default_write "$conf" "$o" "$full" "$key" "$dom" || return 1 + ok "Standard-Server geschrieben: $conf" + printf ' Spiegelt Proxy-Host %s (%s), Zertifikat %s (%s)\n' "$pid" "$dom" "$cid" "$prov" + if [ -n "$ip" ]; then printf ' Danach erreichbar unter: https://%s/\n' "$ip"; fi + npm_reload_hint "$restart" + return 0 ;; + del|delete|rm) local pid="" while [ $# -gt 0 ]; do @@ -4970,6 +5247,7 @@ Ziel : ${PROXY_FORWARD_HOST:-127.0.0.1}:${PROXY_FORWARD_PORT:-5000}" \ list "Vorhandene Proxy-Hosts anzeigen" \ show "Einzelheiten eines Proxy-Hosts (mit Pfaden)" \ certs "Vorhandene Zertifikate anzeigen" \ + ipzugang "HTTPS ueber die blanke IP (ohne Namen) ermoeglichen" \ del "Proxy-Host loeschen" \ status "Anmeldung pruefen" \ reset "Datenbank zuruecksetzen (Konto neu anlegen lassen)" \ @@ -4985,6 +5263,7 @@ Ziel : ${PROXY_FORWARD_HOST:-127.0.0.1}:${PROXY_FORWARD_PORT:-5000}" \ ui_text "Proxy-Host $d3" "$f3"; rm -f "$f3" ;; certs) local f2; f2="$(mktemp)"; NO_COLOR=1 cmd_proxy certs >"$f2" 2>&1 || true ui_text "Zertifikate" "$f2"; rm -f "$f2" ;; + ipzugang) gui_proxy_default || true ;; del) gui_proxy_del || true ;; status) ui_run "Anmeldung pruefen" bash -c "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' proxy status" ;; reset) gui_npm_reset || true ;; @@ -4993,6 +5272,49 @@ Ziel : ${PROXY_FORWARD_HOST:-127.0.0.1}:${PROXY_FORWARD_PORT:-5000}" \ done } +gui_proxy_default() { + local f rc=0 + f="$(mktemp)" + NO_COLOR=1 cmd_proxy default >"$f" 2>&1 || true + ui_text "HTTPS ueber die blanke IP" "$f"; rm -f "$f" + + local sel + sel="$(ui_menu "HTTPS ueber die blanke IP" \ +"Ruft man den Server als https:/// auf, schickt der Browser keinen +Namen mit - SNI kennt keine IP-Adressen. nginx findet den Proxy-Host +dann nicht und lehnt den Handshake ab (ERR_SSL_UNRECOGNIZED_NAME_ALERT). +Ueber http:/// faellt das nicht auf, dort zaehlt der Host-Kopf. + +Ein Standard-Server nimmt genau diese Aufrufe an und geht denselben Weg +wie der gewaehlte Proxy-Host, samt seiner zusaetzlichen Pfade. +Aufrufe mit Namen bleiben unberuehrt." \ + ein "Einrichten - Proxy-Host spiegeln" \ + aus "Entfernen" \ + back "zurueck")" || return 0 + + case "$sel" in + back) return 0 ;; + aus) ui_run "Standard-Server entfernen" bash -c \ + "cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' proxy default --off --restart" || true + return 0 ;; + esac + + local dom + dom="$(ui_input "Einrichten" "Name des Proxy-Hosts, der gespiegelt werden soll:" \ + "${PROXY_DOMAIN:-}")" || return 0 + [ -n "$dom" ] || return 0 + + f="$(mktemp)" + NO_COLOR=1 cmd_proxy default --domain "$dom" --restart >"$f" 2>&1 || rc=$? + if [ "$rc" -eq 0 ]; then + ui_text "Standard-Server eingerichtet" "$f" + else + ui_text "Einrichten fehlgeschlagen" "$f" + fi + rm -f "$f" + return 0 +} + gui_proxy_add() { if ! npm_require_running 2>/dev/null; then ui_msg "Proxy Manager laeuft nicht" \