diff --git a/.env.example b/.env.example index ad24036..4fbb76b 100644 --- a/.env.example +++ b/.env.example @@ -185,3 +185,38 @@ CERT_DOMAIN=checkmk.example.local CERT_DAYS=3650 CERT_COUNTRY=DE CERT_ORG="Beispiel GmbH" + +# --------------------------------------------------------------------- +# Firewall: was von aussen erreichbar sein muss +# +# Diese Werte oeffnen nichts von selbst - sie sagen "cmk-manage.sh +# firewall", welche Ports dieser Aufbau braucht. +# --------------------------------------------------------------------- + +# Rolle im verteilten Monitoring: +# single - eigenstaendige Site (Vorgabe) +# central - Zentrale; baut die Verbindungen selbst auf, braucht +# eingehend nichts zusaetzlich +# remote - Remote-Site; die Zentrale fragt hier Livestatus (6557) ab, +# dazu muss CMK_LIVESTATUS_TCP=on gesetzt sein +FW_ROLE=single + +# Event Console: Syslog annehmen - off, udp, tcp oder beide (Port 514) +FW_EC_SYSLOG=off + +# Event Console: SNMP-Traps annehmen (Port 162/udp) +FW_EC_SNMPTRAP=off + +# Event Console: Status und Ereignisse ueber TCP (Port 6558) +FW_EC_TCP=off + +# Laeuft auf diesem Host ein Checkmk-Agent, der im Pull-Betrieb +# abgefragt wird? (Port 6556/tcp) +FW_AGENT=off + +# Weitere Ports, die mitgeprueft werden sollen - durch Komma getrennt, +# Beschreibung nach dem Gleichheitszeichen. Ohne Protokoll gilt tcp. +# Enthaelt eine Beschreibung Leerzeichen, gehoert der ganze Wert in +# Anfuehrungszeichen (das erledigt "cmk-manage.sh config set" selbst). +# FW_EXTRA_PORTS=3000/tcp=Grafana,5667/tcp=NSCA,514/udp=Syslog +FW_EXTRA_PORTS= diff --git a/README.md b/README.md index 1fbcd23..1f4b447 100644 --- a/README.md +++ b/README.md @@ -557,6 +557,10 @@ Die wichtigsten Schalter: | `BACKUP_DIR` | Ablage für **manuelle** Backups (Vorgabe `./backups`); geplante Jobs haben eigene Pfade | | `BACKUP_AUTO` | `on`/`off` – sollen die Backup-Jobs per cron laufen? | | `CERT_DOMAIN`, `CERT_DAYS` | Vorgaben für selbstsignierte Zertifikate | +| `FW_ROLE` | `single`/`central`/`remote` – Rolle im verteilten Monitoring, steuert die Firewall-Prüfung | +| `FW_EC_SYSLOG`, `FW_EC_SNMPTRAP`, `FW_EC_TCP` | Event Console: Syslog (514), SNMP-Traps (162/udp), Status über TCP (6558) | +| `FW_AGENT` | läuft auf diesem Host ein Checkmk-Agent im Pull-Betrieb? (6556) | +| `FW_EXTRA_PORTS` | weitere Ports für die Prüfung, z. B. `3000/tcp=Grafana` | ### 7.1 Wer liest die `.env` – und warum gibt es sie zusätzlich zum Skript? @@ -1799,6 +1803,96 @@ mit aus. ## 13. Firewall +RHEL und Rocky bringen firewalld ab Werk mit, und dort ist in der Regel nur +SSH freigegeben. Ein Stack, der sauber läuft, ist von außen trotzdem nicht +erreichbar – die Agents kommen nicht durch, und im Browser passiert nichts. +Deshalb prüft der Manager das auf Wunsch selbst: + +```bash +./scripts/cmk-manage.sh firewall # Prüfbericht +./scripts/cmk-manage.sh firewall open # Kommandos nur anzeigen +sudo ./scripts/cmk-manage.sh firewall open --apply +sudo ./scripts/cmk-manage.sh firewall open --apply --source 10.0.0.0/24 +``` + +Erkannt werden **firewalld**, **ufw**, **nftables** und **iptables** – in dieser +Reihenfolge, die erste aktive gewinnt. Bei firewalld werden alle *aktiven +Zonen* ausgewertet, nicht nur die Standardzone: hängen zwei Netzwerkkarten in +verschiedenen Zonen, zählt ein Port als offen, sobald ihn eine davon freigibt. +Neben einzelnen Ports werden auch Dienstnamen (`http`, `https`, `ssh`) und +Rich Rules berücksichtigt. + +Welche Ports gebraucht werden, hängt an der Konfiguration. Ports werden +durchweg als `nummer/protokoll` geführt – Syslog und SNMP-Traps laufen über +UDP, und eine Freigabe für TCP nützt ihnen nichts. + +**Immer:** + +| Port | Zweck | Nötig | +|------|-------|-------| +| `80/tcp` | HTTP, Prüfung durch Let's Encrypt | nur mit `NPM_ENABLED=on` | +| `443/tcp` | HTTPS für Checkmk und weitere Dienste | nur mit `NPM_ENABLED=on` | +| `NPM_ADMIN_PORT/tcp` (81) | Adminoberfläche des Proxy Managers | nur aus dem Adminnetz | +| `5000/tcp` | Checkmk direkt | **nur** mit `NPM_ENABLED=off`, sonst geschlossen lassen | +| `8000/tcp` | Agent-Receiver (registrierte Agents) | immer | + +**Verteiltes Monitoring** – über `FW_ROLE` gesteuert: + +| Port | Zweck | Nötig | +|------|-------|-------| +| `6557/tcp` | Livestatus – die Zentrale fragt diese Site ab | bei `FW_ROLE=remote`, sonst nur mit `CMK_LIVESTATUS_TCP=on` | +| `443/tcp` | Konfigurationsabgleich und Automations-API der Zentrale | bei `FW_ROLE=remote` | + +Die **Zentrale** (`FW_ROLE=central`) baut die Verbindungen selbst auf und +braucht eingehend nichts zusätzlich – dafür muss *ausgehend* 6557/tcp und +443/tcp zu den Remote-Sites erlaubt sein. Darauf weist der Bericht hin. +Ebenso auf den häufigsten Stolperstein: `FW_ROLE=remote` bei +`CMK_LIVESTATUS_TCP=off` – dann ist der Port zwar offen, aber es lauscht +niemand. + +**Event Console und Agent** – jeweils eigener Schalter, weil das nicht jeder +Aufbau braucht: + +| Port | Zweck | Schalter | +|------|-------|----------| +| `514/udp`, `514/tcp` | Syslog-Meldungen annehmen | `FW_EC_SYSLOG=off\|udp\|tcp\|beide` | +| `162/udp` | SNMP-Traps annehmen | `FW_EC_SNMPTRAP=on` | +| `6558/tcp` | Status und Ereignisse der Event Console über TCP | `FW_EC_TCP=on` | +| `6556/tcp` | Checkmk-Agent dieses Hosts (Pull-Betrieb) | `FW_AGENT=on` | + +Alles Weitere – Grafana, NSCA, was sonst auf dem Rechner liegt – kommt in +`FW_EXTRA_PORTS`: + +```bash +./scripts/cmk-manage.sh config set FW_EXTRA_PORTS "3000/tcp=Grafana,5667/tcp=NSCA" +``` + +Der Bericht markiert auch den umgekehrten Fall: Ist 5000 offen, obwohl der +Proxy Manager läuft, steht dort *„offen – sollte zu sein"*. Ohne Prüfbefund +endet `firewall` mit Rückgabewert 1, lässt sich also in eigenen Skripten +auswerten. + +Portbereiche in bestehenden Regeln werden aufgelöst: Wer `6556-6558/tcp` +(firewalld, nftables) oder `6556:6558/tcp` (ufw, iptables) freigegeben hat, +bekommt alle drei Ports als offen gemeldet. Bei ufw zählt eine Regel ohne +Protokollangabe – `ufw allow 514` – für TCP **und** UDP, so wie ufw sie auch +anlegt. + +`--source` begrenzt jede erzeugte Regel auf ein Quellnetz. Für 8000, 6557 und +514 ist das sinnvoll (nur die überwachten Hosts bzw. die Zentrale), für Port +80 nicht: Let's Encrypt prüft die Domain von außen, eine Begrenzung lässt die +Ausstellung scheitern. Darauf weist der Bericht hin. + +> **Docker und ufw:** Veröffentlichte Container-Ports umgehen ufw sonst über die +> `DOCKER-USER`-Kette. Hier nicht – beide Container laufen im Host-Netz, es +> gelten die Hostregeln unverändert. + +In der Oberfläche steckt derselbe Bericht unter **Firewall prüfen**, samt +Rückfrage, ob die fehlenden Freigaben gleich gesetzt werden sollen. Die +Schalter dazu liegen unter **Einstellungen → Firewall: was offen sein muss**. + +### Von Hand + **firewalld (RHEL/Rocky/Alma):** ```bash @@ -1806,6 +1900,15 @@ sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --permanent --add-service=https sudo firewall-cmd --permanent --add-port=8000/tcp # Agent-Receiver sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 port port=81 protocol=tcp accept' + +# Verteiltes Monitoring: Livestatus nur fuer die Zentrale +sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.5/32 port port=6557 protocol=tcp accept' + +# Event Console: Syslog und SNMP-Traps (UDP!) +sudo firewall-cmd --permanent --add-service=syslog # 514/udp +sudo firewall-cmd --permanent --add-service=snmptrap # 162/udp +sudo firewall-cmd --permanent --add-port=6558/tcp # Status ueber TCP + sudo firewall-cmd --reload # Bei abweichendem Admin-Port (z.B. 10443) stattdessen dessen Nummer freigeben - @@ -1819,6 +1922,9 @@ sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 8000/tcp sudo ufw allow from 10.0.0.0/24 to any port 81 proto tcp +sudo ufw allow from 10.0.0.5 to any port 6557 proto tcp # Livestatus fuer die Zentrale +sudo ufw allow 514/udp # Event Console: Syslog +sudo ufw allow 162/udp # Event Console: SNMP-Traps sudo ufw enable ``` @@ -1845,14 +1951,16 @@ sudo ./scripts/cmk-manage.sh gui │ │ │ status Status von Containern und Sites │ │ stack Stack steuern (Start/Stop/Logs/Update) │ - │ backup Backup erstellen │ + │ backup Backup erstellen / planen │ │ restore Backup wiederherstellen │ │ import Site von Direktinstallation importieren │ │ manage Backups verwalten (Liste/Details/Löschen) │ │ sites Sites im Container verwalten │ │ image Checkmk-Image beschaffen/auswählen │ + │ proxy Proxy-Hosts anlegen/verwalten (HTTPS) │ │ browser Terminal-Browser (Proxy Manager lokal) │ │ cert Selbstsigniertes Zertifikat erzeugen │ + │ firewall Firewall prüfen (benötigte Ports) │ │ config Einstellungen (.env) - menügeführt │ │ shell Root-Shell im Checkmk-Container │ │ quit Beenden │ @@ -2024,6 +2132,7 @@ Eine Dateiauswahl mit Verzeichnisnavigation ist eingebaut. | `npm-reset` | Datenbank beiseitelegen, damit das Konto neu entsteht | | `proxy status\|list\|certs\|add\|del` | Proxy-Hosts über die API anlegen und verwalten | | `cert …` | selbstsigniertes Zertifikat erzeugen | +| `firewall [check\|open]` | prüfen, ob die benötigten Ports offen sind (firewalld, ufw, nftables, iptables) | | `shell` | Root-Shell im Checkmk-Container | | `omd …` | beliebiges `omd`-Kommando im Container | diff --git a/README_en.md b/README_en.md index e4ce7a7..258e4e8 100644 --- a/README_en.md +++ b/README_en.md @@ -560,6 +560,10 @@ The most important settings: | `BACKUP_DIR` | location for **manual** backups (default `./backups`); scheduled jobs have their own paths | | `BACKUP_AUTO` | `on`/`off` – should the backup jobs run via cron? | | `CERT_DOMAIN`, `CERT_DAYS` | defaults for self-signed certificates | +| `FW_ROLE` | `single`/`central`/`remote` – role in distributed monitoring, drives the firewall check | +| `FW_EC_SYSLOG`, `FW_EC_SNMPTRAP`, `FW_EC_TCP` | Event Console: syslog (514), SNMP traps (162/udp), status over TCP (6558) | +| `FW_AGENT` | does a Checkmk agent run on this host in pull mode? (6556) | +| `FW_EXTRA_PORTS` | additional ports for the check, e.g. `3000/tcp=Grafana` | ### 7.1 Who reads `.env` – and why does it exist alongside the script? @@ -1795,6 +1799,93 @@ for you. ## 13. Firewall +RHEL and Rocky ship firewalld enabled, and it normally allows SSH only. A stack +that runs perfectly is still unreachable from outside – agents cannot get +through and the browser shows nothing. The manager therefore checks this for +you: + +```bash +./scripts/cmk-manage.sh firewall # report +./scripts/cmk-manage.sh firewall open # only print the commands +sudo ./scripts/cmk-manage.sh firewall open --apply +sudo ./scripts/cmk-manage.sh firewall open --apply --source 10.0.0.0/24 +``` + +**firewalld**, **ufw**, **nftables** and **iptables** are detected in that +order; the first active one wins. For firewalld all *active zones* are +evaluated, not just the default zone: if two interfaces sit in different zones, +a port counts as open as soon as one of them allows it. Besides individual +ports, service names (`http`, `https`, `ssh`) and rich rules are taken into +account. + +Which ports are needed depends on the configuration. Ports are handled as +`number/protocol` throughout – syslog and SNMP traps use UDP, and a TCP rule +does nothing for them. + +**Always:** + +| Port | Purpose | Needed | +|------|---------|--------| +| `80/tcp` | HTTP, Let's Encrypt challenge | only with `NPM_ENABLED=on` | +| `443/tcp` | HTTPS for Checkmk and other services | only with `NPM_ENABLED=on` | +| `NPM_ADMIN_PORT/tcp` (81) | Proxy Manager admin UI | administration network only | +| `5000/tcp` | Checkmk directly | **only** with `NPM_ENABLED=off`, keep closed otherwise | +| `8000/tcp` | agent receiver (registered agents) | always | + +**Distributed monitoring** – driven by `FW_ROLE`: + +| Port | Purpose | Needed | +|------|---------|--------| +| `6557/tcp` | Livestatus – the central site queries this one | with `FW_ROLE=remote`, otherwise only with `CMK_LIVESTATUS_TCP=on` | +| `443/tcp` | configuration sync and automation API of the central site | with `FW_ROLE=remote` | + +The **central site** (`FW_ROLE=central`) opens the connections itself and +needs nothing extra inbound – but *outbound* 6557/tcp and 443/tcp to the +remote sites must be allowed. The report points this out, as well as the most +common trap: `FW_ROLE=remote` together with `CMK_LIVESTATUS_TCP=off` – the +port is open but nothing listens on it. + +**Event Console and agent** – each has its own switch, because not every setup +needs them: + +| Port | Purpose | Switch | +|------|---------|--------| +| `514/udp`, `514/tcp` | accept syslog messages | `FW_EC_SYSLOG=off\|udp\|tcp\|beide` | +| `162/udp` | accept SNMP traps | `FW_EC_SNMPTRAP=on` | +| `6558/tcp` | Event Console status and events over TCP | `FW_EC_TCP=on` | +| `6556/tcp` | Checkmk agent of this host (pull mode) | `FW_AGENT=on` | + +Anything else – Grafana, NSCA, whatever else lives on the machine – goes into +`FW_EXTRA_PORTS`: + +```bash +./scripts/cmk-manage.sh config set FW_EXTRA_PORTS "3000/tcp=Grafana,5667/tcp=NSCA" +``` + +The report flags the reverse case too: if 5000 is open while the Proxy Manager +is running, it reads *"open – should be closed"*. When something is missing, +`firewall` exits with status 1, so you can evaluate it in your own scripts. + +Port ranges in existing rules are resolved: if you allowed `6556-6558/tcp` +(firewalld, nftables) or `6556:6558/tcp` (ufw, iptables), all three ports are +reported as open. With ufw a rule without a protocol – `ufw allow 514` – +counts for TCP **and** UDP, which is exactly how ufw creates it. + +`--source` restricts every generated rule to a source network. That makes +sense for 8000, 6557 and 514 (only the monitored hosts or the central site), +but not for port 80: Let's Encrypt probes the domain from the internet, and a +restriction makes issuance fail. The report points this out. + +> **Docker and ufw:** published container ports normally bypass ufw through the +> `DOCKER-USER` chain. Not here – both containers run in the host network, so +> the host rules apply unchanged. + +The GUI offers the same report under **Firewall prüfen**, including a prompt to +set the missing rules right away. The switches live under **Einstellungen → +Firewall: was offen sein muss**. + +### By hand + **firewalld (RHEL/Rocky/Alma):** ```bash @@ -1802,6 +1893,15 @@ sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --permanent --add-service=https sudo firewall-cmd --permanent --add-port=8000/tcp # agent receiver sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 port port=81 protocol=tcp accept' + +# distributed monitoring: Livestatus for the central site only +sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.5/32 port port=6557 protocol=tcp accept' + +# Event Console: syslog and SNMP traps (UDP!) +sudo firewall-cmd --permanent --add-service=syslog # 514/udp +sudo firewall-cmd --permanent --add-service=snmptrap # 162/udp +sudo firewall-cmd --permanent --add-port=6558/tcp # status over TCP + sudo firewall-cmd --reload # With a different admin port (e.g. 10443) open that number instead - @@ -1815,6 +1915,9 @@ sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 8000/tcp sudo ufw allow from 10.0.0.0/24 to any port 81 proto tcp +sudo ufw allow from 10.0.0.5 to any port 6557 proto tcp # Livestatus for the central site +sudo ufw allow 514/udp # Event Console: syslog +sudo ufw allow 162/udp # Event Console: SNMP traps sudo ufw enable ``` @@ -1844,14 +1947,16 @@ The menu labels are German; the command table in │ │ │ status Status von Containern und Sites │ │ stack Stack steuern (Start/Stop/Logs/Update) │ - │ backup Backup erstellen │ + │ backup Backup erstellen / planen │ │ restore Backup wiederherstellen │ │ import Site von Direktinstallation importieren │ │ manage Backups verwalten (Liste/Details/Löschen) │ │ sites Sites im Container verwalten │ │ image Checkmk-Image beschaffen/auswählen │ + │ proxy Proxy-Hosts anlegen/verwalten (HTTPS) │ │ browser Terminal-Browser (Proxy Manager lokal) │ │ cert Selbstsigniertes Zertifikat erzeugen │ + │ firewall Firewall prüfen (benötigte Ports) │ │ config Einstellungen (.env) - menügeführt │ │ shell Root-Shell im Checkmk-Container │ │ quit Beenden │ @@ -2022,6 +2127,7 @@ A file chooser with directory navigation is built in. | `npm-reset` | move the database aside so the account is recreated | | `proxy status\|list\|certs\|add\|del` | create and manage proxy hosts through the API | | `cert …` | create a self-signed certificate | +| `firewall [check\|open]` | check whether the required ports are open (firewalld, ufw, nftables, iptables) | | `shell` | root shell in the Checkmk container | | `omd …` | any `omd` command inside the container | diff --git a/install.sh b/install.sh index 716ed1c..a5867e1 100755 --- a/install.sh +++ b/install.sh @@ -312,29 +312,27 @@ fi # Firewall-Hinweise # --------------------------------------------------------------------- print_firewall_hint() { + local be="" if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then -cat <<'EOF' - -Firewall (firewalld) - empfohlene Regeln: - firewall-cmd --permanent --add-service=http - firewall-cmd --permanent --add-service=https - firewall-cmd --permanent --add-port=8000/tcp # Agent-Receiver - # Admin-Oberflaeche des Proxy Managers nur aus dem Admin-Netz: - firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 port port=81 protocol=tcp accept' - # Checkmk-Port 5000 NICHT nach aussen oeffnen (nur ueber den Proxy) - firewall-cmd --reload -EOF - elif command -v ufw >/dev/null 2>&1; then -cat <<'EOF' - -Firewall (ufw) - empfohlene Regeln: - ufw allow 80/tcp - ufw allow 443/tcp - ufw allow 8000/tcp # Agent-Receiver - ufw allow from 10.0.0.0/24 to any port 81 proto tcp # Proxy-Manager-Admin - # Port 5000 (Checkmk direkt) bleibt zu - Zugriff nur ueber den Proxy -EOF + be=firewalld + elif command -v ufw >/dev/null 2>&1 \ + && ( set +o pipefail; ufw status 2>/dev/null | grep -qi '^Status: active' ); then + be=ufw + elif command -v nft >/dev/null 2>&1 && [ -n "$(nft list ruleset 2>/dev/null)" ]; then + be=nftables fi + [ -n "$be" ] || return 0 +cat <"$tmp" 2>&1 || rc=$? + if [ "$rc" -eq 0 ]; then + ui_text "Firewall - alles offen, was gebraucht wird" "$tmp" + rm -f "$tmp"; return 0 + fi + ui_text "Firewall - es fehlen Freigaben" "$tmp" + rm -f "$tmp" + + ui_yesno "Firewall" "Es fehlen Freigaben. + +Sollen die benoetigten Ports jetzt geoeffnet werden? +Die Regeln werden dauerhaft gesetzt." || return 0 + + local src="" + if ui_yesno "Firewall" "Nur fuer ein bestimmtes Quellnetz freigeben? + +Ja = nur dieses Netz darf zugreifen +Nein = Freigabe fuer alle Quellen"; then + src="$(ui_input "Firewall" "Quellnetz in CIDR-Schreibweise:" "$(primary_ip | sed 's/\.[0-9]*$/.0\/24/')")" || src="" + fi + + local -a a=(firewall open --apply --yes) + if [ -n "$src" ]; then a+=(--source "$src"); fi + if [ "$(id -u)" -eq 0 ]; then + ui_run "Firewall freigeben" "$SELF" "${a[@]}" || true + elif command -v sudo >/dev/null 2>&1; then + gui_run_interactive sudo -- "$SELF" "${a[@]}" + else + ui_msg "Firewall" "Dafuer werden root-Rechte benoetigt. +Bitte als root erneut aufrufen." + fi + return 0 +} + gui_main() { ui_require while :; do @@ -5110,6 +5174,7 @@ Backups : $BACKUP_DIR" \ proxy "Proxy-Hosts anlegen/verwalten (HTTPS fuer Checkmk)" \ browser "Terminal-Browser (Proxy Manager lokal bedienen)" \ cert "Selbstsigniertes Zertifikat erzeugen" \ + firewall "Firewall pruefen (welche Ports muessen offen sein)" \ config "Einstellungen (.env) - menuegefuehrt" \ shell "Root-Shell im Checkmk-Container" \ quit "Beenden")" || break @@ -5124,7 +5189,8 @@ Backups : $BACKUP_DIR" \ image) gui_image_menu || true ;; proxy) gui_proxy_menu || true ;; browser) gui_browser_menu || true ;; - cert) gui_cert || true ;; + cert) gui_cert || true ;; + firewall) gui_firewall || true ;; config) gui_settings_menu || true ;; shell) gui_need_container || continue gui_run_interactive cmd_shell ;; @@ -5205,6 +5271,383 @@ ${C_BLD}Auswertung${C_RST} EOF } +# ===================================================================== +# Firewall +# +# RHEL und Rocky haben firewalld ab Werk aktiv, und dort ist in aller +# Regel nur SSH freigegeben. Der Agent-Receiver auf 8000 und HTTPS auf +# 443 waeren damit dicht - die Agents kaemen nicht durch, ohne dass es +# am Stack selbst laege. +# +# Geprueft werden firewalld, ufw, nftables und iptables. Welche Ports +# noetig sind, haengt von der Konfiguration ab: ohne Proxy Manager +# braucht der externe Proxy Zugriff auf 5000, als Remote-Site einer +# verteilten Ueberwachung kommt Livestatus dazu, mit Event Console +# Syslog und SNMP-Traps - und die laufen ueber UDP. Ports werden +# deshalb durchweg als "nummer/protokoll" gefuehrt. +# ===================================================================== + +fw_lc() { printf '%s' "${1:-}" | tr 'A-Z' 'a-z'; } + +fw_on() { # Wert -> 0, wenn eingeschaltet + case "$(fw_lc "${1:-}")" in on|true|1|yes|ja) return 0 ;; esac + return 1 +} + +fw_detect() { + if command -v firewall-cmd >/dev/null 2>&1 \ + && firewall-cmd --state >/dev/null 2>&1; then + printf 'firewalld\n'; return 0 + fi + if command -v ufw >/dev/null 2>&1 \ + && ( set +o pipefail; ufw status 2>/dev/null | grep -qi '^Status: active' ); then + printf 'ufw\n'; return 0 + fi + if command -v nft >/dev/null 2>&1 && [ -n "$(nft list ruleset 2>/dev/null)" ]; then + printf 'nftables\n'; return 0 + fi + if command -v iptables >/dev/null 2>&1 \ + && [ "$(iptables -S 2>/dev/null | wc -l)" -gt 3 ]; then + printf 'iptables\n'; return 0 + fi + printf 'keine\n' +} + +# firewalld kennt Zonen. Hat der Rechner mehrere Netzwerkkarten, haengen +# sie oft in verschiedenen Zonen - dann sagt die Standardzone allein +# nichts darueber aus, ob ein Port erreichbar ist. +fw_zones() { + local z + z="$(firewall-cmd --get-active-zones 2>/dev/null | grep -v '^[[:space:]]' || true)" + if [ -z "$z" ]; then z="$(firewall-cmd --get-default-zone 2>/dev/null || true)"; fi + printf '%s\n' "$z" | sed '/^$/d' +} + +# Dienstnamen von firewalld auf port/protokoll abbilden +fw_service_port() { + case "$1" in + ssh) printf '22/tcp\n' ;; + http) printf '80/tcp\n' ;; + https) printf '443/tcp\n' ;; + cockpit) printf '9090/tcp\n' ;; + syslog) printf '514/udp\n514/tcp\n' ;; + syslog-tls) printf '6514/tcp\n' ;; + snmptrap) printf '162/udp\n' ;; + snmp) printf '161/udp\n' ;; + *) : ;; + esac +} + +# --------------------------------------------------------------------- +# Welche Ports braucht der Stack in dieser Konfiguration? +# port/proto|zweck|noetig (noetig: ja | nein | admin) +# --------------------------------------------------------------------- +fw_required_ports() { + local role; role="$(fw_lc "${FW_ROLE:-single}")" + case "$role" in central|remote) : ;; *) role=single ;; esac + + if npm_enabled; then + printf '80/tcp|HTTP und Lets-Encrypt-Pruefung|ja\n' + printf '443/tcp|HTTPS (Checkmk, Zentrale, weitere Dienste)|ja\n' + printf '%s/tcp|Adminoberflaeche des Proxy Managers|admin\n' "${NPM_ADMIN_PORT:-81}" + printf '5000/tcp|Checkmk direkt - gehoert hinter den Proxy|nein\n' + else + printf '5000/tcp|Checkmk direkt - Ziel des externen Proxys|ja\n' + fi + + printf '8000/tcp|Agent-Receiver (registrierte Agents)|ja\n' + + # --- Verteilte Ueberwachung --------------------------------------- + # Als Remote-Site fragt die Zentrale Livestatus ab. Die Zentrale + # selbst baut nur Verbindungen auf und braucht dafuer nichts offen. + if [ "$role" = remote ]; then + printf '6557/tcp|Livestatus - die Zentrale fragt diese Site ab|ja\n' + elif fw_on "${CMK_LIVESTATUS_TCP:-off}"; then + printf '6557/tcp|Livestatus ueber TCP|ja\n' + else + printf '6557/tcp|Livestatus - aus (CMK_LIVESTATUS_TCP=off)|nein\n' + fi + + # --- Event Console ------------------------------------------------- + case "$(fw_lc "${FW_EC_SYSLOG:-off}")" in + udp) printf '514/udp|Event Console: Syslog|ja\n' ;; + tcp) printf '514/tcp|Event Console: Syslog ueber TCP|ja\n' ;; + beide|both|on) + printf '514/udp|Event Console: Syslog|ja\n' + printf '514/tcp|Event Console: Syslog ueber TCP|ja\n' ;; + esac + if fw_on "${FW_EC_SNMPTRAP:-off}"; then + printf '162/udp|Event Console: SNMP-Traps|ja\n' + fi + if fw_on "${FW_EC_TCP:-off}"; then + printf '6558/tcp|Event Console: Status und Ereignisse ueber TCP|ja\n' + fi + + # --- Agent auf diesem Host ---------------------------------------- + if fw_on "${FW_AGENT:-off}"; then + printf '6556/tcp|Checkmk-Agent dieses Hosts (Pull-Betrieb)|ja\n' + fi + + # --- Frei eingetragene Ports --------------------------------------- + # Form: 3000/tcp=Grafana,5667/tcp=NSCA + local e pp d + while IFS= read -r e; do + e="$(printf '%s' "$e" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + [ -n "$e" ] || continue + pp="${e%%=*}"; d="${e#*=}" + if [ "$d" = "$e" ] || [ -z "$d" ]; then d="zusaetzlich freigegeben"; fi + case "$pp" in */tcp|*/udp) : ;; *) pp="$pp/tcp" ;; esac + printf '%s|%s|ja\n' "$pp" "$d" + done < <(printf '%s\n' "${FW_EXTRA_PORTS:-}" | tr ',' '\n') +} + +# --------------------------------------------------------------------- +# Offene Ports der erkannten Firewall einsammeln, "nummer/proto" je Zeile. +# Bereiche (6000-6010 bzw. 6000:6010) bleiben stehen und werden beim +# Vergleich aufgeloest. +# --------------------------------------------------------------------- +fw_open_ports() { # backend + case "$1" in + firewalld) + { local zone svc + while IFS= read -r zone; do + [ -n "$zone" ] || continue + firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n' + firewall-cmd --zone="$zone" --list-services 2>/dev/null | tr ' ' '\n' \ + | while IFS= read -r svc; do + if [ -n "$svc" ]; then fw_service_port "$svc"; fi + done + firewall-cmd --zone="$zone" --list-rich-rules 2>/dev/null \ + | sed -n 's/.*port port="\([0-9:-]*\)" protocol="\([a-z]*\)".*/\1\/\2/p' + done < <(fw_zones) + } | sed '/^$/d' | sort -u ;; + ufw) + local spec ports proto pp + ufw status 2>/dev/null | awk 'NR>3 {print $1}' \ + | while IFS= read -r spec; do + case "$spec" in *[0-9]*) : ;; *) continue ;; esac + proto=""; ports="$spec" + case "$spec" in + */tcp|*/udp) proto="${spec##*/}"; ports="${spec%/*}" ;; + esac + for pp in $(printf '%s' "$ports" | tr ',' ' '); do + case "$pp" in *[!0-9:-]*) continue ;; esac + if [ -n "$proto" ]; then + printf '%s/%s\n' "$pp" "$proto" + else + printf '%s/tcp\n%s/udp\n' "$pp" "$pp" + fi + done + done | sort -u ;; + nftables) + local proto rest pp + nft list ruleset 2>/dev/null \ + | grep -oE '(tcp|udp) dport[[:space:]]*(\{[^}]*\}|[0-9]+(-[0-9]+)?)' \ + | sed 's/dport//; s/[{}]//g' \ + | while read -r proto rest; do + for pp in $(printf '%s' "$rest" | tr ',' ' '); do + printf '%s/%s\n' "$pp" "$proto" + done + done | sort -u ;; + iptables) + local line proto ports pp + iptables -S 2>/dev/null | grep -E ' -j (ACCEPT|accept)$' \ + | while IFS= read -r line; do + proto="$(printf '%s' "$line" | sed -n 's/.* -p \([a-z0-9]*\).*/\1/p')" + case "$proto" in tcp|udp) : ;; *) continue ;; esac + ports="$(printf '%s' "$line" \ + | grep -oE '\-\-(dport|dports) [0-9,:]+' \ + | sed 's/--dports\? //')" + [ -n "$ports" ] || continue + for pp in $(printf '%s' "$ports" | tr ',' ' '); do + printf '%s/%s\n' "$pp" "$proto" + done + done | sort -u ;; + *) : ;; + esac +} + +# Steht port/proto in der Liste der offenen? Bereiche werden aufgeloest. +fw_port_open() { # port proto (Liste in _FW_OPEN, eine je Zeile) + local port="$1" proto="$2" e ep epr lo hi + while IFS= read -r e; do + [ -n "$e" ] || continue + ep="${e%/*}"; epr="${e##*/}" + [ "$epr" = "$proto" ] || continue + case "$ep" in + *[-:]*) + lo="${ep%%[-:]*}"; hi="${ep##*[-:]}" + case "$lo$hi" in *[!0-9]*) continue ;; esac + if [ "$port" -ge "$lo" ] && [ "$port" -le "$hi" ]; then return 0; fi ;; + *) if [ "$ep" = "$port" ]; then return 0; fi ;; + esac + done <<<"${_FW_OPEN:-}" + return 1 +} + +# Vorschlaege zum Oeffnen +fw_open_cmds() { # backend port proto [quelle] + local be="$1" port="$2" proto="$3" src="${4:-}" z + case "$be" in + firewalld) + z="$(fw_zones | head -n1)" + if [ -n "$src" ]; then + printf "firewall-cmd --permanent --zone=%s --add-rich-rule='rule family=ipv4 source address=%s port port=%s protocol=%s accept'\n" \ + "${z:-public}" "$src" "$port" "$proto" + else + printf 'firewall-cmd --permanent --zone=%s --add-port=%s/%s\n' "${z:-public}" "$port" "$proto" + fi ;; + ufw) + if [ -n "$src" ]; then + printf 'ufw allow from %s to any port %s proto %s\n' "$src" "$port" "$proto" + else + printf 'ufw allow %s/%s\n' "$port" "$proto" + fi ;; + nftables) + if [ -n "$src" ]; then + printf 'nft add rule inet filter input ip saddr %s %s dport %s accept\n' "$src" "$proto" "$port" + else + printf 'nft add rule inet filter input %s dport %s accept\n' "$proto" "$port" + fi ;; + iptables) + if [ -n "$src" ]; then + printf 'iptables -I INPUT -s %s -p %s --dport %s -j ACCEPT\n' "$src" "$proto" "$port" + else + printf 'iptables -I INPUT -p %s --dport %s -j ACCEPT\n' "$proto" "$port" + fi ;; + esac +} + +cmd_firewall() { + local sub="${1:-check}"; [ $# -gt 0 ] && shift || true + case "$sub" in check|open) : ;; *) die "Unbekannt: firewall $sub (check|open)" ;; esac + local apply="" src="" + while [ $# -gt 0 ]; do + case "$1" in + --apply) apply=1; shift ;; + --yes|-y) ASSUME_YES=1; shift ;; + --source) src="$2"; shift 2 ;; + *) die "Unbekannte Option fuer 'firewall': $1" ;; + esac + done + + local be role; be="$(fw_detect)" + role="$(fw_lc "${FW_ROLE:-single}")" + case "$role" in central|remote) : ;; *) role=single ;; esac + + printf '\n%sFirewall%s\n' "$C_BLD" "$C_RST" + case "$be" in + keine) + test_result warn "Keine aktive Firewall erkannt" + printf ' Entweder ist keine eingerichtet, oder die Pruefung braucht + root-Rechte. Mit sudo erneut aufrufen.\n' ;; + *) test_result ok "Erkannt: $be" + if [ "$be" = firewalld ]; then + printf ' Aktive Zonen: %s\n' "$(fw_zones | tr '\n' ' ' | sed 's/ $//')" + fi ;; + esac + case "$role" in + central) printf ' Rolle: Zentrale einer verteilten Ueberwachung\n' ;; + remote) printf ' Rolle: Remote-Site einer verteilten Ueberwachung\n' ;; + esac + + _FW_OPEN="" + if [ "$be" != keine ]; then + _FW_OPEN="$(fw_open_ports "$be")" + printf ' Offene Ports: %s\n' "$(printf '%s' "$_FW_OPEN" | tr '\n' ' ' | sed 's/ $//')" + fi + + printf '\n%sWas dieser Stack braucht%s\n' "$C_BLD" "$C_RST" + printf ' %-10s %-48s %-7s %s\n' "PORT" "ZWECK" "NOETIG" "ZUSTAND" + + local spec zweck need zustand port proto ist fehlend=0 + local -a todo=() + while IFS='|' read -r spec zweck need; do + [ -n "$spec" ] || continue + port="${spec%/*}"; proto="${spec##*/}" + ist=nein + if [ "$be" != keine ] && fw_port_open "$port" "$proto"; then ist=ja; fi + + case "$need:$ist" in + ja:ja) zustand="${C_GRN}offen${C_RST}" ;; + ja:nein) zustand="${C_RED}GESCHLOSSEN${C_RST}"; fehlend=$((fehlend+1)); todo+=("$spec") ;; + admin:ja) zustand="offen - besser auf das Adminnetz begrenzen" ;; + admin:nein) zustand="geschlossen" ;; + nein:ja) zustand="${C_YEL}offen - sollte zu sein${C_RST}" ;; + nein:nein) zustand="zu (richtig)" ;; + esac + if [ "$be" = keine ]; then zustand="nicht pruefbar"; fi + printf ' %-10s %-48s %-7s %b\n' "$spec" "$zweck" "$need" "$zustand" + done < <(fw_required_ports) + + # Hinweise, die sich aus der Konfiguration ergeben + local -a hinweise=() + if [ "$role" = remote ] && ! fw_on "${CMK_LIVESTATUS_TCP:-off}"; then + hinweise+=("Rolle 'remote', aber CMK_LIVESTATUS_TCP=off - dann lauscht auf 6557 nichts. + Einschalten mit: $SELF config set CMK_LIVESTATUS_TCP on (danach 'up')") + fi + if [ "$role" = central ]; then + hinweise+=("Als Zentrale baut diese Site die Verbindungen selbst auf: nach aussen + muessen 6557/tcp und 443/tcp zu den Remote-Sites erlaubt sein. Eingehend + braucht die Zentrale dafuer nichts - ausser dem, was oben steht.") + fi + if [ "${#hinweise[@]}" -gt 0 ]; then + printf '\n%sHinweise%s\n' "$C_BLD" "$C_RST" + local h; for h in "${hinweise[@]}"; do printf ' %s\n' "$h"; done + fi + + if [ "$be" = keine ]; then echo; return 0; fi + + echo + if [ "$fehlend" -eq 0 ]; then + ok "Alle benoetigten Ports sind offen." + echo; return 0 + fi + + err "$fehlend benoetigte(r) Port(s) sind geschlossen." + printf '\n%sZum Oeffnen%s\n' "$C_BLD" "$C_RST" + local t + for t in "${todo[@]}"; do + printf ' %s\n' "$(fw_open_cmds "$be" "${t%/*}" "${t##*/}" "$src")" + done + case "$be" in + firewalld) printf ' firewall-cmd --reload\n' ;; + iptables) printf ' # dauerhaft machen: iptables-save bzw. das Paket iptables-persistent\n' ;; + nftables) printf ' # dauerhaft machen: in /etc/nftables.conf uebernehmen\n' ;; + esac + if [ -n "$src" ]; then + case " ${todo[*]} " in + *" 80/tcp "*|*" 443/tcp "*) + printf '\n Achtung: Lets Encrypt prueft die Domain von aussen ueber Port 80. + Wird 80 auf ein Quellnetz begrenzt, schlaegt die Ausstellung fehl.\n' ;; + esac + fi + + if [ -n "$apply" ]; then + echo + if [ "$(id -u)" -ne 0 ]; then + die "Dafuer werden root-Rechte benoetigt - bitte mit sudo erneut aufrufen." + fi + confirm "Diese Regeln jetzt setzen?" || die "Abgebrochen." + local c + for t in "${todo[@]}"; do + c="$(fw_open_cmds "$be" "${t%/*}" "${t##*/}" "$src")" + log "$c" + eval "$c" || warn "Fehlgeschlagen: $c" + done + if [ "$be" = firewalld ]; then + firewall-cmd --reload >/dev/null 2>&1 && ok "firewalld neu geladen." \ + || warn "Neuladen fehlgeschlagen." + fi + ok "Regeln gesetzt." + else + printf '\n Automatisch setzen: %s firewall open --apply\n' "$SELF" + printf ' Nur fuer ein Netz : %s firewall open --apply --source 10.0.0.0/24\n' "$SELF" + fi + echo + return 1 +} + # ===================================================================== # Selbstpruefung # @@ -5393,6 +5836,7 @@ main() { config) cmd_config "$@" || true ;; selftest) cmd_selftest ;; tui-check) cmd_tui_check ;; + firewall) cmd_firewall "$@" ;; help|-h|--help) usage ;; *) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;; esac diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 2a18eb3..a859d81 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -73,14 +73,16 @@ env_set() { *$'\n'*) die "Ungueltiger Wert fuer '$key' (mehrzeilig)." ;; esac [ -n "$val" ] || [ -n "$allow_empty" ] || die "Leerer Wert fuer '$key' - abgebrochen." - # Werte mit Leerzeichen in Anfuehrungszeichen setzen + # Werte mit Leerzeichen gehoeren in der Datei in Anfuehrungszeichen - + # in der Umgebung aber ohne, sonst stecken die Zeichen spaeter im Wert + local quoted="$val" case "$val" in - *" "*) case "$val" in \"*\") : ;; *) val="\"$val\"" ;; esac ;; + *" "*) case "$val" in \"*\") : ;; *) quoted="\"$val\"" ;; esac ;; esac if grep -qE "^[#[:space:]]*${key}=" "$f"; then - sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${val}|" "$f" + sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${quoted}|" "$f" else - printf '%s=%s\n' "$key" "$val" >>"$f" + printf '%s=%s\n' "$key" "$quoted" >>"$f" fi export "$key=$val" }