- testwebserver/: eigenstaendiger Python-Webserver (python:3.12-alpine, kein Build), Port/TLS/Text in .env, zeigt Client-IP und loggt jeden Zugriff - start.sh: erkennt Docker/Podman (inkl. rootless Port-Grenze), prueft Port-Konflikte, erzeugt bei TLS=yes ein selbstsigniertes Zertifikat, Befehle up/down/restart/logs/ps/test/firewall - firewall-check.sh: erkennt auch HTTP-Dienste ohne TLS als erreichbar - .gitignore: testwebserver/.env versionieren, testwebserver/certs/ ausschliessen - README: Abschnitt Testwebserver Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
335 lines
16 KiB
Bash
Executable File
335 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
|
# Prueft, warum ein veroeffentlichter Port (Standard: Dashboard-Port aus .env) von aussen
|
|
# nicht erreichbar ist, und legt auf Wunsch die fehlende Regel an.
|
|
#
|
|
# sudo ./firewall-check.sh pruefen, bei Problemen nachfragen
|
|
# sudo ./firewall-check.sh --yes pruefen und Regeln ohne Rueckfrage anlegen
|
|
# ./firewall-check.sh --check nur pruefen, nichts aendern
|
|
# ./firewall-check.sh --port 8443 anderen Port pruefen
|
|
# ./firewall-check.sh --port 8080 --tcp Dienst ohne HTTPS (nur TCP-Verbindung pruefen)
|
|
#
|
|
# Erkennt: firewalld (RHEL/Fedora), ufw (Ubuntu/Debian), iptables (legacy und nft),
|
|
# natives nftables, Docker-Ketten (DOCKER-USER, ufw-docker), Podman/netavark, CSF, Shorewall.
|
|
# Prueft ausserdem: lauscht etwas auf dem Port, Portweiterleitung (DNAT) des Containers
|
|
# vorhanden, net.ipv4.ip_forward, lokale Erreichbarkeit.
|
|
set -uo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
PORT=""
|
|
MODE=ask # ask | yes | check
|
|
TCP_ONLY=0 # 1 = keine HTTPS-Antwort erwarten
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--yes|-y) MODE=yes ;;
|
|
--check) MODE=check ;;
|
|
--tcp) TCP_ONLY=1 ;;
|
|
--port=*) PORT="${a#--port=}" ;;
|
|
--port) shift_next=1 ;;
|
|
-h|--help) sed -n '2,15p' "$0"; exit 0 ;;
|
|
*) if [ "${shift_next:-0}" = 1 ]; then PORT="$a"; shift_next=0; else echo "Unbekannte Option: $a" >&2; exit 2; fi ;;
|
|
esac
|
|
done
|
|
[ -n "$PORT" ] || PORT=$(grep '^DASHBOARD_PORT=' .env 2>/dev/null | cut -d= -f2-)
|
|
PORT=${PORT:-443}
|
|
[[ $PORT =~ ^[0-9]+$ ]] || { echo "Ungueltiger Port: $PORT" >&2; exit 2; }
|
|
[ "$MODE" = ask ] && [ ! -t 0 ] && MODE=check # ohne Terminal nie ungefragt aendern
|
|
|
|
ok() { echo -e " \e[1;32mOK\e[0m $*"; }
|
|
bad() { echo -e " \e[1;31mPROBLEM\e[0m $*"; }
|
|
note() { echo -e " \e[1;33mHINWEIS\e[0m $*"; }
|
|
head_() { echo -e "\n\e[1m$*\e[0m"; }
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
is_root() { [ "$(id -u)" -eq 0 ]; }
|
|
|
|
FIX_DESC=()
|
|
FIX_CMD=()
|
|
add_fix() { FIX_DESC+=("$1"); FIX_CMD+=("$2"); }
|
|
PROBLEMS=0
|
|
problem() { bad "$*"; PROBLEMS=$((PROBLEMS + 1)); }
|
|
|
|
echo -e "\e[1mFirewall-Check fuer TCP-Port $PORT\e[0m"
|
|
is_root || note "Nicht als root: iptables/nftables/ufw lassen sich nicht auslesen. Fuer den vollen Check: sudo $0"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
head_ "1. Lauscht etwas auf Port $PORT?"
|
|
LISTEN=$(ss -Hltnp "sport = :$PORT" 2>/dev/null)
|
|
if [ -z "$LISTEN" ]; then
|
|
problem "Nichts lauscht auf Port $PORT. Laeuft der Stack? (./setup.sh ps)"
|
|
else
|
|
ok "$(echo "$LISTEN" | awk '{print $4}' | sort -u | tr '\n' ' ')"
|
|
echo "$LISTEN" | grep -q 'users:' && echo " Prozess: $(echo "$LISTEN" | grep -o 'users:(([^)]*' | head -1 | cut -c9-)"
|
|
if echo "$LISTEN" | awk '{print $4}' | grep -qvE '^(127\.|\[::1\])'; then :; else
|
|
problem "Port lauscht nur auf localhost - in docker-compose.yml/.env den Port ohne 127.0.0.1 veroeffentlichen"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
head_ "2. Container und Portweiterleitung"
|
|
ENGINE=""; CPORT=""
|
|
for e in podman docker; do
|
|
have "$e" || continue
|
|
"$e" --version 2>/dev/null | grep -qi podman && [ "$e" = docker ] && continue # podman-docker-Wrapper
|
|
line=$("$e" ps --format '{{.Names}} {{.Ports}}' 2>/dev/null | grep -E ":$PORT->[0-9]+/tcp" | head -1)
|
|
if [ -n "$line" ]; then
|
|
ENGINE=$e
|
|
CPORT=$(echo "$line" | grep -oE ":$PORT->[0-9]+" | head -1 | cut -d'>' -f2)
|
|
ok "$e-Container ${line%% *} veroeffentlicht $PORT -> $CPORT"
|
|
break
|
|
fi
|
|
done
|
|
[ -z "$ENGINE" ] && note "Kein Docker/Podman-Container mit Port $PORT gefunden (anderer Dienst oder fehlende Rechte)"
|
|
|
|
dnat_present() {
|
|
{ iptables-save -t nat 2>/dev/null; ip6tables-save -t nat 2>/dev/null; nft list ruleset 2>/dev/null; } |
|
|
grep -E -- "(--dport|dport) $PORT\b" | grep -qi 'dnat'
|
|
}
|
|
if [ -n "$ENGINE" ] && is_root; then
|
|
if dnat_present; then
|
|
ok "Portweiterleitung (DNAT) fuer $PORT ist vorhanden"
|
|
elif [ "$ENGINE" = podman ] && podman info --format '{{.Host.NetworkBackend}}' 2>/dev/null | grep -q netavark; then
|
|
problem "Podman/netavark-Portweiterleitung fehlt - typisch nach 'firewall-cmd --reload' oder Firewall-Neustart"
|
|
add_fix "Podman-Netzregeln neu laden (podman network reload --all)" "podman network reload --all"
|
|
elif [ "$ENGINE" = docker ]; then
|
|
problem "Docker-Portweiterleitung fehlt - typisch nach Firewall-Neustart/-Reload ohne Docker-Neustart"
|
|
note "Behebung: systemctl restart docker (startet ALLE Container neu, daher nicht automatisch)"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
head_ "3. IP-Forwarding (fuer Container-Ports noetig)"
|
|
if [ "$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null)" = 1 ]; then
|
|
ok "net.ipv4.ip_forward = 1"
|
|
elif [ -n "$ENGINE" ]; then
|
|
problem "net.ipv4.ip_forward = 0 - weitergeleitete Pakete zum Container werden verworfen"
|
|
add_fix "IP-Forwarding einschalten (dauerhaft in /etc/sysctl.d/99-wazuh-forward.conf)" \
|
|
"echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wazuh-forward.conf && sysctl -q -w net.ipv4.ip_forward=1"
|
|
else
|
|
note "net.ipv4.ip_forward = 0 (nur fuer Container-Ports relevant)"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
head_ "4. Firewalls"
|
|
FOUND_FW=0
|
|
|
|
# --- firewalld (RHEL, Fedora, CentOS, SUSE) ---
|
|
if have firewall-cmd && firewall-cmd --state >/dev/null 2>&1; then
|
|
FOUND_FW=1
|
|
IFACE=$(ip route show default 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
|
|
ZONE=$(firewall-cmd --get-zone-of-interface="$IFACE" 2>/dev/null || firewall-cmd --get-default-zone)
|
|
allowed=no
|
|
firewall-cmd --zone="$ZONE" --query-port="$PORT/tcp" >/dev/null 2>&1 && allowed=port
|
|
if [ "$allowed" = no ]; then
|
|
for svc in $(firewall-cmd --zone="$ZONE" --list-services 2>/dev/null); do
|
|
firewall-cmd --info-service="$svc" 2>/dev/null | grep -E '^\s+ports:' | grep -qw "$PORT/tcp" && { allowed="Dienst $svc"; break; }
|
|
done
|
|
fi
|
|
target=$(firewall-cmd --permanent --zone="$ZONE" --get-target 2>/dev/null)
|
|
[ "$allowed" = no ] && [ "$target" = ACCEPT ] && allowed="Zone-Target ACCEPT"
|
|
if [ "$allowed" = no ] && [ -n "$ENGINE" ] && is_root &&
|
|
nft list chain inet firewalld filter_FORWARD 2>/dev/null | grep -q 'ct status dnat accept'; then
|
|
allowed="Container-Port: firewalld laesst per DNAT weitergeleitete Verbindungen durch"
|
|
fi
|
|
if [ "$allowed" != no ]; then
|
|
ok "firewalld aktiv, Zone '$ZONE' (Interface ${IFACE:-?}): $PORT/tcp erlaubt ($allowed)"
|
|
else
|
|
problem "firewalld aktiv, Zone '$ZONE' (Interface ${IFACE:-?}) erlaubt $PORT/tcp nicht"
|
|
# Runtime + permanent ohne --reload: ein Reload wuerde die Podman/Docker-Regeln loeschen
|
|
add_fix "firewalld: $PORT/tcp in Zone '$ZONE' freigeben (sofort + dauerhaft, ohne Reload)" \
|
|
"firewall-cmd -q --zone='$ZONE' --add-port=$PORT/tcp && firewall-cmd -q --permanent --zone='$ZONE' --add-port=$PORT/tcp"
|
|
fi
|
|
elif have firewall-cmd; then
|
|
note "firewalld installiert, aber nicht aktiv"
|
|
fi
|
|
|
|
# --- ufw (Ubuntu, Debian) ---
|
|
if have ufw && is_root && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
|
FOUND_FW=1
|
|
if ufw status verbose | grep -q 'Default:.*allow (incoming)'; then
|
|
ok "ufw aktiv, eingehend standardmaessig erlaubt"
|
|
elif ufw status | grep -qE "^$PORT(/tcp)?[[:space:]]+(ALLOW|LIMIT)"; then
|
|
ok "ufw aktiv, Regel fuer $PORT/tcp vorhanden"
|
|
else
|
|
problem "ufw aktiv und keine ALLOW-Regel fuer $PORT/tcp"
|
|
add_fix "ufw: $PORT/tcp erlauben" "ufw allow $PORT/tcp comment 'Wazuh Dashboard'"
|
|
fi
|
|
# ufw-docker: filtert Container-Verkehr ueber DOCKER-USER -> braucht "ufw route allow"
|
|
if [ -n "$CPORT" ] && iptables -S DOCKER-USER 2>/dev/null | grep -q 'ufw-user-forward'; then
|
|
if ufw status | grep -qE "^$CPORT/tcp.*ALLOW FWD"; then
|
|
ok "ufw-docker: Weiterleitung auf Container-Port $CPORT erlaubt"
|
|
else
|
|
problem "ufw-docker aktiv, Weiterleitung auf Container-Port $CPORT nicht erlaubt"
|
|
add_fix "ufw-docker: Weiterleitung auf Container-Port $CPORT erlauben" \
|
|
"ufw route allow proto tcp from any to any port $CPORT"
|
|
fi
|
|
fi
|
|
elif have ufw && ! is_root; then
|
|
note "ufw installiert - Status nur als root lesbar"
|
|
fi
|
|
|
|
# --- CSF (ConfigServer Firewall, cPanel/Plesk-Server) ---
|
|
if [ -f /etc/csf/csf.conf ]; then
|
|
FOUND_FW=1
|
|
if grep -E '^TCP_IN\s*=' /etc/csf/csf.conf | grep -qE "[\",]$PORT[\",]"; then
|
|
ok "CSF: $PORT in TCP_IN"
|
|
else
|
|
problem "CSF installiert, $PORT fehlt in TCP_IN (/etc/csf/csf.conf)"
|
|
note "CSF selbst eintragen und 'csf -r' ausfuehren; danach Container-Regeln neu laden (Docker/Podman neu starten bzw. podman network reload --all)"
|
|
fi
|
|
fi
|
|
|
|
# --- Shorewall ---
|
|
if have shorewall && shorewall status >/dev/null 2>&1; then
|
|
FOUND_FW=1
|
|
note "Shorewall aktiv: Regel in /etc/shorewall/rules anlegen, z.B. 'ACCEPT net \$FW tcp $PORT', dann 'shorewall reload'"
|
|
fi
|
|
|
|
# --- iptables (nur wenn kein Frontend die Regeln verwaltet) ---
|
|
if is_root && have iptables && [ "$FOUND_FW" = 0 ]; then
|
|
INPUT=$(iptables -S INPUT 2>/dev/null)
|
|
input_accepts() {
|
|
echo "$INPUT" | grep -E -- '-j ACCEPT' | grep -E -- "--dport $PORT\b|--dports [0-9,:]*\b$PORT\b" | grep -qv -- '-s '
|
|
}
|
|
if echo "$INPUT" | grep -qE '^-P INPUT (DROP|REJECT)' || echo "$INPUT" | grep -qE '^-A INPUT -j (DROP|REJECT)'; then
|
|
FOUND_FW=1
|
|
if input_accepts; then
|
|
ok "iptables INPUT: restriktiv, aber $PORT/tcp erlaubt"
|
|
else
|
|
problem "iptables INPUT verwirft standardmaessig und hat keine ACCEPT-Regel fuer $PORT/tcp"
|
|
add_fix "iptables: ACCEPT-Regel fuer $PORT/tcp in INPUT einfuegen" \
|
|
"iptables -I INPUT -p tcp --dport $PORT -m conntrack --ctstate NEW -j ACCEPT"
|
|
fi
|
|
else
|
|
ok "iptables INPUT: kein generelles DROP/REJECT"
|
|
fi
|
|
# Eigene Sperren in DOCKER-USER (gelten fuer alle Container-Ports)
|
|
if [ "$ENGINE" = docker ] && iptables -S DOCKER-USER 2>/dev/null | grep -qE -- '-j (DROP|REJECT)'; then
|
|
FOUND_FW=1
|
|
if iptables -S DOCKER-USER | grep -E -- '-j (ACCEPT|RETURN)' | grep -q -- "--ctorigdstport $PORT"; then
|
|
ok "DOCKER-USER: Sperrregeln vorhanden, $PORT ausgenommen"
|
|
else
|
|
problem "DOCKER-USER enthaelt DROP/REJECT-Regeln ohne Ausnahme fuer $PORT"
|
|
add_fix "DOCKER-USER: Ausnahme fuer Host-Port $PORT einfuegen" \
|
|
"iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport $PORT --ctdir ORIGINAL -j ACCEPT"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- natives nftables (eigene Tabellen, nicht firewalld/docker/netavark/iptables-nft) ---
|
|
if is_root && have nft && have python3; then
|
|
NFT_CHAINS=$(nft -j list ruleset 2>/dev/null | python3 -c '
|
|
import json, sys
|
|
port = sys.argv[1]
|
|
own = {"firewalld", "netavark"}
|
|
iptables_compat = {"nat", "filter", "mangle", "raw", "security"} # von iptables-nft angelegt
|
|
try:
|
|
data = json.load(sys.stdin)["nftables"]
|
|
except Exception:
|
|
sys.exit(0)
|
|
chains, rules = {}, {}
|
|
for item in data:
|
|
if "chain" in item:
|
|
c = item["chain"]
|
|
if c.get("hook") != "input" or c.get("type") != "filter" or c["table"] in own:
|
|
continue
|
|
if c["family"] in ("ip", "ip6") and c["table"] in iptables_compat:
|
|
continue
|
|
if True:
|
|
chains[(c["family"], c["table"], c["name"])] = c.get("policy", "accept")
|
|
elif "rule" in item:
|
|
r = item["rule"]
|
|
rules.setdefault((r["family"], r["table"], r["chain"]), []).append(json.dumps(r["expr"]))
|
|
for key, policy in chains.items():
|
|
body = rules.get(key, [])
|
|
has_drop = policy == "drop" or any(("\"drop\"" in e or "\"reject\"" in e) and "\"match\"" not in e for e in body)
|
|
allows = any("\"accept\"" in e and "\"dport\"" in e and (f": {port}}}" in e or f", {port}," in e or f"[{port}," in e or f", {port}]" in e) for e in body)
|
|
print(" ".join(key), "drop" if has_drop else "accept", "allow" if allows else "noallow")
|
|
' "$PORT")
|
|
while read -r fam tbl chain pol allow; do
|
|
[ -n "$fam" ] || continue
|
|
FOUND_FW=1
|
|
if [ "$pol" = accept ]; then
|
|
ok "nftables $fam $tbl/$chain: verwirft nicht generell"
|
|
elif [ "$allow" = allow ]; then
|
|
ok "nftables $fam $tbl/$chain: $PORT/tcp erlaubt"
|
|
else
|
|
problem "nftables $fam $tbl/$chain verwirft eingehend und erlaubt $PORT/tcp nicht"
|
|
add_fix "nftables: '$PORT/tcp accept' am Anfang von $fam $tbl/$chain einfuegen" \
|
|
"nft insert rule $fam $tbl $chain tcp dport $PORT accept"
|
|
NFT_PERSIST=1
|
|
fi
|
|
done <<< "$NFT_CHAINS"
|
|
fi
|
|
|
|
if [ "$FOUND_FW" = 0 ]; then
|
|
if is_root; then ok "Keine aktive Host-Firewall gefunden, die eingehend blockiert"
|
|
else note "Ohne root nicht vollstaendig pruefbar (iptables/nftables/ufw)"; fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
head_ "5. Lokaler Verbindungstest"
|
|
probe() { # probe HOST -> "HTTPS <code>" | "HTTP <code>" | "TCP offen" | leer
|
|
local code=""
|
|
have curl && code=$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' "https://$1:$PORT/" 2>/dev/null)
|
|
if [ -n "$code" ] && [ "$code" != 000 ]; then echo "HTTPS $code"; return; fi
|
|
have curl && code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://$1:$PORT/" 2>/dev/null)
|
|
if [ -n "$code" ] && [ "$code" != 000 ]; then echo "HTTP $code, ohne TLS"; return; fi
|
|
timeout 5 bash -c "exec 3<>/dev/tcp/$1/$PORT" 2>/dev/null && echo "TCP offen, aber keine HTTP(S)-Antwort"
|
|
}
|
|
MYIP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="src") print $(i+1)}' | head -1)
|
|
for h in 127.0.0.1 $MYIP; do
|
|
res=$(probe "$h")
|
|
if [ -z "$res" ]; then
|
|
problem "$h:$PORT nicht erreichbar"
|
|
elif [[ $res == TCP* ]] && [ "$TCP_ONLY" = 0 ]; then
|
|
# conmon (Podman) bzw. docker-proxy halten den Port offen, auch wenn dahinter nichts ankommt
|
|
problem "$h:$PORT nimmt Verbindungen an, liefert aber keine HTTP(S)-Antwort - Port nur reserviert (Weiterleitung kaputt?) oder Dashboard startet noch"
|
|
else
|
|
ok "$h:$PORT erreichbar ($res)"
|
|
fi
|
|
done
|
|
note "Lokal erreichbar heisst noch nicht von aussen. Von einem anderen Rechner: curl -kI https://${MYIP:-<server-ip>}:$PORT"
|
|
note "Ausserdem moegliche Sperren ausserhalb dieses Servers: Cloud-Firewall/Security-Group, Hypervisor-Firewall (z.B. Proxmox), Netzwerk-Firewall/VLAN"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
head_ "Ergebnis"
|
|
if [ "${#FIX_CMD[@]}" -eq 0 ]; then
|
|
if [ "$PROBLEMS" -eq 0 ]; then ok "Keine Probleme auf diesem Server gefunden"
|
|
else note "$PROBLEMS Problem(e) gefunden, aber keine automatische Behebung moeglich (siehe oben)"; fi
|
|
exit $(( PROBLEMS > 0 ))
|
|
fi
|
|
|
|
echo "Vorgeschlagene Aenderungen:"
|
|
for i in "${!FIX_CMD[@]}"; do
|
|
echo " $((i + 1)). ${FIX_DESC[$i]}"
|
|
echo " # ${FIX_CMD[$i]}"
|
|
done
|
|
|
|
if [ "$MODE" = check ]; then
|
|
echo "Nur Pruefung (--check). Anwenden mit: sudo $0 --yes"
|
|
exit 1
|
|
fi
|
|
is_root || { bad "Aenderungen brauchen root: sudo $0"; exit 1; }
|
|
if [ "$MODE" = ask ]; then
|
|
read -r -p "Aenderungen jetzt anwenden? [j/N] " answer
|
|
[[ $answer =~ ^[jJyY] ]] || { echo "Abgebrochen."; exit 1; }
|
|
fi
|
|
|
|
rc=0
|
|
for i in "${!FIX_CMD[@]}"; do
|
|
if bash -c "${FIX_CMD[$i]}"; then ok "${FIX_DESC[$i]}"
|
|
else bad "fehlgeschlagen: ${FIX_DESC[$i]}"; rc=1; fi
|
|
done
|
|
# iptables-Regeln dauerhaft speichern, sofern ein Speichermechanismus installiert ist
|
|
if printf '%s\n' "${FIX_CMD[@]}" | grep -q '^iptables '; then
|
|
if have netfilter-persistent; then netfilter-persistent save >/dev/null 2>&1 && ok "iptables-Regeln gespeichert (netfilter-persistent)"
|
|
elif [ -f /etc/sysconfig/iptables ] && have iptables-save; then iptables-save > /etc/sysconfig/iptables && ok "iptables-Regeln gespeichert (/etc/sysconfig/iptables)"
|
|
else note "iptables-Regel gilt bis zum Neustart. Dauerhaft z.B. mit iptables-persistent/iptables-services."; fi
|
|
fi
|
|
[ "${NFT_PERSIST:-0}" = 1 ] && note "nftables-Regel gilt bis zum Neustart. Dauerhaft: in /etc/nftables.conf (bzw. /etc/sysconfig/nftables.conf) eintragen."
|
|
|
|
echo
|
|
echo "Erneute Pruefung:"
|
|
recheck=(--check --port "$PORT"); [ "$TCP_ONLY" = 1 ] && recheck+=(--tcp)
|
|
"$0" "${recheck[@]}" | grep -E 'PROBLEM|Keine Probleme|Problem\(e\)' || true
|
|
exit $rc
|