
Broker de señuelo experimental
Un señuelo de red contenerizado (honeypot) que anuncia SSH, RDP y SMB, observa cada conexión entrante con eBPF y redirige mediante proxy inverso cada sesión a un contenedor señuelo aislado.
El diseño separa dos aspectos:
Esta es una herramienta defensiva para detectar y estudiar actividad no autorizada en redes que posees o estás autorizado a monitorear. Despliégalo solo donde tengas esa autoridad.
flowchart TB
A["Atacante / Escáner"]
subgraph host["Host Señuelo"]
direction TB
NIC["broker eth0<br/>publicados: 22, 3389, 445"]
subgraph brk["contenedor broker"]
direction TB
E["Clasificador eBPF TC<br/>registra cada SYN<br/>ve la IP fuente real"]
P["proxy inverso<br/>CONECTAR al backend"]
L["logs JSON estructurados"]
end
subgraph dec["decoynet (interna, sin ruta al host)"]
direction LR
S["ssh-decoy<br/>OpenCanary ssh<br/>puerto 2222"]
D["rdp-decoy<br/>OpenCanary rdp<br/>puerto 3389"]
M["smb-decoy<br/>Servidor SMB Impacket<br/>puerto 445"]
end
end
A --> NIC
NIC --> E
NIC --> P
E --> L
P --> S
P --> D
P --> M
Cuatro contenedores en total:
Los señuelos viven en una red Docker internal (decoynet) sin ruta al host ni al mundo exterior. Solo el broker puede alcanzarlos. Nada de lo que un atacante haga dentro de un señuelo puede alcanzar la red del host directamente.
El broker publica los puertos 22, 3389 y 445 al host, por lo que los paquetes entrantes llegan a la eth0 del broker. Dos cosas suceden entonces con cada paquete:
broker/bpf/decoy.bpf.c) analiza las cabeceras Ethernet, IP y TCP, y por cada nuevo intento de conexión (SYN establecido, ACK limpio) escribe un conn_event en un buffer anular: IP fuente y puerto, puerto destino, flags TCP, y si el puerto es un servicio anunciado. El paquete se pasa sin cambios (TC_ACT_OK).CONNECT al backend señuelo para ese servicio, luego retransmite bytes en ambos sentidos.El mapa eBPF advertised_ports se rellena al inicio desde config.yaml, por lo que el clasificador puede etiquetar si un sondeo alcanzó un puerto servido o uno no solicitado. Esto hace visibles los escaneos de puertos horizontales aunque solo se estén proxyando tres puertos.
Si deseas anunciar "todo está abierto" y canalizar puertos destino arbitrarios hacia el broker, extiende el clasificador para reescribir el puerto destino o usa una redirección TPROXY / bpf_sk_assign. La versión actual mantiene la ruta del paquete intacta y se limita a la observación, que es el valor predeterminado más seguro.
cyber-decoy/
├── README.md
├── docker-compose.yml # Pila de 4 contenedores
├── docker-compose.override.yml # Desarrollo local macOS: sin capacidades eBPF, reasignación del puerto 22
├── Makefile # Helpers build / up / down / bpf
├── LICENSE
├── scripts/
│ └── setup.sh # Comprobaciones previas del host
├── broker/
│ ├── Dockerfile # Compila objeto eBPF + binario Go
│ ├── config.yaml # Servicios anunciados (configurables)
│ ├── go.mod
│ ├── main.go # Punto de entrada
│ ├── bpf/
│ │ └── decoy.bpf.c # Clasificador eBPF TC
│ └── internal/
│ ├── config/config.go # Cargador de configuración
│ ├── proxy/proxy.go # Proxy inverso TCP
│ └── bpf/loader.go # Carga + adjunta eBPF, transmite eventos
└── decoys/ # Los tres ejecutan OpenCanary
├── ssh/
│ ├── Dockerfile
│ └── opencanary.conf # Módulo ssh, puerto 2222
├── rdp/
│ ├── Dockerfile
│ └── opencanary.conf # Módulo rdp, puerto 3389
└── smb/
├── Dockerfile # Proceso Python único, no root
├── smb_decoy.py # SimpleSMBServer de Impacket + registro JSON
└── requirements.txt # impacket (fijado)
docker-compose.override.yml incluido, que depende de las etiquetas !reset / !override).sudo mount -t bpf bpf /sys/fs/bpf.La imagen del broker detecta su arquitectura de compilación y pasa la macro __TARGET_ARCH_* correspondiente a clang, por lo que se compila tanto en x86_64 como en aarch64 (Apple Silicon, Graviton). Ten en cuenta que gcc-multilib deliberadamente no está instalado: es un paquete solo x86 sin candidato arm64, e incluirlo rompe la compilación en arm64 con el código de salida 100 de apt. Solo se necesitan clang y libbpf-dev para compilar el objeto eBPF.
Docker Desktop en macOS ejecuta contenedores dentro de una máquina virtual LinuxKit en lugar de en tu kernel host, por lo que el adjunto TC/TCX eBPF generalmente no funcionará allí. Esto no es fatal: eBPF es de mejor esfuerzo por diseño, por lo que el broker registra ebpf disabled: attach failed y el proxy inverso más los tres señuelos se ejecutan y registran normalmente. Puedes desarrollar y probar toda la ruta del proxy localmente, luego obtener observación eBPF real cuando despliegues en un host Linux.
docker-compose.override.yml se carga automáticamente y hace esto agradable: elimina las capacidades eBPF (inútiles en la VM) y reasigna el puerto host 22 a 2022, ya que el propio sshd del Mac posee el 22.
docker compose up --build # desarrollo local, override aplicado
docker compose -f docker-compose.yml up -d # despliegue real, override omitido
Ejecuta la comprobación previa primero:
./scripts/setup.sh
# 1. Construir las cuatro imágenes (compila el objeto eBPF dentro de la imagen del broker)
make build
# 2. Iniciar la pila
make up
# 3. Observar lo que sucede
make logs
Luego pruébalo desde otra máquina (o localhost para una prueba de humo):
ssh -p 22 user@DECOY_HOST # golpea el señuelo SSH
nc DECOY_HOST 3389 # golpea el señuelo RDP
nc DECOY_HOST 445 # golpea el señuelo SMB
nc DECOY_HOST 8080 # no anunciado: observado por eBPF, sin proxy
El broker emite JSON para eventos de sonda eBPF y sesiones proxyadas; cada señuelo emite eventos JSON de OpenCanary. Para ver las credenciales llegar:
docker compose logs -f ssh-decoy | grep 4002
A diferencia de un stub de solo banner, ssh -p 22 user@DECOY_HOST ahora completa un intercambio de claves real y solicita una contraseña. Cada intento es capturado. Verifica que la huella del servicio se mantenga bajo detección de versión:
nmap -sV -p 22,3389,445 DECOY_HOST
Detener con:
make down
Los servicios se definen en broker/config.yaml. Cada entrada es activable y reasignable de forma independiente:
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
Para agregar un servicio, añade una entrada aquí, publica el puerto en docker-compose.yml y añade un contenedor señuelo. Para deshabilitar uno, establece enabled: false (y opcionalmente elimina su puerto publicado).
Ten en cuenta que el puerto 22 del host suele estar ocupado por el demonio SSH real del host. Para un laboratorio puedes reasignar el lado publicado en docker-compose.yml, por ejemplo "2022:22", y apuntar tu escáner allí.
Los tres señuelos ejecutan OpenCanary (Thinkst), configurados para que cada contenedor habilite exactamente un módulo. Los registros se emiten como JSON en stdout, por lo que docker compose logs y cualquier transportador SIEM funcionan sin tuberías adicionales.
OpenCanary etiqueta cada evento con un logtype numérico. Los que verás aquí:
Una credencial SSH capturada se ve así:
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
Esta es una consecuencia directa de la arquitectura del broker, y es lo más importante de entender al leer estos registros.
El broker termina la conexión TCP del atacante y abre una nueva hacia el señuelo. Por lo tanto, desde el punto de vista de OpenCanary, el cliente es el broker. Cada src_host en un evento del señuelo será la dirección del broker en decoynet, no la fuente real.
La verdadera IP fuente aún se captura, solo que en un lugar diferente:
Por lo tanto, la atribución requiere correlacionar los registros del broker con los del señuelo, uniendo por marca de tiempo y servicio. El broker registra remote (la verdadera dirección del atacante) y backend para cada sesión, que es lo que hace posible la unión:
docker compose logs broker | grep 'session opened' # quién
docker compose logs ssh-decoy | grep '"logtype": 4002' # qué intentaron
Si necesitas la IP real dentro del señuelo mismo, las opciones son enviar el protocolo PROXY (los señuelos no lo analizan, por lo que esto implicaría parchearlos), o reemplazar el proxy de espacio de usuario con una redirección transparente (TPROXY o eBPF bpf_sk_assign) que preserve la dirección fuente original. Ambos se enumeran en la Hoja de ruta. Hasta entonces, trata al broker como la fuente de verdad para "quién" y al señuelo como la fuente de verdad para "qué".
A diferencia de SSH y RDP, este señuelo no usa OpenCanary. El módulo smb de OpenCanary es solo un observador de registros: sigue un archivo y analiza las líneas smbd_audit emitidas por un servidor Samba real, lo que significaba ejecutar Samba más rsyslog más opencanaryd bajo supervisord, una cadena de cinco eslabones donde cualquier eslabón podría fallar en silencio.
smb-decoy reemplaza todo eso con un único proceso Python basado en SimpleSMBServer de Impacket, una implementación pura en Python de SMB1/2/3. Enlaza el puerto 445, presenta recursos compartidos señuelo de solo lectura, responde a la negociación SMB2/3 (por lo que nmap -sV ve un servicio real) y registra conexiones e intentos de autenticación NTLM como un objeto JSON por línea en stdout. El nombre de usuario, dominio y estación de trabajo capturados del mensaje de autenticación del atacante son la recompensa de captura de credenciales.
Nota de seguridad: el smbserver de Impacket tenía un path traversal crítico, CVE-2021-31800, que afectaba específicamente a honeypots. Fue corregido en 0.9.23. requirements.txt fija una versión actual y no debe degradarse por debajo de esa. El contenedor también se ejecuta como no root, solo lectura, con todas las capacidades eliminadas excepto NET_BIND_SERVICE.
Cada señuelo posee un opencanary.conf (instalado en /etc/opencanaryd/). Ajustes útiles:
ssh.version en decoys/ssh/opencanary.conf. Actualmente afirma SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1. Haz que coincida con el SO que pretendes ser; un banner Ubuntu en una máquina que dice ser Windows es una pista.addShare(...) en decoys/smb/smb_decoy.py, más los archivos señuelo creados en decoys/smb/Dockerfile. Los nombres de recursos y archivos son el cebo.backend en broker/config.yaml.Para habilitar otro módulo OpenCanary (ftp, telnet, mysql, vnc, redis y otros están disponibles), establece <module>.enabled y <module>.port, añade un contenedor señuelo y añade un servicio coincidente en broker/config.yaml.
ssh-decoy monta un volumen nombrado en /var/lib/opencanary (ssh.key_path), por lo que la clave de host generada sobrevive a reinicios. Sin él, OpenCanary genera una clave nueva en cada inicio y la huella cambiante es una pista evidente.
El señuelo SMB ahora es un solo proceso, por lo que la solución de problemas es sencilla.
docker compose logs -f smb-decoy
Cada línea es JSON. Deberías ver un smb_decoy_start al arrancar, luego eventos smb_connect, smb_auth_attempt y smb_tree_connect a medida que los clientes interactúan. Pruébalo desde el host con cualquier cliente SMB:
# Finder de macOS: Ir > Conectar al servidor
open 'smb://guest@localhost/HR-Payroll'
# o desde Linux
smbclient -L //localhost -p 445 -N
Problemas comunes:
smb_decoy_start y el contenedor termina: verifica que requirements.txt se instaló limpiamente. Impacket necesita Python 3.8+; la imagen usa 3.12.smb_auth_attempt: algunos clientes enumeran recursos compartidos de forma anónima sin autenticarse nunca. Eso aún produce smb_connect y smb_tree_connect. Fuerza la autenticación montando un recurso compartido con un nombre de usuario.src_host es la dirección del broker, no la del atacante real. Correlaciona con los registros del broker por marca de tiempo.NET_ADMIN (y BPF / PERFMON en kernels recientes) para cargar y adjuntar el programa eBPF. El archivo Compose solicita estas capacidades con ámbito. Si tu host o versión de Docker las rechaza, la alternativa es privileged: true en el servicio del broker, que es más amplio y debe usarse solo cuando las capacidades con ámbito no funcionan.internal sin ruta al host. Mantenlo así. Trata cada contenedor señuelo como potencialmente comprometido.bpf_sk_assign, eliminando la necesidad de correlacionar registros del broker y del señuelo.MIT. Ver LICENSE.
| Contenedor | Rol | Red |
|---|
broker | Puerta frontal pública: observación eBPF más proxy inverso | edge + decoynet |
ssh-decoy | Módulo ssh de OpenCanary (apretón de manos real, captura credenciales) | solo decoynet |
rdp-decoy | Módulo rdp de OpenCanary (imitación NLA, captura nombres de usuario) | solo decoynet |
smb-decoy | SimpleSMBServer de Impacket (SMB2/3 real, captura autenticación) | solo decoynet |
| Contenedor | Módulo OpenCanary | Escucha | Qué hace realmente |
|---|
ssh-decoy | ssh | 2222 | Intercambio de claves SSH real mediante twisted.conch. Captura cada par usuario/contraseña. |
rdp-decoy | rdp | 3389 | Imita un servidor con NLA habilitado, siempre devuelve fallo de inicio de sesión, extrae el nombre de usuario mstshash. |
smb-decoy | Impacket | 445 | Servidor SMB2/3 puro en Python. Presenta recursos compartidos señuelo y registra conexiones e intentos de autenticación NTLM como JSON. |
| logtype | Constante en opencanary/logger.py | Significado |
|---|
| 1000 | LOG_BASE_BOOT | Inicio del daemon |
| 4000 | LOG_SSH_NEW_CONNECTION | Conexión SSH abierta |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | El cliente envió su cadena de versión |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | Intento de inicio de sesión SSH (incluye USERNAME y PASSWORD) |
| 5000 | LOG_SMB_FILE_OPEN | Archivo SMB abierto (incluye USER, SHARENAME, FILENAME) |
| 14001 | LOG_RDP | Conexión / intento de inicio de sesión RDP |
| Capa | ¿Conoce la IP fuente real? | ¿Sabe qué se intentó? |
|---|
Clasificador eBPF (probe observed) | Sí | No, solo metadatos SYN |
Proxy del broker (session opened) | Sí | No, solo recuentos de bytes |
Señuelo OpenCanary (logtype 4002) | No | Sí, credenciales/archivos |