Initial commit: DECNET honeypot/deception network framework

Core CLI, service plugins (SSH/SMB/FTP/HTTP/RDP), Docker Compose
orchestration, MACVLAN networking, and Logstash log forwarding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 18:56:25 -03:00
commit 3e98c71ca4
37 changed files with 1822 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
libssl-dev libffi-dev \
git authbind \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash cowrie
WORKDIR /home/cowrie
RUN python3 -m venv cowrie-env \
&& cowrie-env/bin/pip install --no-cache-dir cowrie jinja2
# Authbind to bind port 22 as non-root
RUN touch /etc/authbind/byport/22 /etc/authbind/byport/2222 \
&& chmod 500 /etc/authbind/byport/22 /etc/authbind/byport/2222 \
&& chown cowrie /etc/authbind/byport/22 /etc/authbind/byport/2222
RUN mkdir -p /home/cowrie/cowrie-env/etc \
/home/cowrie/cowrie-env/var/log/cowrie \
/home/cowrie/cowrie-env/var/run \
&& chown -R cowrie /home/cowrie/cowrie-env/etc \
/home/cowrie/cowrie-env/var
COPY cowrie.cfg.j2 /home/cowrie/cowrie.cfg.j2
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER cowrie
EXPOSE 22 2222
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,26 @@
[honeypot]
hostname = {{ COWRIE_HOSTNAME | default('svr01') }}
listen_endpoints = tcp:2222:interface=0.0.0.0
[ssh]
enabled = true
listen_endpoints = tcp:2222:interface=0.0.0.0
{% if COWRIE_LOG_HOST is defined and COWRIE_LOG_HOST %}
[output_jsonlog]
enabled = true
logfile = cowrie.json
[output_localsocket]
enabled = false
# Forward JSON events to SIEM/aggregator
[output_tcp]
enabled = true
host = {{ COWRIE_LOG_HOST }}
port = {{ COWRIE_LOG_PORT | default('5140') }}
{% else %}
[output_jsonlog]
enabled = true
logfile = cowrie.json
{% endif %}

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Render Jinja2 template using the venv's python (has jinja2)
/home/cowrie/cowrie-env/bin/python3 - <<'EOF'
import os
from jinja2 import Template
with open("/home/cowrie/cowrie.cfg.j2") as f:
tpl = Template(f.read())
rendered = tpl.render(**os.environ)
with open("/home/cowrie/cowrie-env/etc/cowrie.cfg", "w") as f:
f.write(rendered)
EOF
exec authbind --deep /home/cowrie/cowrie-env/bin/twistd -n --pidfile= cowrie

14
templates/ftp/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir --break-system-packages twisted jinja2
COPY ftp_honeypot.py /opt/ftp_honeypot.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 21
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
set -e
exec python3 /opt/ftp_honeypot.py

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
FTP honeypot using Twisted's FTP server infrastructure.
Accepts any credentials, logs all commands and file requests,
forwards events as JSON to LOG_TARGET if set.
"""
import json
import os
import socket
import sys
from datetime import datetime, timezone
from twisted.internet import defer, protocol, reactor
from twisted.protocols.ftp import FTP, FTPFactory
from twisted.python import log as twisted_log
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "ftpserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
def _forward(event: dict) -> None:
if not LOG_TARGET:
return
try:
host, port = LOG_TARGET.rsplit(":", 1)
with socket.create_connection((host, int(port)), timeout=3) as s:
s.sendall((json.dumps(event) + "\n").encode())
except Exception:
pass
def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "ftp",
"host": HONEYPOT_NAME,
"event": event_type,
**kwargs,
}
print(json.dumps(event), flush=True)
_forward(event)
class HoneypotFTP(FTP):
def connectionMade(self):
peer = self.transport.getPeer()
_log("connection", src_ip=peer.host, src_port=peer.port)
super().connectionMade()
def ftp_USER(self, username):
self._honeypot_user = username
_log("user", username=username)
return super().ftp_USER(username)
def ftp_PASS(self, password):
_log("auth_attempt", username=getattr(self, "_honeypot_user", "?"), password=password)
# Accept everything — we're a honeypot
self.state = self.AUTHED
self._user = getattr(self, "_honeypot_user", "anonymous")
return defer.succeed((230, "Login successful."))
def ftp_RETR(self, path):
_log("download_attempt", path=path)
self.sendLine(b"550 File unavailable.")
return defer.succeed(None)
def connectionLost(self, reason):
peer = self.transport.getPeer()
_log("disconnect", src_ip=peer.host, src_port=peer.port)
super().connectionLost(reason)
class HoneypotFTPFactory(FTPFactory):
protocol = HoneypotFTP
if __name__ == "__main__":
twisted_log.startLogging(sys.stdout)
_log("startup", msg=f"FTP honeypot starting as {HONEYPOT_NAME} on port 21")
reactor.listenTCP(21, HoneypotFTPFactory())
reactor.run()

14
templates/http/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir --break-system-packages flask jinja2
COPY http_honeypot.py /opt/http_honeypot.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
set -e
exec python3 /opt/http_honeypot.py

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
HTTP honeypot using Flask.
Accepts all requests, logs every detail (method, path, headers, body),
and responds with convincing but empty pages. Forwards events as JSON
to LOG_TARGET if set.
"""
import json
import os
import socket
from datetime import datetime, timezone
from flask import Flask, request
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "webserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
app = Flask(__name__)
def _forward(event: dict) -> None:
if not LOG_TARGET:
return
try:
host, port = LOG_TARGET.rsplit(":", 1)
with socket.create_connection((host, int(port)), timeout=3) as s:
s.sendall((json.dumps(event) + "\n").encode())
except Exception:
pass
def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "http",
"host": HONEYPOT_NAME,
"event": event_type,
**kwargs,
}
print(json.dumps(event), flush=True)
_forward(event)
@app.before_request
def log_request():
_log(
"request",
method=request.method,
path=request.path,
remote_addr=request.remote_addr,
headers=dict(request.headers),
body=request.get_data(as_text=True)[:512],
)
@app.route("/", defaults={"path": ""})
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
def catch_all(path):
return (
"<html><body><h1>403 Forbidden</h1></body></html>",
403,
{"Server": "Apache/2.4.54 (Debian)", "Content-Type": "text/html"},
)
if __name__ == "__main__":
_log("startup", msg=f"HTTP honeypot starting as {HONEYPOT_NAME}")
app.run(host="0.0.0.0", port=80, debug=False)

