
Broker de leurres expérimentaux
Un leurre réseau conteneurisé (honeypot) qui annonce SSH, RDP et SMB, observe chaque connexion entrante avec eBPF, et proxy inverse chaque session dans un conteneur leurre isolé.
La conception sépare deux préoccupations :
Cet outil est un outil défensif pour détecter et étudier les activités non autorisées sur les réseaux que vous possédez ou que vous êtes autorisé à surveiller. Déployez-le uniquement là où vous avez cette autorité.
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
Quatre conteneurs au total :
Les leurres vivent sur un réseau Docker internal (decoynet) sans route vers l'hôte ou le monde extérieur. Seul le broker peut les atteindre. Rien de ce qu'un attaquant fait à l'intérieur d'un leurre ne peut atteindre directement le réseau hôte.
Le broker publie les ports 22, 3389 et 445 vers l'hôte, donc les paquets entrants arrivent sur l'eth0 du broker. Deux choses se produisent alors pour chaque paquet :
broker/bpf/decoy.bpf.c) analyse les en-têtes Ethernet, IP et TCP, et pour chaque nouvelle tentative de connexion (SYN positionné, ACK effacé) écrit un conn_event dans un tampon circulaire : IP source et port, port de destination, drapeaux TCP, et si le port est un service annoncé. Le paquet est transmis inchangé (TC_ACT_OK).CONNECT vers le backend leurre pour ce service, puis relaie les octets dans les deux sens.La table eBPF advertised_ports est peuplée au démarrage depuis config.yaml, afin que le classifieur puisse marquer si un sondage a touché un port servi ou non sollicité. Cela rend visibles les scans de ports horizontaux même si seuls trois ports sont proxyés.
Si vous souhaitez annoncer « tout est ouvert » et diriger des ports de destination arbitraires vers le broker, étendez le classifieur pour réécrire le port de destination ou utilisez une redirection TPROXY / bpf_sk_assign. La version actuelle laisse le chemin du paquet intact et se limite à l'observation, ce qui est le défaut le plus sûr.
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 fourni, qui repose sur les balises !reset / !override).sudo mount -t bpf bpf /sys/fs/bpf.L'image broker détecte son architecture de construction et transmet la macro __TARGET_ARCH_* correspondante à clang, de sorte qu'elle se compile à la fois sur x86_64 et aarch64 (Apple Silicon, Graviton). Notez que gcc-multilib n'est délibérément pas installé : c'est un paquetage x86 uniquement sans candidat arm64, et l'inclure casse la construction sur arm64 avec le code de sortie apt 100. Seuls clang et libbpf-dev sont nécessaires pour compiler l'objet eBPF.
Docker Desktop sur macOS exécute les conteneurs dans une VM LinuxKit plutôt que sur votre noyau hôte, donc l'attachement eBPF TC/TCX ne fonctionnera généralement pas là-bas. Ce n'est pas fatal : eBPF est au mieux fourni par conception, donc le broker enregistre ebpf disabled: attach failed et le proxy inverse ainsi que les trois leurres fonctionnent et journalisent normalement. Vous pouvez développer et tester l'ensemble du chemin de proxy localement, puis obtenir une véritable observation eBPF lorsque vous déployez sur un hôte Linux.
docker-compose.override.yml est chargé automatiquement et rend cela agréable : il supprime les capacités eBPF (inutiles dans la VM) et remappe le port hôte 22 vers 2022, puisque le propre sshd du Mac occupe le 22.
docker compose up --build # local dev, override applied
docker compose -f docker-compose.yml up -d # real deployment, override bypassed
Exécutez d'abord la vérification préliminaire :
./scripts/setup.sh
# 1. Build all four images (compiles the eBPF object inside the broker image)
make build
# 2. Start the stack
make up
# 3. Watch what happens
make logs
Ensuite, sondez-le depuis une autre machine (ou localhost pour un test de fumée) :
ssh -p 22 user@DECOY_HOST # hits the SSH decoy
nc DECOY_HOST 3389 # hits the RDP decoy
nc DECOY_HOST 445 # hits the SMB decoy
nc DECOY_HOST 8080 # unadvertised: observed by eBPF, no proxy
Le broker émet du JSON pour les événements de sonde eBPF et les sessions proxyées ; chaque leurre émet des événements JSON OpenCanary. Pour voir arriver les identifiants :
docker compose logs -f ssh-decoy | grep 4002
Contrairement à un simple bannière, ssh -p 22 user@DECOY_HOST effectue désormais un véritable échange de clés et demande un mot de passe. Chaque tentative est capturée. Vérifiez que l'empreinte du service tient sous la détection de version :
nmap -sV -p 22,3389,445 DECOY_HOST
Arrêtez avec :
make down
Les services sont définis dans broker/config.yaml. Chaque entrée est indépendamment activable et remappable :
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
Pour ajouter un service, ajoutez une entrée ici, publiez le port dans docker-compose.yml, et ajoutez un conteneur leurre. Pour en désactiver un, définissez enabled: false (et éventuellement supprimez son port publié).
Notez que le port hôte 22 est généralement occupé par le vrai démon SSH de l'hôte. Pour un laboratoire, vous pouvez remapper le côté publié dans docker-compose.yml, par exemple "2022:22", et pointer votre scanner vers celui-ci.
Les trois leurres exécutent OpenCanary (Thinkst), configurés pour que chaque conteneur active exactement un module. Les journaux sont émis au format JSON sur stdout, donc docker compose logs et tout expéditeur SIEM fonctionnent sans plomberie supplémentaire.
OpenCanary étiquette chaque événement avec un logtype numérique. Ceux que vous verrez ici :
Un identifiant SSH capturé ressemble à ceci :
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
C'est une conséquence directe de l'architecture du broker, et c'est la chose la plus importante à comprendre pour lire ces journaux.
Le broker termine la connexion TCP de l'attaquant et en ouvre une nouvelle vers le leurre. Donc du point de vue d'OpenCanary, le client est le broker. Chaque src_host dans un événement leurre sera l'adresse du broker sur decoynet, pas la source réelle.
La véritable adresse IP source est toujours capturée, mais à un endroit différent :
Donc l'attribution nécessite de corréler les journaux du broker avec ceux du leurre, en joignant sur l'horodatage et le service. Le broker enregistre remote (la véritable adresse de l'attaquant) et backend pour chaque session, ce qui rend la jointure possible :
docker compose logs broker | grep 'session opened' # who
docker compose logs ssh-decoy | grep '"logtype": 4002' # what they tried
Si vous avez besoin de la véritable IP à l'intérieur du leurre lui-même, les options sont d'envoyer le protocole PROXY (les leurres ne le parsent pas, ce qui signifierait les patcher), ou de remplacer le proxy en espace utilisateur par une redirection transparente (TPROXY ou eBPF bpf_sk_assign) qui préserve l'adresse source d'origine. Les deux sont listés dans la feuille de route. En attendant, traitez le broker comme la source de vérité pour « qui » et le leurre comme la source de vérité pour « quoi ».
Contrairement à SSH et RDP, ce leurre n'utilise pas OpenCanary. Le module smb d'OpenCanary n'est qu'un observateur de journaux : il suit un fichier et analyse les lignes smbd_audit émises par un vrai serveur Samba, ce qui signifiait exécuter Samba plus rsyslog plus opencanaryd sous supervisord, une chaîne de cinq maillons où n'importe quel maillon pouvait échouer silencieusement.
smb-decoy remplace tout cela par un seul processus Python basé sur SimpleSMBServer d'Impacket, une implémentation pure Python de SMB1/2/3. Il écoute sur 445, présente des partages appâts en lecture seule, répond à la négociation SMB2/3 (donc nmap -sV voit un vrai service), et journalise les connexions et les tentatives d'authentification NTLM sous forme d'un objet JSON par ligne sur stdout. Le nom d'utilisateur, le domaine et le poste de travail capturés du message d'authentification de l'attaquant sont le gain de capture d'identifiants.
Note de sécurité : Le smbserver d'Impacket comportait un path traversal critique, CVE-2021-31800, qui affectait spécifiquement les honeypots. Il a été corrigé dans la version 0.9.23. requirements.txt fixe une version actuelle et ne doit pas être rétrogradée en dessous. Le conteneur s'exécute également non-root, en lecture seule, avec toutes les capacités supprimées sauf NET_BIND_SERVICE.
Chaque leurre possède un opencanary.conf (installé dans /etc/opencanaryd/). Paramètres utiles :
ssh.version dans decoys/ssh/opencanary.conf. Actuellement, elle annonce SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1. Faites-la correspondre au système d'exploitation que vous prétendez être ; une bannière Ubuntu sur une machine se prétendant Windows est un indice.addShare(...) dans decoys/smb/smb_decoy.py, plus les fichiers appâts créés dans decoys/smb/Dockerfile. Les noms de partages et de fichiers sont l'appât.backend dans broker/config.yaml.Pour activer un autre module OpenCanary (ftp, telnet, mysql, vnc, redis, et d'autres sont disponibles), définissez <module>.enabled et <module>.port, ajoutez un conteneur leurre, et ajoutez un service correspondant dans broker/config.yaml.
ssh-decoy monte un volume nommé sur /var/lib/opencanary (ssh.key_path), de sorte que la clé d'hôte générée survive aux redémarrages. Sans cela, OpenCanary génère une nouvelle clé à chaque démarrage et l'empreinte changeante est un indice évident.
Le leurre SMB est maintenant un processus unique, donc le dépannage est simple.
docker compose logs -f smb-decoy
Chaque ligne est du JSON. Vous devriez voir un smb_decoy_start au démarrage, puis des événements smb_connect, smb_auth_attempt et smb_tree_connect lorsque les clients interagissent. Testez-le depuis l'hôte avec n'importe quel client SMB :
# macOS Finder: Go > Connect to Server
open 'smb://guest@localhost/HR-Payroll'
# or from Linux
smbclient -L //localhost -p 445 -N
Problèmes courants :
smb_decoy_start et le conteneur se ferme : vérifiez que requirements.txt s'est installé proprement. Impacket a besoin de Python 3.8+ ; l'image utilise 3.12.smb_auth_attempt : certains clients énumèrent les partages de manière anonyme sans jamais s'authentifier. Cela produit quand même smb_connect et smb_tree_connect. Forcez l'authentification en mappant un partage avec un nom d'utilisateur.src_host est l'adresse du broker, pas l'attaquant réel. Corrélez avec les journaux du broker sur l'horodatage.NET_ADMIN (et BPF / PERFMON sur les noyaux récents) pour charger et attacher le programme eBPF. Le fichier Compose demande ces capacités limitées. Si votre hôte ou version de Docker les rejette, le repli est privileged: true sur le service broker, ce qui est plus large et ne devrait être utilisé que lorsque les capacités limitées ne fonctionnent pas.internal sans route hôte. Gardez-le ainsi. Traitez chaque conteneur leurre comme potentiellement compromis.bpf_sk_assign, supprimant le besoin de corréler les journaux du broker et du leurre.MIT. Voir LICENSE.
| Container | Rôle | Réseau |
|---|
broker | Porte d'entrée publique : observation eBPF et proxy inverse | edge + decoynet |
ssh-decoy | Module ssh OpenCanary (vrai handshake, capture des identifiants) | decoynet uniquement |
rdp-decoy | Module rdp OpenCanary (imite NLA, capture les noms d'utilisateur) | decoynet uniquement |
smb-decoy | SimpleSMBServer d'Impacket (vrai SMB2/3, capture l'authentification) | decoynet uniquement |
| Container | Module OpenCanary | Écoute | Ce qu'il fait réellement |
|---|
ssh-decoy | ssh | 2222 | Véritable échange de clés SSH via twisted.conch. Capture chaque paire nom d'utilisateur/mot de passe. |
rdp-decoy | rdp | 3389 | Imite un serveur avec NLA, renvoie toujours un échec de connexion, extrait le nom d'utilisateur mstshash. |
smb-decoy | Impacket | 445 | Serveur SMB2/3 pur Python. Présente des partages appâts et journalise les connexions et les tentatives d'authentification NTLM au format JSON. |
| logtype | Constante dans opencanary/logger.py | Signification |
|---|
| 1000 | LOG_BASE_BOOT | Démarrage du démon |
| 4000 | LOG_SSH_NEW_CONNECTION | Connexion SSH ouverte |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | Le client a envoyé sa chaîne de version |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | Tentative de connexion SSH (inclut USERNAME et PASSWORD) |
| 5000 | LOG_SMB_FILE_OPEN | Fichier SMB ouvert (inclut USER, SHARENAME, FILENAME) |
| 14001 | LOG_RDP | Connexion RDP / tentative de connexion |
| Couche | Connaît la véritable IP source ? | Connaît ce qui a été tenté ? |
|---|
Classifieur eBPF (probe observed) | Oui | Non, métadonnées SYN uniquement |
Proxy broker (session opened) | Oui | Non, compteurs d'octets uniquement |
Leurre OpenCanary (logtype 4002) | Non | Oui, identifiants/fichiers |