From b912578f2744578888eef1ccb3992b0198561639 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 20 Sep 2026 19:46:33 +0200 Subject: [PATCH] Firewall-Pruefung: welche Ports offen sein muessen - und welche nicht Auf RHEL ist firewalld ab Werk aktiv und gibt in aller Regel nur SSH frei. Der Stack laeuft dann sauber, ist aber von aussen nicht zu erreichen: die Agents kommen nicht durch den Receiver auf 8000, und im Browser passiert nichts. Das war bisher nur eine Handvoll statischer Beispielzeilen in der README und in install.sh. Neu ist "cmk-manage.sh firewall". Erkannt werden firewalld, ufw, nftables und iptables; welche Ports noetig sind, ergibt sich aus der Konfiguration statt aus einer festen Liste: - ohne Proxy Manager wird 5000 zum Pflichtport, mit ihm soll es zu bleiben - offenes 5000 wird deshalb ausdruecklich angemerkt - der Admin-Port kommt aus NPM_ADMIN_PORT, nicht fest aus 81 - FW_ROLE unterscheidet single/central/remote: eine Remote-Site braucht 6557 eingehend, die Zentrale baut die Verbindungen selbst auf und braucht dafuer nichts. Steht FW_ROLE=remote bei CMK_LIVESTATUS_TCP=off, ist der Port zwar offen, es lauscht aber niemand - auch darauf weist der Bericht hin. - Event Console ueber FW_EC_SYSLOG / FW_EC_SNMPTRAP / FW_EC_TCP, Agent dieses Hosts ueber FW_AGENT, alles Weitere (Grafana, NSCA) ueber FW_EXTRA_PORTS Syslog und SNMP-Traps laufen ueber UDP, eine TCP-Freigabe nuetzt ihnen nichts. Ports werden deshalb durchweg als nummer/protokoll gefuehrt - beim Pruefen wie beim Erzeugen der Regeln. Bestehende Regeln werden entsprechend gelesen: Portbereiche (6556-6558 bzw. 6556:6558) werden aufgeloest, firewalld-Dienstnamen und Rich Rules zaehlen mit, bei firewalld alle aktiven Zonen statt nur der Standardzone, und eine ufw-Regel ohne Protokoll zaehlt fuer TCP und UDP - so wie ufw sie anlegt. "firewall open" zeigt die passenden Kommandos, --apply setzt sie nach Rueckfrage, --source begrenzt sie auf ein Quellnetz (fuer Port 80 mit Warnung, weil Lets Encrypt von aussen prueft). Ohne Befund ist der Rueckgabewert 1, damit sich das auswerten laesst. In der Oberflaeche steckt derselbe Bericht unter "Firewall pruefen". Nebenbei: env_set hat Werte mit Leerzeichen samt Anfuehrungszeichen in die laufende Umgebung exportiert. In der Datei gehoeren sie hin, im Wert nicht - das fiel erst mit den Beschreibungen in FW_EXTRA_PORTS auf. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 35 ++++ README.md | 111 ++++++++++- README_en.md | 108 +++++++++- install.sh | 40 ++-- scripts/cmk-manage.sh | 446 +++++++++++++++++++++++++++++++++++++++++- scripts/lib/common.sh | 10 +- 6 files changed, 722 insertions(+), 28 deletions(-) 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" }