14
templates/rdp/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir --break-system-packages twisted jinja2
COPY rdp_honeypot.py /opt/rdp_honeypot.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 3389
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
set -e
exec python3 /opt/rdp_honeypot.py

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Minimal RDP honeypot using Twisted.
Listens on port 3389, logs connection attempts and any credentials sent
in the initial RDP negotiation request. Forwards events as JSON to
LOG_TARGET if set.
"""
import json
import os
import socket
import sys
from datetime import datetime, timezone
from twisted.internet import protocol, reactor
from twisted.python import log as twisted_log
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "WORKSTATION")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
def _forward(event: dict) -> None:
if not LOG_TARGET:
return
try:
host, port = LOG_TARGET.rsplit(":", 1)
with socket.create_connection((host, int(port)), timeout=3) as s:
s.sendall((json.dumps(event) + "\n").encode())
except Exception:
pass
def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "rdp",
"host": HONEYPOT_NAME,
"event": event_type,
**kwargs,
}
print(json.dumps(event), flush=True)
_forward(event)
class RDPHoneypotProtocol(protocol.Protocol):
def connectionMade(self):
peer = self.transport.getPeer()
_log("connection", src_ip=peer.host, src_port=peer.port)
# Send a minimal RDP Connection Confirm PDU to keep clients talking
# X.224 Connection Confirm: length=0x0e, type=0xd0 (CC), dst=0, src=0, class=0
self.transport.write(b"\x03\x00\x00\x0b\x06\xd0\x00\x00\x00\x00\x00")
def dataReceived(self, data: bytes):
peer = self.transport.getPeer()
_log("data", src_ip=peer.host, src_port=peer.port, bytes=len(data), hex=data[:64].hex())
# Drop the connection after receiving data — we're just a logger
self.transport.loseConnection()
def connectionLost(self, reason):
peer = self.transport.getPeer()
_log("disconnect", src_ip=peer.host, src_port=peer.port)
class RDPHoneypotFactory(protocol.ServerFactory):
protocol = RDPHoneypotProtocol
if __name__ == "__main__":
twisted_log.startLogging(sys.stdout)
_log("startup", msg=f"RDP honeypot starting as {HONEYPOT_NAME} on port 3389")
reactor.listenTCP(3389, RDPHoneypotFactory())
reactor.run()

14
templates/smb/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir --break-system-packages impacket jinja2
COPY smb_honeypot.py /opt/smb_honeypot.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 445 139
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
mkdir -p /tmp/smb_share
exec python3 /opt/smb_honeypot.py

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
Minimal SMB honeypot using Impacket's SimpleSMBServer.
Logs all connection attempts, optionally forwarding them as JSON to LOG_TARGET.
"""
import json
import os
import socket
from datetime import datetime, timezone
from impacket import smbserver
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "WORKSTATION")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
def _forward(event: dict) -> None:
if not LOG_TARGET:
return
try:
host, port = LOG_TARGET.rsplit(":", 1)
with socket.create_connection((host, int(port)), timeout=3) as s:
s.sendall((json.dumps(event) + "\n").encode())
except Exception:
pass
def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "smb",
"host": HONEYPOT_NAME,
"event": event_type,
**kwargs,
}
print(json.dumps(event), flush=True)
_forward(event)
if __name__ == "__main__":
_log("startup", msg=f"SMB honeypot starting as {HONEYPOT_NAME}")
os.makedirs("/tmp/smb_share", exist_ok=True)
server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=445)
server.setSMB2Support(True)
server.setSMBChallenge("")
server.addShare("SHARE", "/tmp/smb_share", "Shared Documents")
try:
server.start()
except KeyboardInterrupt:
_log("shutdown")