# 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//*.log ──┐ │ Host db01 ◄──ssh──┼─┤ (journalctl -f / tail -F, Reconnect + Resume) │ │ Host ... ◄──ssh──┤ │ ▼ │ │ │ wazuh.manager ◄── (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.service` aktivieren, 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. ```bash ./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: ```bash 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 ```bash 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): ```bash ./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 ```bash ./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:/// (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 ```bash 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://: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. ```bash 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.log` oder `predecoder.hostname: web01` - FIM: `data.sshpull_fim.host: web01`, Befehls-Diffs: `location` enthält `wazuh-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` `yes` 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: 1. In `hosts.yml` beim Host `agent_tunnel: true` setzen. Host mit `--tunnel` vorbereiten. 2. Agent auf dem Host installieren (Paket von packages.wazuh.com, gleiche Version wie der Manager) mit `WAZUH_MANAGER=127.0.0.1`, bzw. in `ossec.conf` `
127.0.0.1
`. 3. `./setup.sh apply`. Der Collector hält den Tunnel offen, der Agent registriert sich über `127.0.0.1:1515` und 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 ```bash ./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 Gruppe `systemd-journal`. Danach neu einloggen bzw. den Collector neu starten. - Podman: Hängt `./setup.sh up` bei „podman wait“, konnte ein Container nicht angelegt werden. Die Ursache zeigt `sudo podman-compose --verbose up -d 2>&1 | grep -i error`. - `Host key verification failed`: `./setup.sh keyscan` ausführen, oder in `hosts.yml` `ssh.host_key_checking: accept-new` setzen. - Agentless-Checks laufen das erste Mal kurz nach dem Start, danach alle `interval` Sekunden. 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`) das `interval` großzügig wählen. - Baseline eines Hosts neu anlegen (z. B. nach einem gewollten Update): ` exec wazuh_wazuh.manager_1 rm /var/ossec/agentless/.sshpull/web01.json` - Ohne `sudo` sind root-only Dateien (z. B. `/etc/shadow`, Host-Keys) nur über Größe, Rechte und Besitzer überwacht, nicht über den Inhalt.