
Experimenteller Köder-Broker
Ein containerisierter Netzwerk-Köder (Honeypot), der SSH, RDP und SMB annonciert, jede eingehende Verbindung mit eBPF beobachtet und jede Sitzung per Reverse-Proxy in einen isolierten Köder-Container leitet.
Das Design trennt zwei Aufgaben:
Dies ist ein defensives Werkzeug zur Erkennung und Untersuchung unbefugter Aktivitäten in Netzwerken, die Ihnen gehören oder die Sie überwachen dürfen. Setzen Sie es nur dort ein, wo Sie diese Befugnis haben.
flowchart TB
A["Attacker / Scanner"]
subgraph host["Decoy Host"]
direction TB
NIC["broker eth0<br/>published: 22, 3389, 445"]
subgraph brk["broker container"]
direction TB
E["eBPF TC classifier<br/>logs every SYN<br/>sees true source IP"]
P["reverse proxy<br/>CONNECT to backend"]
L["structured JSON logs"]
end
subgraph dec["decoynet (internal, no host route)"]
direction LR
S["ssh-decoy<br/>OpenCanary ssh<br/>port 2222"]
D["rdp-decoy<br/>OpenCanary rdp<br/>port 3389"]
M["smb-decoy<br/>Impacket SMB server<br/>port 445"]
end
end
A --> NIC
NIC --> E
NIC --> P
E --> L
P --> S
P --> D
P --> M
Insgesamt vier Container:
Die Köder leben in einem internal Docker-Netzwerk (decoynet) ohne Route zum Host oder zur Außenwelt. Nur der Broker kann sie erreichen. Nichts, was ein Angreifer in einem Köder tut, kann direkt das Host-Netzwerk erreichen.
Der Broker veröffentlicht die Ports 22, 3389 und 445 an den Host, sodass eingehende Pakete auf der eth0 des Brokers eintreffen. Zwei Dinge passieren dann mit jedem Paket:
broker/bpf/decoy.bpf.c) parst die Ethernet-, IP- und TCP-Header und schreibt für jeden neuen Verbindungsversuch (SYN gesetzt, ACK nicht gesetzt) ein conn_event in einen Ringpuffer: Quell-IP und Port, Ziel-Port, TCP-Flags und ob der Port ein annoncierter Dienst ist. Das Paket wird unverändert durchgelassen (TC_ACT_OK).CONNECT zum hinterlegten Köder-Backend für diesen Dienst aus, leitet dann Bytes in beide Richtungen.Die eBPF-Map advertised_ports wird beim Start aus config.yaml befüllt, sodass der Klassifikator markieren kann, ob ein Test einen bedienten Port oder einen unaufgeforderten Port getroffen hat. Dadurch sind horizontale Portscans sichtbar, obwohl nur drei Ports proxied werden.
Wenn Sie „alles ist offen“ annoncieren und beliebige Zielports in den Broker leiten möchten, erweitern Sie den Klassifikator, um den Zielport umzuschreiben, oder verwenden Sie eine TPROXY/bpf_sk_assign-Umleitung. Die aktuelle Version lässt den Paketpfad unberührt und beschränkt sich auf Beobachtung, was die sicherere Voreinstellung ist.
cyber-decoy/
├── README.md
├── docker-compose.yml # 4-container stack
├── docker-compose.override.yml # local macOS dev: no eBPF caps, port 22 remap
├── Makefile # build / up / down / bpf helpers
├── LICENSE
├── scripts/
│ └── setup.sh # host preflight checks
├── broker/
│ ├── Dockerfile # compiles eBPF object + Go binary
│ ├── config.yaml # advertised services (configurable)
│ ├── go.mod
│ ├── main.go # entrypoint
│ ├── bpf/
│ │ └── decoy.bpf.c # eBPF TC classifier
│ └── internal/
│ ├── config/config.go # config loader
│ ├── proxy/proxy.go # TCP reverse proxy
│ └── bpf/loader.go # loads + attaches eBPF, streams events
└── decoys/ # all three run OpenCanary
├── ssh/
│ ├── Dockerfile
│ └── opencanary.conf # ssh module, port 2222
├── rdp/
│ ├── Dockerfile
│ └── opencanary.conf # rdp module, port 3389
└── smb/
├── Dockerfile # single Python process, non-root
├── smb_decoy.py # Impacket SimpleSMBServer + JSON logging
└── requirements.txt # impacket (pinned)
docker-compose.override.yml verwenden, die auf die !reset/!override-Tags angewiesen ist).sudo mount -t bpf bpf /sys/fs/bpf.Das Broker-Image erkennt seine Build-Architektur und übergibt das passende Makro __TARGET_ARCH_* an clang, sodass es sowohl auf x86_64 als auch auf aarch64 (Apple Silicon, Graviton) baut. Beachten Sie, dass gcc-multilib bewusst nicht installiert ist: Es ist ein reines x86-Paket ohne arm64-Kandidaten, und seine Einbindung bricht den Build auf arm64 mit apt-Exitcode 100 ab. Es werden nur clang und libbpf-dev benötigt, um das eBPF-Objekt zu kompilieren.
Docker Desktop unter macOS führt Container in einer LinuxKit-VM aus, nicht auf Ihrem Host-Kernel, daher funktioniert TC/TCX-eBPF-Attach dort im Allgemeinen nicht. Das ist nicht fatal: eBPF ist absichtlich als „Best Effort“ konzipiert, sodass der Broker ebpf disabled: attach failed protokolliert und der Reverse-Proxy sowie alle drei Köder normal laufen und protokollieren. Sie können den gesamten Proxy-Pfad lokal entwickeln und testen und dann echte eBPF-Beobachtung erhalten, wenn Sie auf einem Linux-Host bereitstellen.
docker-compose.override.yml wird automatisch geladen und macht dies angenehm: Es entfernt die eBPF-Fähigkeiten (in der VM nutzlos) und kartiert Host-Port 22 auf 2022 um, da der Mac seinen eigenen sshd auf Port 22 hat.
docker compose up --build # lokale Entwicklung, Override angewandt
docker compose -f docker-compose.yml up -d # echte Bereitstellung, Override umgangen
Führen Sie zuerst den Vorab-Check durch:
./scripts/setup.sh
# 1. Baue alle vier Images (kompiliert das eBPF-Objekt im Broker-Image)
make build
# 2. Starte den Stack
make up
# 3. Beobachte, was passiert
make logs
Testen Sie dann von einem anderen Rechner (oder localhost für einen Rauchtest):
ssh -p 22 user@DECOY_HOST # trifft den SSH-Köder
nc DECOY_HOST 3389 # trifft den RDP-Köder
nc DECOY_HOST 445 # trifft den SMB-Köder
nc DECOY_HOST 8080 # nicht annonciert: von eBPF beobachtet, kein Proxy
Der Broker emittiert JSON für eBPF-Sondierungsereignisse und proxierte Sitzungen; jeder Köder emittiert OpenCanary-JSON-Ereignisse. Um zu sehen, wie Anmeldedaten eingehen:
docker compose logs -f ssh-decoy | grep 4002
Im Gegensatz zu einem reinen Banner-Stub führt ssh -p 22 user@DECOY_HOST nun einen echten Schlüsselaustausch durch und fordert zur Eingabe eines Passworts auf. Jeder Versuch wird erfasst. Überprüfen Sie, ob der Service-Fingerprint unter Versionserkennung standhält:
nmap -sV -p 22,3389,445 DECOY_HOST
Beenden mit:
make down
Dienste sind in broker/config.yaml definiert. Jeder Eintrag kann unabhängig aktiviert und umgekartet werden:
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
Um einen Dienst hinzuzufügen, fügen Sie hier einen Eintrag hinzu, veröffentlichen Sie den Port in docker-compose.yml und fügen Sie einen Köder-Container hinzu. Um einen zu deaktivieren, setzen Sie enabled: false (und optional entfernen Sie seinen veröffentlichten Port).
Beachten Sie, dass Host-Port 22 normalerweise vom echten SSH-Daemon des Hosts belegt ist. Für ein Labor können Sie die veröffentlichte Seite in docker-compose.yml umkarten, z. B. "2022:22", und Ihren Scanner dorthin richten.
Alle drei Köder laufen mit OpenCanary (Thinkst), so konfiguriert, dass jeder Container genau ein Modul aktiviert. Protokolle werden als JSON auf stdout emittiert, sodass docker compose logs und jeder SIEM-Shipper ohne zusätzliche Verkabelung funktionieren.
OpenCanary markiert jedes Ereignis mit einem numerischen logtype. Die hier vorkommenden:
Ein erfasster SSH-Anmeldeversuch sieht so aus:
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
Dies ist eine direkte Folge der Broker-Architektur und der wichtigste Punkt, den Sie beim Lesen dieser Protokolle verstehen müssen.
Der Broker beendet die TCP-Verbindung des Angreifers und öffnet eine neue Verbindung zum Köder. Aus Sicht von OpenCanary ist der Client also der Broker. Jeder src_host in einem Köder-Ereignis wird die Adresse des Brokers im decoynet sein, nicht die echte Quelle.
Die wahre Quell-IP wird dennoch erfasst, nur an einer anderen Stelle:
Die Zuordnung erfordert daher die Korrelation von Broker-Protokollen mit Köder-Protokollen, verknüpft über Zeitstempel und Dienst. Der Broker protokolliert remote (die echte Angreiferadresse) und backend für jede Sitzung – das ermöglicht die Verknüpfung:
docker compose logs broker | grep 'session opened' # wer
docker compose logs ssh-decoy | grep '"logtype": 4002' # was sie versucht haben
Wenn Sie die echte IP im Köder selbst benötigen, bestehen die Optionen darin, PROXY-Protokoll zu senden (die Köder parsen es nicht, das würde eine Änderung erfordern) oder den Userspace-Proxy durch eine transparente Weiterleitung (TPROXY oder eBPF bpf_sk_assign) zu ersetzen, die die ursprüngliche Quelladresse erhält. Beide sind in der Roadmap aufgeführt. Bis dahin behandeln Sie den Broker als Quelle der Wahrheit für „wer“ und den Köder als Quelle der Wahrheit für „was“.
Anders als SSH und RDP verwendet dieser Köder nicht OpenCanary. Das OpenCanary-SMB-Modul ist nur ein Log-Überwacher: Es verfolgt eine Datei und parst smbd_audit-Zeilen, die von einem echten Samba-Server emittiert werden, was den Betrieb von Samba plus rsyslog plus opencanaryd unter supervisord bedeutete – eine fünfstufige Kette, bei der jede Stufe stillschweigend ausfallen konnte.
smb-decoy ersetzt all dies durch einen einzigen Python-Prozess, der auf Impackets SimpleSMBServer basiert, einer reinen Python-Implementierung von SMB1/2/3. Er bindet Port 445, präsentiert lesbare Köder-Freigaben, beantwortet SMB2/3-Aushandlung (sodass nmap -sV einen echten Dienst sieht) und protokolliert Verbindungen und NTLM-Authentifizierungsversuche als ein JSON-Objekt pro Zeile auf stdout. Der erfasste Benutzername, die Domäne und der Arbeitsplatz aus der Authentifizierungsnachricht des Angreifers sind der Ertrag der Anmeldeerfassung.
Sicherheitshinweis: Impackets smbserver trug eine kritische Path-Traversal-Schwachstelle, CVE-2021-31800, die speziell Honeypots betraf. Sie wurde in 0.9.23 behoben. requirements.txt fixiert eine aktuelle Version und darf nicht unter diese herabgestuft werden. Der Container läuft außerdem nicht-root, schreibgeschützt und ohne jegliche Fähigkeiten außer NET_BIND_SERVICE.
Jeder Köder besitzt eine eigene opencanary.conf (installiert nach /etc/opencanaryd/). Nützliche Stellschrauben:
ssh.version in decoys/ssh/opencanary.conf. Derzeit gibt er SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1 vor. Passen Sie ihn an das Betriebssystem an, das Sie vorgeben; ein Ubuntu-Banner auf einem angeblichen Windows-Rechner ist verräterisch.addShare(...)-Aufrufe in decoys/smb/smb_decoy.py, sowie die Köderdateien, die in decoys/smb/Dockerfile erstellt werden. Die Freigabenamen und Dateinamen sind der Köder.backend in broker/config.yaml konsistent.Um ein weiteres OpenCanary-Modul zu aktivieren (ftp, telnet, mysql, vnc, redis u. a. sind verfügbar), setzen Sie <module>.enabled und <module>.port, fügen Sie einen Köder-Container hinzu und fügen Sie einen passenden Dienst in broker/config.yaml hinzu.
ssh-decoy mountet ein benanntes Volume unter /var/lib/opencanary (ssh.key_path), sodass der generierte Hostschlüssel Neustarts überlebt. Ohne dies generiert OpenCanary bei jedem Start einen neuen Schlüssel, und der wechselnde Fingerabdruck ist ein offensichtliches Indiz.
Der SMB-Köder ist nun ein einzelner Prozess, sodass die Fehlerbehebung unkompliziert ist.
docker compose logs -f smb-decoy
Jede Zeile ist JSON. Sie sollten beim Start ein smb_decoy_start sehen, danach Ereignisse wie smb_connect, smb_auth_attempt und smb_tree_connect, wenn Clients interagieren. Testen Sie es vom Host mit einem beliebigen SMB-Client:
# macOS Finder: Gehe > Mit Server verbinden
open 'smb://guest@localhost/HR-Payroll'
# oder von Linux
smbclient -L //localhost -p 445 -N
Häufige Probleme:
smb_decoy_start-Zeile und der Container beendet sich: Überprüfen Sie, ob requirements.txt sauber installiert wurde. Impacket benötigt Python 3.8+; das Image verwendet 3.12.smb_auth_attempt: Manche Clients listen Freigaben anonym auf, ohne sich jemals zu authentifizieren. Das erzeugt trotzdem smb_connect und smb_tree_connect. Erzwingen Sie eine Authentifizierung, indem Sie eine Freigabe mit einem Benutzernamen einbinden.src_host die Adresse des Brokers, nicht die des echten Angreifers. Korrelieren Sie mit Broker-Protokollen anhand des Zeitstempels.NET_ADMIN (sowie BPF/PERFMON auf neueren Kerneln), um das eBPF-Programm zu laden und anzuhängen. Die Compose-Datei fordert diese eingeschränkten Fähigkeiten an. Wenn Ihr Host oder Ihre Docker-Version sie ablehnt, ist die Ausweichlösung privileged: true beim Broker-Dienst, was breiter ist und nur verwendet werden sollte, wenn eingeschränkte Caps nicht funktionieren.internal-Netzwerk ohne Host-Route. Behalten Sie das so bei. Behandeln Sie jeden Köder-Container als potenziell kompromittiert.bpf_sk_assign in die Köder übertragen – die Notwendigkeit der Korrelation von Broker- und Köder-Protokollen entfällt.MIT. Siehe LICENSE.
| Container | Rolle | Netzwerk |
|---|
broker | Öffentliche Eingangstür: eBPF-Beobachtung plus Reverse-Proxy | edge + decoynet |
ssh-decoy | OpenCanary ssh-Modul (echter Handshake, erfasst Anmeldedaten) | nur decoynet |
rdp-decoy | OpenCanary rdp-Modul (NLA-Imitation, erfasst Benutzernamen) | nur decoynet |
smb-decoy | Impacket SimpleSMBServer (echtes SMB2/3, erfasst Authentifizierung) | nur decoynet |
| Container | OpenCanary-Modul | Hört auf | Was es tatsächlich tut |
|---|
ssh-decoy | ssh | 2222 | Echter SSH-Schlüsselaustausch über twisted.conch. Erfasst jedes Benutzername/Passwort-Paar. |
rdp-decoy | rdp | 3389 | Imitiert einen NLA-fähigen Server, gibt immer Login-Fehler zurück, extrahiert den mstshash-Benutzernamen. |
smb-decoy | Impacket | 445 | Reiner Python-SMB2/3-Server. Präsentiert Köder-Freigaben und protokolliert Verbindungen und NTLM-Authentifizierungsversuche als JSON. |
| logtype | Konstante in opencanary/logger.py | Bedeutung |
|---|
| 1000 | LOG_BASE_BOOT | Daemon-Start |
| 4000 | LOG_SSH_NEW_CONNECTION | SSH-Verbindung geöffnet |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | Client hat seine Versionszeichenkette gesendet |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | SSH-Anmeldeversuch (enthält BENUTZERNAME und PASSWORT) |
| 5000 | LOG_SMB_FILE_OPEN | SMB-Datei geöffnet (enthält BENUTZER, FREIGABENAME, DATEINAME) |
| 14001 | LOG_RDP | RDP-Verbindung / -Anmeldeversuch |
| Ebene | Kennt die echte Quell-IP? | Weiß, was versucht wurde? |
|---|
eBPF-Klassifikator (probe observed) | Ja | Nein, nur SYN-Metadaten |
Broker-Proxy (session opened) | Ja | Nein, nur Bytezahlen |
OpenCanary-Köder (logtype 4002) | Nein | Ja, Anmeldedaten/Dateien |