- 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>
14 KiB
Wazuh in Docker – Daten per SSH abholen
Wazuh 4.14 (Manager, Indexer, Dashboard) als Single-Node in Docker. Die überwachten Hosts brauchen keinen Agent und keine eingehende Verbindung zum Server. Alles läuft über SSH, das der Server zu den Hosts aufbaut.
┌──────────────────────── Docker ───────────────────────────┐
Host web01 ◄──ssh──┐ │ sshpull.collector ──► /remote-logs/<host>/*.log ──┐ │
Host db01 ◄──ssh──┼─┤ (journalctl -f / tail -F, Reconnect + Resume) │ │
Host ... ◄──ssh──┤ │ ▼ │
│ │ wazuh.manager ◄── <localfile> (Decoder/Regeln) │
└─┤ wazuh-agentlessd: FIM + Befehls-Diffs per ssh │
│ │ filebeat │
│ ▼ │
│ wazuh.indexer ◄── wazuh.dashboard :443 │
└───────────────────────────────────────────────────────────┘
Was pro Host abgeholt wird
| Baustein | Wie | Ergebnis in Wazuh |
|---|---|---|
logs: journal |
journalctl -o json -f über eine dauerhafte SSH-Verbindung. Nach einem Abbruch geht es per Journal-Cursor lückenlos weiter. |
Alle Standard-Decoder/-Regeln (sshd, sudo, PAM, Kernel, …), z. B. Brute-Force-Erkennung |
logs: file |
tail -F auf eine Datei. Nach einem Abbruch geht es über Inode und Byte-Offset weiter. |
Wie oben, format wählbar (syslog, apache, json, audit, …) |
integrity |
Agentless: Größe, Rechte, Besitzer und SHA256 aller Dateien unter den Pfaden, periodisch. Der Vergleich läuft gegen eine eigene Baseline pro Host. | Alarme bei neuen, geänderten oder gelöschten Dateien (siehe Regeln unten) |
commands |
Agentless: Befehle ausführen und die Ausgabe mit dem letzten Lauf vergleichen. | Alarm „agentless: Change detected“ mit Diff (z. B. neuer offener Port, neuer Benutzer) |
agent_tunnel |
Optional: Reverse-Tunnel ssh -R 1514/1515. Ein echter Wazuh-Agent auf dem Host verbindet sich mit 127.0.0.1. |
Voller Agent-Umfang (SCA, Schwachstellen, Inventar, Echtzeit-FIM) |
Die Agentless-Skripte (manager/agentless/sshpull_*) sind eigene Versionen ohne expect. Sie
nutzen Schlüssel-Login und optional sudo -n. Bei den mitgelieferten Wazuh-Skripten wirkt die
use_sudo-Option nicht.
Der Integritätscheck nutzt bewusst nicht das FIM-Protokoll von Wazuh. Damit landen alle
Agentless-Hosts in der FIM-Datenbank des Managers, nur nach Pfad geordnet. /etc/passwd von
Host A, Host B und vom Manager würde sich gegenseitig überschreiben, und der erste Lauf erzeugt
pro Datei einen „File added“-Alarm. Hier gilt stattdessen: Der erste Lauf legt nur die Baseline
an (ein Alarm), danach werden nur echte Änderungen gemeldet.
Eigene Regeln (manager/rules/sshpull_rules.xml)
| ID | Level | Bedeutung |
|---|---|---|
| 100501 | 3 | FIM-Baseline für einen Host angelegt |
| 100502 | 5 | Datei hinzugefügt |
| 100503 | 7 | Datei geändert (Inhalt, Größe, Rechte oder Besitzer) |
| 100504 | 7 | Datei gelöscht |
| 100505 | 10 | Mehr als 200 Änderungen in einem Lauf: nur eine Zusammenfassung (Update oder Kompromittierung?) |
| 100506 | 10 | Sicherheitskritische Datei (passwd, shadow, sudoers, sshd_config, pam.d, cron, authorized_keys, ld.so.preload) |
| 100507 | 10 | Neue oder geänderte Datei mit SUID/SGID-Bit |
| 100511 | 7 | SSH-Abholung gestört (Log-Stream, Tunnel oder Agentless-Check). Wird nur beim Zustandswechsel gemeldet. |
| 100512 | 3 | SSH-Abholung wieder verbunden |
Befehls-Diffs kommen über die Wazuh-Standardregel 555 („Integrity checksum for agentless device changed“), mit dem Diff im Alarmtext.
Installation
Voraussetzungen: Docker oder Podman, ca. 8 GB RAM frei, ca. 10 GB Platz für Images und Daten.
setup.sh erkennt die Umgebung selbst (./setup.sh info zeigt, was erkannt wurde):
| Erkannt | Verwendet |
|---|---|
| Docker | docker compose oder docker-compose |
| Podman (z. B. RHEL, auch mit podman-docker-Wrapper) | podman-compose oder podman compose |
| SELinux aktiv | Alle Bind-Mounts haben :z. Ohne SELinux wirkt das nicht. |
Als root (sudo ./setup.sh ...) passt das Skript das System zusätzlich an:
vm.max_map_count=262144(dauerhaft in/etc/sysctl.d/99-wazuh.conf)- Firewall-Check: Dashboard-Port prüfen und fehlende Regeln anlegen (siehe unten)
- Podman:
podman-restart.serviceaktivieren, damit der Stack nach einem Reboot startet
Ohne root gibt es dafür nur Hinweise. Mit WAZUH_SKIP_SYSCTL=1 bleibt der Kernel-Parameter
unangetastet. Mit CONTAINER_ENGINE=docker|podman lässt sich die Erkennung übersteuern.
./setup.sh # Passwörter (.env), TLS-Zertifikate, SSH-Key ssh/id_ed25519, Images bauen
nano hosts.yml # Hosts eintragen (siehe hosts.yml.example)
Proxy (optional)
Kommt der Server nur über einen Proxy ins Internet, trägst du ihn in .env ein:
cp .env.example .env # nur vor dem allerersten ./setup.sh nötig
nano .env # HTTP_PROXY=http://proxy.firma.local:3128, HTTPS_PROXY=..., ggf. NO_PROXY=...
Alternativ übernimmt ./setup.sh beim ersten Lauf HTTP_PROXY/HTTPS_PROXY aus der Shell.
Genutzt wird der Proxy für:
- Image-Build (
dnf,apt,pip) und das Zertifikat-Tool - Laufzeit: Manager und Dashboard (Schwachstellen-Feeds von cti.wazuh.com, Update-Check)
- Podman: auch für das Laden der Images
Die internen Dienstnamen (wazuh.manager, wazuh.indexer, …) sind immer in NO_PROXY. SSH
zu den Hosts läuft nie über den Proxy.
Docker: Images lädt der Docker-Daemon, nicht der Container. Ohne Daemon-Proxy gibt
setup.sh eine Warnung mit der fertigen systemd-Konfiguration aus. setup.sh ändert die nicht
selbst, weil ein Neustart von Docker alle Container auf dem Server neu startet.
Red Hat / Podman
sudo dnf install -y podman openssh-clients
sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm # RHEL 9
sudo dnf install -y podman-compose
sudo ./setup.sh # Podman bitte als root, alle weiteren Befehle ebenfalls mit sudo
Podman muss als root laufen. Rootless scheitert an den Ulimits der Wazuh-Container (memlock,
nofile), an Port 443 und an den Zertifikatsrechten. setup.sh bricht in diesem Fall mit einem
Hinweis ab.
Zielhosts vorbereiten
Auf jedem Host wird ein Benutzer wazuh-pull angelegt. Er bekommt die Gruppen
systemd-journal und adm (Leserechte auf Logs) und den öffentlichen Schlüssel mit
restrict (kein PTY, keine Weiterleitungen):
./setup.sh host-script | ssh admin@web01 sudo sh
# Optionen: --sudo NOPASSWD-sudo (nur nötig für root-only Dateien, z.B. /etc/shadow im FIM)
# --tunnel erlaubt Reverse-Forwarding auf 127.0.0.1:1514/1515 (für agent_tunnel)
Das Skript erst ansehen, dann ausführen: ./setup.sh host-script
Host-Keys, Test, Start
./setup.sh keyscan # Host-Keys -> ssh/known_hosts, Fingerprints mit den Hosts vergleichen!
./setup.sh test # prüft Login, Gruppen, Journal-/Datei-Leserechte
./setup.sh up # starten (down, ps, logs [dienst] analog)
./setup.sh passwords # Login fürs Dashboard: https://<server>/ (admin / ...)
Der erste Start dauert 1–3 Minuten, bis der Indexer bereit ist.
Hosts ändern
hosts.yml bearbeiten, dann ./setup.sh apply. Damit werden Manager und Collector neu
erzeugt. Der Manager schreibt seine Agentless- und Localfile-Konfiguration beim Start aus
hosts.yml neu (manager/generate.py).
Dashboard nicht erreichbar? Firewall-Check
sudo ./setup.sh firewall # = sudo ./firewall-check.sh (prüft, fragt vor Änderungen)
sudo ./setup.sh firewall --yes # fehlende Regeln ohne Rückfrage anlegen
./setup.sh firewall --check # nur prüfen
Geprüft wird der Dashboard-Port aus .env (anderer Port: --port N, Dienst ohne HTTPS: --tcp):
| Prüfung | Automatische Behebung |
|---|---|
| Lauscht etwas auf dem Port (nicht nur auf 127.0.0.1)? | – |
| Container-Portweiterleitung (DNAT) vorhanden? | Podman: podman network reload --all. Docker: nur Hinweis, weil systemctl restart docker alle Container neu startet. |
net.ipv4.ip_forward |
einschalten (dauerhaft) |
| firewalld (Zone des Interfaces mit Default-Route) | Port freigeben, sofort und dauerhaft, ohne --reload |
| ufw, auch ufw-docker | ufw allow bzw. ufw route allow |
| iptables (INPUT-Policy, DOCKER-USER) | ACCEPT-Regel. Wird gespeichert, wenn netfilter-persistent oder iptables-services vorhanden ist. |
natives nftables (eigene input-Kette mit drop) |
nft insert rule … (dauerhaft selbst in nftables.conf eintragen) |
| CSF, Shorewall | erkennen, nur Hinweis |
| Lokaler HTTPS-Test (localhost und Server-IP) | – |
Typischer Fall „netstat zeigt den Port, aber nichts kommt an“: Bei Podman hält conmon
den Port reserviert, auch wenn die Portweiterleitung fehlt. Das passiert z. B., wenn die
Firewall nach dem Container-Start neu geladen wurde (firewall-cmd --reload, Neustart von
iptables/nftables). Der Check erkennt das am fehlenden DNAT bzw. an „TCP offen, aber kein
HTTPS“ und lädt die Podman-Regeln neu.
./setup.sh up ruft den Check nach dem Start automatisch auf, als root mit --yes. Sperren
außerhalb des Servers (Cloud-Firewall, Proxmox-Firewall, Netzwerk-Firewall) kann er nicht
sehen. Dafür von einem anderen Rechner testen: curl -kI https://<server>:443
Testwebserver (testwebserver/)
Ein kleiner Webserver, um die Erreichbarkeit eines Ports unabhängig von Wazuh zu testen. Er ist eigenständig, du kannst das Verzeichnis auch allein auf einen anderen Rechner kopieren.
cd testwebserver
nano .env # PORT=8080, TLS=no|yes, MESSAGE=...
./start.sh # erkennt Docker/Podman, startet und testet lokal
./start.sh logs # jede Anfrage mit Client-IP: kommt der Zugriff von außen überhaupt an?
sudo ./start.sh firewall # Firewall-Check für diesen Port (legt fehlende Regeln an)
./start.sh down
Es wird kein eigenes Image gebaut, nur das offizielle python:3.12-alpine geladen. Bei
TLS=yes erzeugt start.sh ein selbstsigniertes Zertifikat (braucht openssl). Ist der Port
schon belegt, meldet start.sh das vor dem Start.
Wo finde ich die Daten im Dashboard?
Per SSH geholte Logs und Agentless-Ergebnisse laufen über den Manager. Im Dashboard
erscheinen sie deshalb beim Agent 000 (wazuh.manager). Unterscheiden lassen sich die Hosts
so:
- Logs:
location: /remote-logs/web01/journal.logoderpredecoder.hostname: web01 - FIM:
data.sshpull_fim.host: web01, Befehls-Diffs:locationenthältwazuh-pull@web01 - Verbindungsstatus:
rule.groups: sshpull_status
Beispiel-Filter in Threat Hunting: rule.groups: sshpull oder location:*web01*
Hinweis: Rohlogs ohne Regeltreffer werden (wie bei Wazuh üblich) nicht indexiert, nur Alerts.
Alles archivieren: in config/wazuh_cluster/wazuh_manager.conf <logall_json>yes</logall_json>
setzen und in Filebeat das Archiv aktivieren (siehe Wazuh-Doku „archives“).
Optional: voller Agent über SSH-Tunnel
Wenn auf einem Host ein Wazuh-Agent installiert werden darf, er aber den Server nicht erreicht:
- In
hosts.ymlbeim Hostagent_tunnel: truesetzen. Host mit--tunnelvorbereiten. - Agent auf dem Host installieren (Paket von packages.wazuh.com, gleiche Version wie der
Manager) mit
WAZUH_MANAGER=127.0.0.1, bzw. inossec.conf<address>127.0.0.1</address>. ./setup.sh apply. Der Collector hält den Tunnel offen, der Agent registriert sich über127.0.0.1:1515und erscheint als eigener Agent im Dashboard.
Ports 1514 und 1515 sind nicht nach außen veröffentlicht. Nur der Collector erreicht sie.
Dateien
| Pfad | Zweck |
|---|---|
hosts.yml |
Die einzige Stelle für Hosts und Optionen |
setup.sh |
Einrichtung und Betrieb (erkennt Docker/Podman) |
firewall-check.sh |
Erreichbarkeit des Dashboard-Ports prüfen und reparieren |
testwebserver/ |
Eigenständiger Testwebserver für Port-/Firewall-Tests |
docker-compose.yml |
Stack. Nach außen geht nur das Dashboard (DASHBOARD_PORT) |
.env |
Version, Zeitzone, Passwörter (von setup.sh erzeugt, nicht committen) |
ssh/ |
id_ed25519 (privater Schlüssel, nicht committen), known_hosts |
common/sshpull_hosts.py |
Laden und Prüfen von hosts.yml, SSH-Config erzeugen (Manager und Collector) |
collector/ |
Log-Streaming-Dienst |
manager/ |
Manager-Image: openssh-client, Agentless-Skripte, Config-Generator |
config/ |
Wazuh-Konfiguration (Basis: wazuh/wazuh-docker single-node) |
Fehlersuche
./setup.sh logs sshpull.collector # Verbindungen/Reconnects pro Host
./setup.sh logs wazuh.manager | grep -i agentless
Für exec die passende Engine nehmen (docker oder sudo podman). Die Containernamen zeigt
./setup.sh ps, z. B.:
sudo podman exec wazuh_wazuh.manager_1 ls -la /remote-logs/web01/
journal=WARNUNG:nur-eigene-Logs…bei./setup.sh test: Der Benutzer ist nicht in der Gruppesystemd-journal. Danach neu einloggen bzw. den Collector neu starten.- Podman: Hängt
./setup.sh upbei „podman wait“, konnte ein Container nicht angelegt werden. Die Ursache zeigtsudo podman-compose --verbose up -d 2>&1 | grep -i error. Host key verification failed:./setup.sh keyscanausführen, oder inhosts.ymlssh.host_key_checking: accept-newsetzen.- Agentless-Checks laufen das erste Mal kurz nach dem Start, danach alle
intervalSekunden. Der erste Lauf legt nur die Baseline an, Alarme gibt es erst bei Änderungen. Die Checks laufen nacheinander. Bei vielen Hosts mit großen Pfaden (/usr) dasintervalgroßzügig wählen. - Baseline eines Hosts neu anlegen (z. B. nach einem gewollten Update):
<docker|podman> exec wazuh_wazuh.manager_1 rm /var/ossec/agentless/.sshpull/web01.json - Ohne
sudosind root-only Dateien (z. B./etc/shadow, Host-Keys) nur über Größe, Rechte und Besitzer überwacht, nicht über den Inhalt.