diff --git a/.gitignore b/.gitignore index d192447..2cccd90 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ images/* backup-jobs.conf scripts/.h.sh scripts/.harness.sh +firewall-opened.conf diff --git a/README.md b/README.md index 3d5b443..fa0a8a6 100644 --- a/README.md +++ b/README.md @@ -1997,6 +1997,41 @@ anlegt. 80 nicht: Let's Encrypt prüft die Domain von außen, eine Begrenzung lässt die Ausstellung scheitern. Darauf weist der Bericht hin. +### Wieder zumachen + +`firewall open` kennt nur eine Richtung. Schaltet man die Event Console wieder +ab oder streicht einen Eintrag aus `FW_EXTRA_PORTS`, bleibt die Freigabe +stehen – der Port ist offen, ohne dass noch etwas dahinter lauscht. Dafür gibt +es die Gegenrichtung: + +```bash +./scripts/cmk-manage.sh firewall close # Kommandos nur anzeigen +sudo ./scripts/cmk-manage.sh firewall close --apply +``` + +Angeboten wird nur, was **offen ist, zu diesem Aufbau gehört und gerade nicht +gebraucht wird**. Dazu zählen die Ports aus der Tabelle oben und alles, was +`firewall open --apply` selbst geöffnet hat – das merkt sich das Werkzeug in +`firewall-opened.conf`, damit ein gestrichener `FW_EXTRA_PORTS`-Eintrag nicht +in Vergessenheit gerät. + +Drei Sicherungen sind eingebaut: + +| | | +|---|---| +| **SSH bleibt offen** | Port 22, der Port, auf dem `sshd` wirklich lauscht (aus `sshd -T`), und der Port der laufenden Sitzung (`SSH_CONNECTION`) | +| **Fremde Freigaben bleiben** | Wer 3306 für seine Datenbank offen hat, behält sie – geschlossen wird nur, was dieser Aufbau kennt | +| **Sammelregeln bleiben** | Eine Regel wie `80,443/tcp` deckt mehrere Ports ab; sie wird nicht angefasst, sondern gemeldet | + +Bei ufw wird dabei bewusst **nicht** nach Regelnummern gelöscht: die +verschieben sich beim ersten Löschen, der nächste Aufruf träfe die falsche +Regel. Stattdessen wird über die Regelangabe gelöscht +(`ufw delete allow 514/udp`), bei nftables über den Handle und bei iptables mit +derselben Regelangabe, mit der sie eingetragen wurde. + +In der Oberfläche liegt das unter **Einstellungen → Firewall → Nicht mehr +benötigte Ports schließen**. + > **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. @@ -2261,7 +2296,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) | +| `firewall [check\|open\|close]` | prüfen, öffnen und nicht mehr Benötigtes wieder schließen (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 3a59434..ef01a93 100644 --- a/README_en.md +++ b/README_en.md @@ -1988,6 +1988,38 @@ 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. +### Closing them again + +`firewall open` only knows one direction. Turn the Event Console off again or +drop an entry from `FW_EXTRA_PORTS`, and the rule stays – the port is open with +nothing listening behind it. Hence the opposite direction: + +```bash +./scripts/cmk-manage.sh firewall close # only print the commands +sudo ./scripts/cmk-manage.sh firewall close --apply +``` + +Only what is **open, belongs to this setup and is not currently needed** is +offered. That covers the ports from the table above plus everything +`firewall open --apply` opened itself – recorded in `firewall-opened.conf`, so +a deleted `FW_EXTRA_PORTS` entry is not forgotten. + +Three safeguards are built in: + +| | | +|---|---| +| **SSH stays open** | port 22, the port `sshd` actually listens on (from `sshd -T`), and the port of the running session (`SSH_CONNECTION`) | +| **Foreign rules stay** | if you have 3306 open for your database, you keep it – only what this setup knows about is closed | +| **Collective rules stay** | a rule like `80,443/tcp` covers several ports; it is reported instead of deleted | + +With ufw the rules are deliberately **not** deleted by number: those shift on +the first deletion, and the next call would hit the wrong rule. Deletion goes +by rule specification instead (`ufw delete allow 514/udp`), with nftables by +handle, and with iptables using the same specification it was added with. + +The GUI has this under **Einstellungen → Firewall → Nicht mehr benötigte Ports +schließen**. + > **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. @@ -2253,7 +2285,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) | +| `firewall [check\|open\|close]` | check, open, and close again what is no longer needed (firewalld, ufw, nftables, iptables) | | `shell` | root shell in the Checkmk container | | `omd …` | any `omd` command inside the container | diff --git a/scripts/cmk-manage.sh b/scripts/cmk-manage.sh index 0b9a377..787cf5b 100755 --- a/scripts/cmk-manage.sh +++ b/scripts/cmk-manage.sh @@ -52,9 +52,11 @@ KOMMANDOS - Stack pull Images aktualisieren status Status von Containern und Sites anzeigen logs [-f] [DIENST] Logs anzeigen - firewall [check|open [--apply] [--source CIDR]] + firewall [check|open|close] [--apply] [--source CIDR] Firewall pruefen: welche Ports der Stack braucht und - ob sie offen sind (firewalld, ufw, nftables, iptables) + ob sie offen sind (firewalld, ufw, nftables, iptables). + "close" schliesst wieder, was nicht mehr gebraucht wird + - SSH niemals, fremde Freigaben bleiben unberuehrt selftest Skripte auf bekannte Fallstricke pruefen tui-check Testzeilen fuer die Rahmendarstellung ausgeben (hilft bei q/x/l/m statt Linien, etwa in PuTTY) @@ -2007,6 +2009,7 @@ EOF # dieser Datei erzeugt wird ("job auto on"). # ===================================================================== +FW_STATE_FILE_NAME="firewall-opened.conf" JOBS_FILE_NAME="backup-jobs.conf" CRON_FILE="/etc/cron.d/checkmk-backup" @@ -5402,7 +5405,8 @@ gui_settings_category() { items+=("__npmrst__" ">> Datenbank zuruecksetzen (Konto neu anlegen lassen)") ;; backup) items+=("__jobs__" ">> Backup-Jobs verwalten") ;; firewall) items+=("__fwcheck__" ">> Firewall jetzt pruefen") - items+=("__fwapply__" ">> Firewall-Regeln uebernehmen (Ports oeffnen)") ;; + items+=("__fwapply__" ">> Firewall-Regeln uebernehmen (Ports oeffnen)") + items+=("__fwclose__" ">> Nicht mehr benoetigte Ports schliessen") ;; esac items+=("back" "zurueck") local sel @@ -5417,6 +5421,7 @@ gui_settings_category() { __jobs__) gui_jobs_menu || true; continue ;; __fwcheck__) gui_firewall || true; continue ;; __fwapply__) gui_firewall_apply || true; continue ;; + __fwclose__) gui_firewall_close || true; continue ;; __npm__) local f; f="$(mktemp)" { NO_COLOR=1 cmd_npm_enable; NO_COLOR=1 cmd_npm_port; } >"$f" 2>&1 || true ui_text "NGINX Proxy Manager" "$f"; rm -f "$f"; continue ;; @@ -6004,6 +6009,40 @@ Bitte als root erneut aufrufen." return 0 } +# Nicht mehr benoetigte Ports wieder schliessen +gui_firewall_close() { + local tmp rc=0 hat="" + tmp="$(mktemp)" + cmd_firewall close >"$tmp" 2>&1 || rc=$? + if ( set +o pipefail; grep -q 'Zum Schliessen' "$tmp" ); then hat=1; fi + ui_text "Nicht mehr benoetigte Ports" "$tmp" + rm -f "$tmp" + [ -n "$hat" ] || return 0 + + ui_yesno "Ports schliessen" "Die angezeigten Ports jetzt schliessen? + +SSH bleibt in jedem Fall offen - Port 22, der Port von sshd und der +Port dieser Sitzung. Freigaben fuer fremde Dienste werden nicht +angeruehrt." || return 0 + + local -a a=(firewall close --apply --yes) + if [ "$(id -u)" -eq 0 ]; then + ui_run "Ports schliessen" "$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." + return 0 + fi + + tmp="$(mktemp)" + cmd_firewall check >"$tmp" 2>&1 || true + ui_text "Firewall - Stand danach" "$tmp" + rm -f "$tmp" + return 0 +} + gui_main() { ui_require while :; do @@ -6369,9 +6408,301 @@ fw_open_cmds() { # backend port proto [quelle] esac } +# --------------------------------------------------------------------- +# Ports wieder zumachen +# +# "firewall open" kennt nur eine Richtung. Nimmt man einen Dienst +# wieder aus der Konfiguration - Event Console abgeschaltet, ein Port +# aus FW_EXTRA_PORTS gestrichen -, bleibt die Freigabe stehen. +# +# Zugemacht wird nur, was dieser Aufbau kennt: die Ports aus der Liste +# unten und das, was "firewall open --apply" selbst geoeffnet hat. +# Fremde Freigaben bleiben unangetastet - wer 3306 fuer seine Datenbank +# offen hat, soll sie behalten. +# +# SSH wird nie geschlossen. Weder Port 22 noch der Port, auf dem sshd +# tatsaechlich lauscht, noch der, ueber den diese Sitzung laeuft. +# --------------------------------------------------------------------- + +# Alle Ports, die zu diesem Aufbau gehoeren koennten - unabhaengig davon, +# ob sie gerade gebraucht werden +fw_known_ports() { + printf '80/tcp|HTTP\n' + printf '443/tcp|HTTPS\n' + printf '%s/tcp|Adminoberflaeche des Proxy Managers\n' "${NPM_ADMIN_PORT:-81}" + printf '5000/tcp|Checkmk direkt\n' + printf '8000/tcp|Agent-Receiver\n' + printf '6557/tcp|Livestatus\n' + printf '6558/tcp|Event Console ueber TCP\n' + printf '6556/tcp|Checkmk-Agent dieses Hosts\n' + printf '514/udp|Event Console: Syslog\n' + printf '514/tcp|Event Console: Syslog ueber TCP\n' + printf '162/udp|Event Console: SNMP-Traps\n' + # Was wir selbst geoeffnet haben, gehoert auch dazu + local e + while IFS= read -r e; do + [ -n "$e" ] || continue + printf '%s|selbst geoeffnet\n' "$e" + done < <(fw_state_list) +} + +fw_state_file() { printf '%s/%s\n' "$PROJECT_DIR" "$FW_STATE_FILE_NAME"; } + +fw_state_list() { + local f; f="$(fw_state_file)" + [ -f "$f" ] || return 0 + sed 's/#.*//; s/[[:space:]]//g' "$f" | sed '/^$/d' | sort -u +} + +fw_state_add() { # port/proto + local f; f="$(fw_state_file)" + if [ ! -f "$f" ]; then + cat >"$f" <<'EOF' +# Von "cmk-manage.sh firewall open --apply" geoeffnete Ports. +# Dient "firewall close" als Gedaechtnis - hier stehen nur Eintraege, +# die dieses Werkzeug selbst gesetzt hat. Von Hand geaendert werden +# muss hier nichts. +EOF + fi + ( set +o pipefail; grep -qxF "$1" "$f" ) || printf '%s\n' "$1" >>"$f" +} + +fw_state_del() { # port/proto + local f tmp; f="$(fw_state_file)" + [ -f "$f" ] || return 0 + tmp="$f.tmp" + # Bleibt nichts uebrig, liefert grep 1 - das darf weder die Datei + # stehen lassen noch unter "set -e" den Ablauf beenden. + grep -vxF "$1" "$f" > "$tmp" 2>/dev/null || true + mv "$tmp" "$f" 2>/dev/null || rm -f "$tmp" + return 0 +} + +# Ports, die unter keinen Umstaenden geschlossen werden +fw_protected_ports() { + printf '22/tcp\n' + # Wo lauscht sshd wirklich? + if command -v sshd >/dev/null 2>&1; then + sshd -T 2>/dev/null | sed -n 's/^port[[:space:]]\+\([0-9]\+\).*/\1\/tcp/p' + fi + sed -n 's/^[[:space:]]*[Pp]ort[[:space:]]\+\([0-9]\+\).*/\1\/tcp/p' \ + /etc/ssh/sshd_config 2>/dev/null + # Und der Port, ueber den diese Sitzung gerade laeuft + if [ -n "${SSH_CONNECTION:-}" ]; then + printf '%s/tcp\n' "$(printf '%s' "$SSH_CONNECTION" | awk '{print $4}')" + fi +} + +# Kommandos zum Schliessen. Anders als beim Oeffnen reicht eine Vorlage +# nicht: die Regel muss so getroffen werden, wie sie dasteht. +fw_close_cmds() { # backend port proto + local be="$1" port="$2" proto="$3" + case "$be" in + firewalld) + local zone svc rule + while IFS= read -r zone; do + [ -n "$zone" ] || continue + local plist + plist="$(firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n')" + if ( set +o pipefail; printf '%s\n' "$plist" | grep -qxF "$port/$proto" ); then + printf 'firewall-cmd --permanent --zone=%s --remove-port=%s/%s\n' "$zone" "$port" "$proto" + fi + while IFS= read -r svc; do + [ -n "$svc" ] || continue + if ( set +o pipefail; fw_service_port "$svc" | grep -qxF "$port/$proto" ); then + printf 'firewall-cmd --permanent --zone=%s --remove-service=%s\n' "$zone" "$svc" + fi + done < <(firewall-cmd --zone="$zone" --list-services 2>/dev/null | tr ' ' '\n') + while IFS= read -r rule; do + [ -n "$rule" ] || continue + case "$rule" in + *"port=\"$port\""*"protocol=\"$proto\""*) + printf "firewall-cmd --permanent --zone=%s --remove-rich-rule='%s'\n" "$zone" "$rule" ;; + esac + done < <(firewall-cmd --zone="$zone" --list-rich-rules 2>/dev/null) + done < <(fw_zones) ;; + ufw) + # NICHT nach Nummern loeschen: die verschieben sich beim + # ersten Loeschen, und der naechste Aufruf traefe die falsche + # Regel. Ueber die Regelangabe ist es von der Reihenfolge + # unabhaengig. Deckt eine Regel mehrere Ports ab, bleibt sie + # stehen - sonst faellt mehr weg als gewollt. + ufw status numbered 2>/dev/null \ + | awk -v p="$port" -v pr="$proto" ' + /^\[/ { + line = $0 + sub(/^\[[ \t]*[0-9]+\][ \t]*/, "", line) + if (line ~ /\(v6\)/) next # ufw loescht v4 und v6 zusammen + n = split(line, f, /[ \t]+/) + spec = f[1]; act = ""; from = "" + for (i = 2; i <= n; i++) { + if (f[i] == "ALLOW" || f[i] == "DENY" || f[i] == "LIMIT" || f[i] == "REJECT") { + act = f[i]; j = i + 1 + if (f[j] == "IN" || f[j] == "OUT" || f[j] == "FWD") j++ + for (; j <= n; j++) from = from (from == "" ? "" : " ") f[j] + break + } + } + if (act != "ALLOW") next + split(spec, a, "/") + plist = a[1]; sp = (length(a) > 1 ? a[2] : "") + if (sp != "" && sp != pr) next + m = split(plist, b, ",") + for (i = 1; i <= m; i++) if (b[i] == p) { + if (m > 1) { + printf "# Regel \"%s\" deckt mehrere Ports ab - bitte von Hand: ufw status numbered\n", spec + } else if (from == "Anywhere" || from == "") { + printf "ufw delete allow %s\n", spec + } else { + printf "ufw delete allow from %s to any port %s", from, p + if (sp != "") printf " proto %s", sp + printf "\n" + } + next + } + }' ;; + iptables) + # Die Regel so loeschen, wie sie eingetragen ist. Sammelregeln + # (--dports 80,443) bleiben stehen, sonst faellt mehr weg als + # gewollt. + iptables -S 2>/dev/null | grep -E ' -j (ACCEPT|accept)$' \ + | while IFS= read -r line; do + case "$line" in "-A "*) : ;; *) continue ;; esac + case "$line" in *" -p $proto "*) : ;; *) continue ;; esac + local plist="" + plist="$(printf '%s' "$line" | sed -n 's/.*--dports\{0,1\} \([0-9,:]*\).*/\1/p')" + [ -n "$plist" ] || continue + case ",$plist," in *",$port,"*) : ;; *) continue ;; esac + case "$plist" in + *,*) printf '# Regel deckt mehrere Ports ab (%s) - bitte von Hand: iptables -S\n' "$plist" ;; + *) printf 'iptables -D %s\n' "${line#-A }" ;; + esac + done ;; + nftables) + # Geloescht wird ueber den Handle. Steht der Port in einer + # Menge mit anderen, bleibt die Regel stehen. + nft -a list ruleset 2>/dev/null \ + | awk -v p="$port" -v pr="$proto" ' + /^table / { fam = $2; tbl = $3 } + /^[[:space:]]*chain / { ch = $2 } + $0 ~ (pr " dport") && $0 ~ /handle [0-9]+$/ { + line = $0 + h = line; sub(/.*handle /, "", h) + if (match(line, /dport[ \t]*\{[^}]*\}/)) { + set = substr(line, RSTART, RLENGTH) + sub(/dport[ \t]*\{/, "", set); sub(/\}/, "", set) + gsub(/[ \t]/, "", set) + n = split(set, a, ",") + for (i = 1; i <= n; i++) if (a[i] == p) { + if (n > 1) + printf "# Regel %s deckt mehrere Ports ab (%s) - bitte von Hand: nft -a list ruleset\n", h, set + else + printf "nft delete rule %s %s %s handle %s\n", fam, tbl, ch, h + } + } else if (match(line, ("dport[ \t]+" p "([^0-9]|$)"))) { + printf "nft delete rule %s %s %s handle %s\n", fam, tbl, ch, h + } + }' ;; + esac +} + +# Welche Ports sind offen, gehoeren zu diesem Aufbau, werden aber nicht +# mehr gebraucht? +fw_close_report() { # backend apply + local be="$1" apply="${2:-}" + local spec need port proto desc + local -a todo=() + + # Was gebraucht wird, darf nicht weg + local noetig="" + while IFS='|' read -r spec desc need; do + [ -n "$spec" ] || continue + case "$need" in ja|admin) noetig="$noetig$spec"$'\n' ;; esac + done < <(fw_required_ports) + + local geschuetzt; geschuetzt="$(fw_protected_ports | sed '/^$/d' | sort -u)" + + printf '\n%sNicht mehr benoetigt%s\n' "$C_BLD" "$C_RST" + printf ' %-10s %-44s %s\n' "PORT" "GEHOERT ZU" "ZUSTAND" + + local seen="" + while IFS='|' read -r spec desc; do + [ -n "$spec" ] || continue + case "$seen" in *"|$spec|"*) continue ;; esac + seen="$seen|$spec|" + port="${spec%/*}"; proto="${spec##*/}" + + fw_port_open "$port" "$proto" || continue + if ( set +o pipefail; printf '%s' "$noetig" | grep -qxF "$spec" ); then continue; fi + if ( set +o pipefail; printf '%s' "$geschuetzt" | grep -qxF "$spec" ); then + printf ' %-10s %-44s %b\n' "$spec" "$desc" "${C_GRN}bleibt offen (SSH)${C_RST}" + continue + fi + todo+=("$spec") + printf ' %-10s %-44s %b\n' "$spec" "$desc" "${C_YEL}kann zu${C_RST}" + done < <(fw_known_ports) + + if [ "${#todo[@]}" -eq 0 ]; then + echo + ok "Es ist nichts offen, was nicht gebraucht wird." + printf ' Fremde Freigaben - etwa fuer andere Dienste auf diesem Rechner - + werden dabei nicht angeruehrt.\n\n' + return 0 + fi + + printf '\n%sZum Schliessen%s\n' "$C_BLD" "$C_RST" + local t c n=0 + for t in "${todo[@]}"; do + while IFS= read -r c; do + [ -n "$c" ] || continue + printf ' %s\n' "$c"; n=$((n+1)) + done < <(fw_close_cmds "$be" "${t%/*}" "${t##*/}") + done + if [ "$n" -eq 0 ]; then + echo + warn "Die Regeln liessen sich nicht zuordnen - bitte von Hand pruefen." + echo; return 0 + fi + case "$be" in + firewalld) printf ' firewall-cmd --reload\n' ;; + iptables) printf ' # dauerhaft machen: iptables-save bzw. iptables-persistent\n' ;; + nftables) printf ' # dauerhaft machen: /etc/nftables.conf nachziehen\n' ;; + esac + + if [ -z "$apply" ]; then + printf '\n Ausfuehren mit: %s firewall close --apply\n\n' "$SELF" + return 0 + fi + + echo + if [ "$(id -u)" -ne 0 ]; then + die "Dafuer werden root-Rechte benoetigt - bitte mit sudo erneut aufrufen." + fi + confirm "Diese Ports jetzt schliessen?" || die "Abgebrochen." + for t in "${todo[@]}"; do + local fehler="" + while IFS= read -r c; do + [ -n "$c" ] || continue + case "$c" in + \#*) warn "${c#\# }"; fehler=1; continue ;; + esac + log "$c" + eval "$c" || { warn "Fehlgeschlagen: $c"; fehler=1; } + done < <(fw_close_cmds "$be" "${t%/*}" "${t##*/}") + if [ -z "$fehler" ]; then fw_state_del "$t"; fi + done + if [ "$be" = firewalld ]; then + firewall-cmd --reload >/dev/null 2>&1 && ok "firewalld neu geladen." \ + || warn "Neuladen fehlgeschlagen." + fi + ok "Ports geschlossen." + echo + return 0 +} + cmd_firewall() { local sub="${1:-check}"; [ $# -gt 0 ] && shift || true - case "$sub" in check|open) : ;; *) die "Unbekannt: firewall $sub (check|open)" ;; esac + case "$sub" in check|open|close) : ;; *) die "Unbekannt: firewall $sub (check|open|close)" ;; esac local apply="" src="" while [ $# -gt 0 ]; do case "$1" in @@ -6410,6 +6741,15 @@ cmd_firewall() { printf ' Offene Ports: %s\n' "$(printf '%s' "$_FW_OPEN" | tr '\n' ' ' | sed 's/ $//')" fi + if [ "$sub" = close ]; then + if [ "$be" = keine ]; then + echo; warn "Ohne erkannte Firewall gibt es nichts zu schliessen."; echo + return 0 + fi + fw_close_report "$be" "$apply" + return $? + fi + printf '\n%sWas dieser Stack braucht%s\n' "$C_BLD" "$C_RST" printf ' %-10s %-48s %-7s %s\n' "PORT" "ZWECK" "NOETIG" "ZUSTAND" @@ -6486,7 +6826,12 @@ cmd_firewall() { for t in "${todo[@]}"; do c="$(fw_open_cmds "$be" "${t%/*}" "${t##*/}" "$src")" log "$c" - eval "$c" || warn "Fehlgeschlagen: $c" + if eval "$c"; then + # Merken, damit "firewall close" spaeter weiss, was von uns war + fw_state_add "$t" + else + warn "Fehlgeschlagen: $c" + fi done if [ "$be" = firewalld ]; then firewall-cmd --reload >/dev/null 2>&1 && ok "firewalld neu geladen." \