Add per-service customization, stealth hardening, and BYOS support

- HTTP: configurable server_header, response_code, fake_app presets
  (apache/nginx/wordpress/phpmyadmin/iis), extra_headers, custom_body,
  static files directory mount
- SSH/Cowrie: configurable kernel_version, hardware_platform, ssh_banner,
  and users/passwords via COWRIE_USERDB_ENTRIES; switched to build mode
  so cowrie.cfg.j2 persona fields and userdb.txt generation work
- SMTP: configurable banner and MTA hostname
- MySQL: configurable version string in protocol greeting
- Redis: configurable redis_version and os string in INFO response
- BYOS: [custom-*] INI sections define bring-your-own Docker services
- Stealth: rename all *_honeypot.py → server.py; replace HONEYPOT_NAME
  env var with NODE_NAME across all 22+ service templates and plugins;
  strip "honeypot" from all in-container file content
- Config: DeckyConfig.service_config dict; INI [decky-N.svc] subsections;
  composer passes service_cfg to compose_fragment
- 350 tests passing (100%)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 04:08:27 -03:00
parent 07c06e3c0a
commit cf1e00af28
102 changed files with 974 additions and 309 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"mcp__plugin_context-mode_context-mode__ctx_batch_execute"
]
}
}

View File

@@ -46,3 +46,11 @@ DECNET is a honeypot/deception network framework. It deploys fake machines (call
- The logging/aggregation network must be isolated from the decoy network.
- A publicly accessible real server acts as the bridge between the two networks.
- Deckies should differ in exposed services and OS fingerprints to appear as a heterogeneous network.
## Development and testing
- For every new feature, pytests must me made.
- Pytest is the main testing framework in use.
- NEVER pass broken code to the user.
- Broken means: not running, not passing 100% tests, etc.
- After tests pass with 100%, always git commit your changes.

View File

@@ -168,6 +168,7 @@ def _build_deckies_from_ini(
base_image=distro.image,
build_base=distro.build_base,
hostname=hostname,
service_config=spec.service_config,
))
return deckies
@@ -217,6 +218,20 @@ def deploy(
f"[dim]Subnet:[/] {subnet_cidr} [dim]Gateway:[/] {effective_gateway} "
f"[dim]Host IP:[/] {host_ip}")
# Register bring-your-own services from INI before validation
if ini.custom_services:
from decnet.custom_service import CustomService
from decnet.services.registry import register_custom_service
for cs in ini.custom_services:
register_custom_service(
CustomService(
name=cs.name,
image=cs.image,
exec_cmd=cs.exec_cmd,
ports=cs.ports,
)
)
effective_log_target = log_target or ini.log_target
decky_configs = _build_deckies_from_ini(
ini, subnet_cidr, effective_gateway, host_ip, randomize_services

View File

@@ -46,7 +46,10 @@ def generate_compose(config: DecnetConfig) -> dict:
# --- Service containers: share base network namespace ---
for svc_name in decky.services:
svc = get_service(svc_name)
fragment = svc.compose_fragment(decky.name, log_target=config.log_target)
svc_cfg = decky.service_config.get(svc_name, {})
fragment = svc.compose_fragment(
decky.name, log_target=config.log_target, service_cfg=svc_cfg
)
# Inject the per-decky base image into build services so containers
# vary by distro and don't all fingerprint as debian:bookworm-slim.

View File

@@ -26,6 +26,7 @@ class DeckyConfig(BaseModel):
base_image: str # Docker image for the base/IP-holder container
build_base: str = "debian:bookworm-slim" # apt-compatible image for service Dockerfiles
hostname: str
service_config: dict[str, dict] = {} # optional per-service persona config
@field_validator("services")
@classmethod

41
decnet/custom_service.py Normal file
View File

@@ -0,0 +1,41 @@
"""
Bring-your-own-service (BYOS) support.
CustomService wraps a user-defined service from an INI [custom-*] section.
It is instantiated dynamically and registered via register_custom_service(),
not through the auto-discovery mechanism in the registry.
"""
from decnet.services.base import BaseService
class CustomService(BaseService):
"""A user-defined service that runs an arbitrary Docker image."""
def __init__(self, name: str, image: str, exec_cmd: str, ports: list[int] | None = None):
self.name = name
self.default_image = image
self.ports = ports or []
self._exec_cmd = exec_cmd
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
slug = self.name.replace("_", "-")
fragment: dict = {
"image": self.default_image,
"container_name": f"{decky_name}-{slug}",
"restart": "unless-stopped",
"environment": {"NODE_NAME": decky_name},
}
if self._exec_cmd:
fragment["command"] = self._exec_cmd.split()
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self):
return None

View File

@@ -12,11 +12,26 @@ Format:
ip=192.168.1.82 # optional
services=ssh,smb # optional; falls back to --randomize-services
[hostname-1.ssh] # optional per-service persona config
kernel_version=5.15.0-76-generic
users=root:toor,admin:admin123
[hostname-1.http]
server_header=nginx/1.18.0
fake_app=wordpress
[hostname-2]
services=ssh
[hostname-3]
ip=192.168.1.32
# Custom (bring-your-own) service definitions:
[custom-myservice]
binary=my-docker-image:latest
exec=/usr/bin/myservice -p 8080
ports=8080
"""
import configparser
@@ -29,6 +44,16 @@ class DeckySpec:
name: str
ip: str | None = None
services: list[str] | None = None
service_config: dict[str, dict] = field(default_factory=dict)
@dataclass
class CustomServiceSpec:
"""Spec for a user-defined (bring-your-own) service."""
name: str # service slug, e.g. "myservice" (section is "custom-myservice")
image: str # Docker image to use
exec_cmd: str # command to run inside the container
ports: list[int] = field(default_factory=list)
@dataclass
@@ -38,6 +63,7 @@ class IniConfig:
interface: str | None = None
log_target: str | None = None
deckies: list[DeckySpec] = field(default_factory=list)
custom_services: list[CustomServiceSpec] = field(default_factory=list)
def load_ini(path: str | Path) -> IniConfig:
@@ -56,13 +82,40 @@ def load_ini(path: str | Path) -> IniConfig:
cfg.interface = g.get("interface")
cfg.log_target = g.get("log_target") or g.get("log-target")
# First pass: collect decky sections and custom service definitions
for section in cp.sections():
if section == "general":
continue
if "." in section:
continue # subsections handled in second pass
if section.startswith("custom-"):
# Bring-your-own service definition
s = cp[section]
svc_name = section[len("custom-"):]
image = s.get("binary", "")
exec_cmd = s.get("exec", "")
ports_raw = s.get("ports", "")
ports = [int(p.strip()) for p in ports_raw.split(",") if p.strip().isdigit()]
cfg.custom_services.append(
CustomServiceSpec(name=svc_name, image=image, exec_cmd=exec_cmd, ports=ports)
)
continue
s = cp[section]
ip = s.get("ip")
svc_raw = s.get("services")
services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None
cfg.deckies.append(DeckySpec(name=section, ip=ip, services=services))
# Second pass: collect per-service subsections [decky-name.service]
decky_names = {d.name for d in cfg.deckies}
decky_map = {d.name: d for d in cfg.deckies}
for section in cp.sections():
if "." not in section:
continue
decky_name, _, svc_name = section.partition(".")
if decky_name not in decky_names:
continue # orphaned subsection — ignore
svc_cfg = {k: v for k, v in cp[section].items()}
decky_map[decky_name].service_config[svc_name] = svc_cfg
return cfg

View File

@@ -15,7 +15,12 @@ class BaseService(ABC):
default_image: str # Docker image tag, or "build" if a Dockerfile is needed
@abstractmethod
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
"""
Return the docker-compose service dict for this service on a given decky.
@@ -26,6 +31,7 @@ class BaseService(ABC):
Args:
decky_name: unique identifier for the decky (e.g. "decky-01")
log_target: "ip:port" string if log forwarding is enabled, else None
service_cfg: optional per-service persona config from INI subsection
"""
def dockerfile_context(self) -> Path | None:

View File

@@ -12,7 +12,7 @@ class ConpotService(BaseService):
ports = [502, 161, 80]
default_image = "honeynet/conpot"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
return {
"image": "honeynet/conpot",
"container_name": f"{decky_name}-conpot",

View File

@@ -9,12 +9,12 @@ class DockerAPIService(BaseService):
ports = [2375, 2376]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-docker-api",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -10,13 +10,13 @@ class ElasticsearchService(BaseService):
ports = [9200]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-elasticsearch",
"restart": "unless-stopped",
"environment": {
"HONEYPOT_NAME": decky_name,
"NODE_NAME": decky_name,
},
}
if log_target:

View File

@@ -9,13 +9,13 @@ class FTPService(BaseService):
ports = [21]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ftp",
"restart": "unless-stopped",
"environment": {
"HONEYPOT_NAME": decky_name,
"NODE_NAME": decky_name,
},
}
if log_target:

View File

@@ -1,3 +1,4 @@
import json
from pathlib import Path
from decnet.services.base import BaseService
@@ -9,17 +10,43 @@ class HTTPService(BaseService):
ports = [80, 443]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
cfg = service_cfg or {}
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-http",
"restart": "unless-stopped",
"environment": {
"HONEYPOT_NAME": decky_name,
"NODE_NAME": decky_name,
},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
# Optional persona overrides — only injected when explicitly set
if "server_header" in cfg:
fragment["environment"]["SERVER_HEADER"] = cfg["server_header"]
if "response_code" in cfg:
fragment["environment"]["RESPONSE_CODE"] = str(cfg["response_code"])
if "fake_app" in cfg:
fragment["environment"]["FAKE_APP"] = cfg["fake_app"]
if "extra_headers" in cfg:
val = cfg["extra_headers"]
fragment["environment"]["EXTRA_HEADERS"] = (
json.dumps(val) if isinstance(val, dict) else val
)
if "custom_body" in cfg:
fragment["environment"]["CUSTOM_BODY"] = cfg["custom_body"]
if "files" in cfg:
files_path = str(Path(cfg["files"]).resolve())
fragment["environment"]["FILES_DIR"] = "/opt/html_files"
fragment.setdefault("volumes", []).append(f"{files_path}:/opt/html_files:ro")
return fragment
def dockerfile_context(self) -> Path | None:

View File

@@ -9,12 +9,12 @@ class IMAPService(BaseService):
ports = [143, 993]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-imap",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class KubernetesAPIService(BaseService):
ports = [6443, 8080]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-k8s",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,13 +9,13 @@ class LDAPService(BaseService):
ports = [389, 636]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ldap",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -16,12 +16,12 @@ class LLMNRService(BaseService):
ports = [5355, 5353]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-llmnr",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class MongoDBService(BaseService):
ports = [27017]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mongodb",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class MQTTService(BaseService):
ports = [1883]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mqtt",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class MSSQLService(BaseService):
ports = [1433]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mssql",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,15 +9,23 @@ class MySQLService(BaseService):
ports = [3306]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
cfg = service_cfg or {}
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mysql",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
if "version" in cfg:
fragment["environment"]["MYSQL_VERSION"] = cfg["version"]
return fragment
def dockerfile_context(self) -> Path | None:

View File

@@ -9,12 +9,12 @@ class POP3Service(BaseService):
ports = [110, 995]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-pop3",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class PostgresService(BaseService):
ports = [5432]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-postgres",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,13 +9,13 @@ class RDPService(BaseService):
ports = [3389]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-rdp",
"restart": "unless-stopped",
"environment": {
"HONEYPOT_NAME": decky_name,
"NODE_NAME": decky_name,
},
}
if log_target:

View File

@@ -9,15 +9,25 @@ class RedisService(BaseService):
ports = [6379]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
cfg = service_cfg or {}
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-redis",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
if "version" in cfg:
fragment["environment"]["REDIS_VERSION"] = cfg["version"]
if "os_string" in cfg:
fragment["environment"]["REDIS_OS"] = cfg["os_string"]
return fragment
def dockerfile_context(self) -> Path | None:

View File

@@ -31,6 +31,12 @@ def _load_plugins() -> None:
_loaded = True
def register_custom_service(instance: BaseService) -> None:
"""Register a dynamically created service (e.g. BYOS from INI)."""
_load_plugins()
_registry[instance.name] = instance
def get_service(name: str) -> BaseService:
_load_plugins()
if name not in _registry:

View File

@@ -9,12 +9,12 @@ class SIPService(BaseService):
ports = [5060]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-sip",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,14 +9,14 @@ class SMBService(BaseService):
ports = [445, 139]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-smb",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": {
"HONEYPOT_NAME": decky_name,
"NODE_NAME": decky_name,
},
}
if log_target:

View File

@@ -10,18 +10,28 @@ class SMTPService(BaseService):
ports = [25, 587]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
cfg = service_cfg or {}
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-smtp",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": {
"HONEYPOT_NAME": decky_name,
"NODE_NAME": decky_name,
},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
if "banner" in cfg:
fragment["environment"]["SMTP_BANNER"] = cfg["banner"]
if "mta" in cfg:
fragment["environment"]["SMTP_MTA"] = cfg["mta"]
return fragment
def dockerfile_context(self) -> Path:

View File

@@ -9,12 +9,12 @@ class SNMPService(BaseService):
ports = [161]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-snmp",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -1,15 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "cowrie"
class SSHService(BaseService):
name = "ssh"
ports = [22, 2222]
default_image = "cowrie/cowrie"
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(
self,
decky_name: str,
log_target: str | None = None,
service_cfg: dict | None = None,
) -> dict:
cfg = service_cfg or {}
env: dict = {
# Override [honeypot] and [ssh] listen_endpoints to also bind port 22
"COWRIE_HONEYPOT_HOSTNAME": decky_name,
"NODE_NAME": decky_name,
"COWRIE_HOSTNAME": decky_name,
"COWRIE_HONEYPOT_LISTEN_ENDPOINTS": "tcp:22:interface=0.0.0.0 tcp:2222:interface=0.0.0.0",
"COWRIE_SSH_LISTEN_ENDPOINTS": "tcp:22:interface=0.0.0.0 tcp:2222:interface=0.0.0.0",
}
@@ -18,13 +27,26 @@ class SSHService(BaseService):
env["COWRIE_OUTPUT_TCP_ENABLED"] = "true"
env["COWRIE_OUTPUT_TCP_HOST"] = host
env["COWRIE_OUTPUT_TCP_PORT"] = port
# Optional persona overrides
if "kernel_version" in cfg:
env["COWRIE_HONEYPOT_KERNEL_VERSION"] = cfg["kernel_version"]
if "kernel_build_string" in cfg:
env["COWRIE_HONEYPOT_KERNEL_BUILD_STRING"] = cfg["kernel_build_string"]
if "hardware_platform" in cfg:
env["COWRIE_HONEYPOT_HARDWARE_PLATFORM"] = cfg["hardware_platform"]
if "ssh_banner" in cfg:
env["COWRIE_SSH_VERSION"] = cfg["ssh_banner"]
if "users" in cfg:
env["COWRIE_USERDB_ENTRIES"] = cfg["users"]
return {
"image": "cowrie/cowrie",
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ssh",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": env,
}
def dockerfile_context(self):
return None
def dockerfile_context(self) -> Path:
return TEMPLATES_DIR

View File

@@ -6,7 +6,7 @@ class TelnetService(BaseService):
ports = [23]
default_image = "cowrie/cowrie"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
env: dict = {
"COWRIE_HONEYPOT_HOSTNAME": decky_name,
"COWRIE_TELNET_ENABLED": "true",

View File

@@ -9,12 +9,12 @@ class TFTPService(BaseService):
ports = [69]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-tftp",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class VNCService(BaseService):
ports = [5900]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-vnc",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
"environment": {"NODE_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -1,10 +1,14 @@
[honeypot]
hostname = {{ COWRIE_HOSTNAME | default('svr01') }}
listen_endpoints = tcp:2222:interface=0.0.0.0
kernel_version = {{ COWRIE_HONEYPOT_KERNEL_VERSION | default('5.15.0-76-generic') }}
kernel_build_string = {{ COWRIE_HONEYPOT_KERNEL_BUILD_STRING | default('#83-Ubuntu SMP Thu Jun 15 19:16:32 UTC 2023') }}
hardware_platform = {{ COWRIE_HONEYPOT_HARDWARE_PLATFORM | default('x86_64') }}
[ssh]
enabled = true
listen_endpoints = tcp:2222:interface=0.0.0.0
version = {{ COWRIE_SSH_VERSION | default('SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5') }}
{% if COWRIE_LOG_HOST is defined and COWRIE_LOG_HOST %}
[output_jsonlog]

View File

@@ -1,7 +1,7 @@
#!/bin/bash
set -e
# Render Jinja2 template using the venv's python (has jinja2)
# Render Jinja2 config template
/home/cowrie/cowrie-env/bin/python3 - <<'EOF'
import os
from jinja2 import Template
@@ -15,4 +15,19 @@ with open("/home/cowrie/cowrie-env/etc/cowrie.cfg", "w") as f:
f.write(rendered)
EOF
# Write userdb.txt if custom users were provided
# Format: COWRIE_USERDB_ENTRIES=root:toor,admin:admin123
if [ -n "${COWRIE_USERDB_ENTRIES}" ]; then
USERDB="/home/cowrie/cowrie-env/etc/userdb.txt"
: > "$USERDB"
IFS=',' read -ra PAIRS <<< "${COWRIE_USERDB_ENTRIES}"
for pair in "${PAIRS[@]}"; do
user="${pair%%:*}"
pass="${pair#*:}"
uid=1000
[ "$user" = "root" ] && uid=0
echo "${user}:${uid}:${pass}" >> "$USERDB"
done
fi
exec authbind --deep /home/cowrie/cowrie-env/bin/twistd -n --pidfile= cowrie

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir flask
COPY docker_api_honeypot.py /opt/docker_api_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Docker API honeypot.
Docker APIserver.
Serves a fake Docker REST API on port 2375. Responds to common recon
endpoints (/version, /info, /containers/json, /images/json) with plausible
but fake data. Logs all requests as JSON.
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
from flask import Flask, request
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "docker-host")
NODE_NAME = os.environ.get("NODE_NAME", "docker-host")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
app = Flask(__name__)
@@ -38,7 +38,7 @@ _INFO = {
"MemoryLimit": True,
"SwapLimit": True,
"KernelMemory": False,
"Name": HONEYPOT_NAME,
"Name": NODE_NAME,
"DockerRootDir": "/var/lib/docker",
"HttpProxy": "",
"HttpsProxy": "",
@@ -73,7 +73,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "docker_api",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -127,5 +127,5 @@ def catch_all(path):
if __name__ == "__main__":
_log("startup", msg=f"Docker API honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"Docker API server starting as {NODE_NAME}")
app.run(host="0.0.0.0", port=2375, debug=False)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY elasticsearch_honeypot.py /opt/elasticsearch_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Elasticsearch honeypot presents a convincing ES 7.x HTTP API on port 9200.
Elasticsearch server presents a convincing ES 7.x HTTP API on port 9200.
Logs all requests (especially recon probes like /_cat/, /_cluster/, /_nodes/)
as JSON. Designed to attract automated scanners and credential stuffers.
"""
@@ -11,14 +11,14 @@ import socket
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "esserver")
NODE_NAME = os.environ.get("NODE_NAME", "esserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
_CLUSTER_UUID = "xC3Pr9abTq2mNkOeLvXwYA"
_NODE_UUID = "dJH7Lm2sRqWvPn0kFiEtBo"
_ROOT_RESPONSE = {
"name": HONEYPOT_NAME,
"name": NODE_NAME,
"cluster_name": "elasticsearch",
"cluster_uuid": _CLUSTER_UUID,
"version": {
@@ -51,7 +51,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "elasticsearch",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -110,7 +110,7 @@ class ESHandler(BaseHTTPRequestHandler):
if "_search" in path or "_bulk" in path:
self._send_json(200, {"took": 1, "timed_out": False, "hits": {"total": {"value": 0}, "hits": []}})
else:
self._send_json(200, {"result": "created", "_id": "1", "_index": "honeypot"})
self._send_json(200, {"result": "created", "_id": "1", "_index": "server"})
def do_PUT(self):
src = self.client_address[0]
@@ -133,6 +133,6 @@ class ESHandler(BaseHTTPRequestHandler):
if __name__ == "__main__":
_log("startup", msg=f"Elasticsearch honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"Elasticsearch server starting as {NODE_NAME}")
server = HTTPServer(("0.0.0.0", 9200), ESHandler)
server.serve_forever()

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir twisted jinja2
COPY ftp_honeypot.py /opt/ftp_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
FTP honeypot using Twisted's FTP server infrastructure.
FTP server using Twisted's FTP server infrastructure.
Accepts any credentials, logs all commands and file requests,
forwards events as JSON to LOG_TARGET if set.
"""
@@ -15,7 +15,7 @@ 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")
NODE_NAME = os.environ.get("NODE_NAME", "ftpserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "ftp",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -42,22 +42,22 @@ def _log(event_type: str, **kwargs) -> None:
_forward(event)
class HoneypotFTP(FTP):
class ServerFTP(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
self._server_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
_log("auth_attempt", username=getattr(self, "_server_user", "?"), password=password)
# Accept everything — we're a server
self.state = self.AUTHED
self._user = getattr(self, "_honeypot_user", "anonymous")
self._user = getattr(self, "_server_user", "anonymous")
return defer.succeed((230, "Login successful."))
def ftp_RETR(self, path):
@@ -71,12 +71,12 @@ class HoneypotFTP(FTP):
super().connectionLost(reason)
class HoneypotFTPFactory(FTPFactory):
protocol = HoneypotFTP
class ServerFTPFactory(FTPFactory):
protocol = ServerFTP
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())
_log("startup", msg=f"FTP server starting as {NODE_NAME} on port 21")
reactor.listenTCP(21, ServerFTPFactory())
reactor.run()

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir flask jinja2
COPY http_honeypot.py /opt/http_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,69 +0,0 @@
#!/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)

118
templates/http/server.py Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
HTTP service emulator using Flask.
Accepts all requests, logs every detail (method, path, headers, body),
and responds with configurable pages. Forwards events as JSON to LOG_TARGET if set.
"""
import json
import os
import socket
from datetime import datetime, timezone
from pathlib import Path
from flask import Flask, request, send_from_directory
NODE_NAME = os.environ.get("NODE_NAME", "webserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
SERVER_HEADER = os.environ.get("SERVER_HEADER", "Apache/2.4.54 (Debian)")
RESPONSE_CODE = int(os.environ.get("RESPONSE_CODE", "403"))
FAKE_APP = os.environ.get("FAKE_APP", "")
EXTRA_HEADERS = json.loads(os.environ.get("EXTRA_HEADERS", "{}"))
CUSTOM_BODY = os.environ.get("CUSTOM_BODY", "")
FILES_DIR = os.environ.get("FILES_DIR", "")
_FAKE_APP_BODIES: dict[str, str] = {
"apache_default": (
"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n"
"<html><head><title>Apache2 Debian Default Page</title></head>\n"
"<body><h1>Apache2 Debian Default Page</h1>\n"
"<p>It works!</p></body></html>"
),
"nginx_default": (
"<!DOCTYPE html><html><head><title>Welcome to nginx!</title></head>\n"
"<body><h1>Welcome to nginx!</h1>\n"
"<p>If you see this page, the nginx web server is successfully installed.</p>\n"
"</body></html>"
),
"wordpress": (
"<!DOCTYPE html><html><head><title>WordPress &rsaquo; Error</title></head>\n"
"<body id=\"error-page\"><div class=\"wp-die-message\">\n"
"<h1>Error establishing a database connection</h1></div></body></html>"
),
"phpmyadmin": (
"<!DOCTYPE html><html><head><title>phpMyAdmin</title></head>\n"
"<body><form method=\"post\" action=\"index.php\">\n"
"<input type=\"text\" name=\"pma_username\" />\n"
"<input type=\"password\" name=\"pma_password\" />\n"
"<input type=\"submit\" value=\"Go\" /></form></body></html>"
),
"iis_default": (
"<!DOCTYPE html><html><head><title>IIS Windows Server</title></head>\n"
"<body><h1>IIS Windows Server</h1>\n"
"<p>Welcome to Internet Information Services</p></body></html>"
),
}
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": NODE_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):
# Serve static files directory if configured
if FILES_DIR and path:
files_path = Path(FILES_DIR) / path
if files_path.is_file():
return send_from_directory(FILES_DIR, path)
# Select response body: custom > fake_app preset > default 403
if CUSTOM_BODY:
body = CUSTOM_BODY
elif FAKE_APP and FAKE_APP in _FAKE_APP_BODIES:
body = _FAKE_APP_BODIES[FAKE_APP]
else:
body = "<html><body><h1>403 Forbidden</h1></body></html>"
headers = {"Server": SERVER_HEADER, "Content-Type": "text/html", **EXTRA_HEADERS}
return body, RESPONSE_CODE, headers
if __name__ == "__main__":
_log("startup", msg=f"HTTP server starting as {NODE_NAME}")
app.run(host="0.0.0.0", port=80, debug=False)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY imap_honeypot.py /opt/imap_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
IMAP honeypot.
IMAPserver.
Presents an IMAP4rev1 banner, captures LOGIN credentials (plaintext and
AUTHENTICATE), then returns a NO response. Logs all commands as JSON.
"""
@@ -11,9 +11,9 @@ import os
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mailserver")
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
BANNER = f"* OK [{HONEYPOT_NAME}] IMAP4rev1 Service Ready\r\n"
BANNER = f"* OK [{NODE_NAME}] IMAP4rev1 Service Ready\r\n"
def _forward(event: dict) -> None:
@@ -31,7 +31,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "imap",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -87,7 +87,7 @@ class IMAPProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"IMAP honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"IMAP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(IMAPProtocol, "0.0.0.0", 143)
async with server:

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Kubernetes API honeypot.
Kubernetes APIserver.
Serves a fake K8s REST API on port 6443 (HTTPS-ish, plain HTTP) and 8080.
Responds to recon endpoints (/version, /api, /apis, /api/v1/namespaces,
/api/v1/pods) with plausible but fake data. Logs all requests as JSON.
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
from flask import Flask, request
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "k8s-master")
NODE_NAME = os.environ.get("NODE_NAME", "k8s-master")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
app = Flask(__name__)
@@ -33,7 +33,7 @@ _VERSION = {
_API_VERSIONS = {
"kind": "APIVersions",
"versions": ["v1"],
"serverAddressByClientCIDRs": [{"clientCIDR": "0.0.0.0/0", "serverAddress": f"{HONEYPOT_NAME}:6443"}],
"serverAddressByClientCIDRs": [{"clientCIDR": "0.0.0.0/0", "serverAddress": f"{NODE_NAME}:6443"}],
}
_NAMESPACES = {
@@ -80,7 +80,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "k8s",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -138,5 +138,5 @@ def catch_all(path):
if __name__ == "__main__":
_log("startup", msg=f"Kubernetes API honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"Kubernetes API server starting as {NODE_NAME}")
app.run(host="0.0.0.0", port=6443, debug=False)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY ldap_honeypot.py /opt/ldap_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
LDAP honeypot.
LDAPserver.
Parses BER-encoded BindRequest messages, logs DN and password, returns an
invalidCredentials error. Logs all interactions as JSON.
"""
@@ -11,7 +11,7 @@ import os
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "ldapserver")
NODE_NAME = os.environ.get("NODE_NAME", "ldapserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -30,7 +30,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "ldap",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -154,7 +154,7 @@ class LDAPProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"LDAP honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"LDAP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(LDAPProtocol, "0.0.0.0", 389)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY llmnr_honeypot.py /opt/llmnr_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -13,7 +13,7 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "lan-host")
NODE_NAME = os.environ.get("NODE_NAME", "lan-host")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -32,7 +32,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "llmnr",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -104,7 +104,7 @@ class LLMNRProtocol(asyncio.DatagramProtocol):
async def main():
_log("startup", msg=f"LLMNR/mDNS honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"LLMNR/mDNS server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
# LLMNR: UDP 5355

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY mongodb_honeypot.py /opt/mongodb_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
MongoDB honeypot.
MongoDBserver.
Implements the MongoDB wire protocol OP_MSG/OP_QUERY handshake. Responds
to isMaster/hello, listDatabases, and authenticate commands. Logs all
received messages as JSON.
@@ -13,7 +13,7 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mongodb")
NODE_NAME = os.environ.get("NODE_NAME", "mongodb")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# Minimal BSON helpers
@@ -64,7 +64,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "mongodb",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -115,7 +115,7 @@ class MongoDBProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"MongoDB honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"MongoDB server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(MongoDBProtocol, "0.0.0.0", 27017)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY mqtt_honeypot.py /opt/mqtt_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
MQTT honeypot (port 1883).
MQTT server (port 1883).
Parses MQTT CONNECT packets, extracts client_id, username, and password,
then returns CONNACK with return code 5 (not authorized). Logs all
interactions as JSON.
@@ -13,7 +13,7 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mqtt-broker")
NODE_NAME = os.environ.get("NODE_NAME", "mqtt-broker")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# CONNACK: packet type 0x20, remaining length 2, session_present=0, return_code=5
@@ -35,7 +35,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "mqtt",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -137,7 +137,7 @@ class MQTTProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"MQTT honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"MQTT server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(MQTTProtocol, "0.0.0.0", 1883)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY mssql_honeypot.py /opt/mssql_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
MSSQL (TDS) honeypot.
MSSQL (TDS)server.
Reads TDS pre-login and login7 packets, extracts username, responds with
a login failed error. Logs auth attempts as JSON.
"""
@@ -12,7 +12,7 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "dbserver")
NODE_NAME = os.environ.get("NODE_NAME", "dbserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# Minimal TDS pre-login response
@@ -54,7 +54,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "mssql",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -137,7 +137,7 @@ class MSSQLProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"MSSQL honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"MSSQL server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(MSSQLProtocol, "0.0.0.0", 1433)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY mysql_honeypot.py /opt/mysql_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
MySQL honeypot.
MySQLserver.
Sends a realistic MySQL 5.7 server handshake, reads the client login
packet, extracts username, then closes with Access Denied. Logs auth
attempts as JSON.
@@ -13,24 +13,25 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "dbserver")
NODE_NAME = os.environ.get("NODE_NAME", "dbserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
_MYSQL_VER = os.environ.get("MYSQL_VERSION", "5.7.38-log")
# Minimal MySQL 5.7 server greeting (protocol v10)
# Minimal MySQL server greeting (protocol v10) — version string is configurable
_GREETING = (
b"\x0a" # protocol version 10
b"5.7.38-honeypot\x00" # server version + NUL
b"\x01\x00\x00\x00" # connection id = 1
b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1
b"\x00" # filler
b"\xff\xf7" # capability flags low
b"\x21" # charset utf8
b"\x02\x00" # status flags
b"\xff\x81" # capability flags high
b"\x15" # auth plugin data length
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserved (10 bytes)
b"\x21\x4f\x7d\x25\x3e\x55\x4d\x7c\x67\x75\x5e\x31\x00" # auth part 2
b"mysql_native_password\x00" # auth plugin name
+ _MYSQL_VER.encode() + b"\x00" # server version + NUL
+ b"\x01\x00\x00\x00" # connection id = 1
+ b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1
+ b"\x00" # filler
+ b"\xff\xf7" # capability flags low
+ b"\x21" # charset utf8
+ b"\x02\x00" # status flags
+ b"\xff\x81" # capability flags high
+ b"\x15" # auth plugin data length
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserved (10 bytes)
+ b"\x21\x4f\x7d\x25\x3e\x55\x4d\x7c\x67\x75\x5e\x31\x00" # auth part 2
+ b"mysql_native_password\x00" # auth plugin name
)
@@ -54,7 +55,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "mysql",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -110,7 +111,7 @@ class MySQLProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"MySQL honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"MySQL server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(MySQLProtocol, "0.0.0.0", 3306)
async with server:

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
POP3 honeypot.
POP3server.
Presents a convincing POP3 banner, collects USER/PASS credentials, then
stalls with a generic error. Logs every interaction as JSON and forwards
to LOG_TARGET if set.
@@ -12,9 +12,9 @@ import os
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mailserver")
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
BANNER = f"+OK {HONEYPOT_NAME} POP3 server ready\r\n"
BANNER = f"+OK {NODE_NAME} POP3 server ready\r\n"
def _forward(event: dict) -> None:
@@ -32,7 +32,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "pop3",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -83,7 +83,7 @@ class POP3Protocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"POP3 honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"POP3 server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(POP3Protocol, "0.0.0.0", 110)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY postgres_honeypot.py /opt/postgres_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
PostgreSQL honeypot.
PostgreSQLserver.
Reads the startup message, extracts username and database, responds with
an AuthenticationMD5Password challenge, logs the hash sent back, then
returns an error. Logs all interactions as JSON.
@@ -13,7 +13,7 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "pgserver")
NODE_NAME = os.environ.get("NODE_NAME", "pgserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
SALT = b"\xde\xad\xbe\xef"
@@ -40,7 +40,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "postgres",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -118,7 +118,7 @@ class PostgresProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"PostgreSQL honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"PostgreSQL server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(PostgresProtocol, "0.0.0.0", 5432)
async with server:

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir twisted jinja2
COPY rdp_honeypot.py /opt/rdp_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Minimal RDP honeypot using Twisted.
Minimal RDP server 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.
@@ -15,7 +15,7 @@ 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")
NODE_NAME = os.environ.get("NODE_NAME", "WORKSTATION")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "rdp",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -42,7 +42,7 @@ def _log(event_type: str, **kwargs) -> None:
_forward(event)
class RDPHoneypotProtocol(protocol.Protocol):
class RDPServerProtocol(protocol.Protocol):
def connectionMade(self):
peer = self.transport.getPeer()
_log("connection", src_ip=peer.host, src_port=peer.port)
@@ -61,12 +61,12 @@ class RDPHoneypotProtocol(protocol.Protocol):
_log("disconnect", src_ip=peer.host, src_port=peer.port)
class RDPHoneypotFactory(protocol.ServerFactory):
protocol = RDPHoneypotProtocol
class RDPServerFactory(protocol.ServerFactory):
protocol = RDPServerProtocol
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())
_log("startup", msg=f"RDP server starting as {NODE_NAME} on port 3389")
reactor.listenTCP(3389, RDPServerFactory())
reactor.run()

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY redis_honeypot.py /opt/redis_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Redis honeypot.
Redisserver.
Implements enough of the RESP protocol to respond to AUTH, INFO, CONFIG GET,
KEYS, and arbitrary commands. Logs every command and argument as JSON.
"""
@@ -11,19 +11,22 @@ import os
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "cache-server")
NODE_NAME = os.environ.get("NODE_NAME", "cache-server")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
_REDIS_VER = os.environ.get("REDIS_VERSION", "7.0.12")
_REDIS_OS = os.environ.get("REDIS_OS", "Linux 5.15.0")
_INFO = f"""# Server
redis_version:7.0.12
redis_mode:standalone
os:Linux 5.15.0
arch_bits:64
tcp_port:6379
uptime_in_seconds:864000
connected_clients:1
# Keyspace
""".encode()
_INFO = (
f"# Server\n"
f"redis_version:{_REDIS_VER}\n"
f"redis_mode:standalone\n"
f"os:{_REDIS_OS}\n"
f"arch_bits:64\n"
f"tcp_port:6379\n"
f"uptime_in_seconds:864000\n"
f"connected_clients:1\n"
f"# Keyspace\n"
).encode()
def _forward(event: dict) -> None:
@@ -41,7 +44,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "redis",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -158,7 +161,7 @@ class RedisProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"Redis honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"Redis server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(RedisProtocol, "0.0.0.0", 6379)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY sip_honeypot.py /opt/sip_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
SIP honeypot (UDP + TCP port 5060).
SIP server (UDP + TCP port 5060).
Parses SIP REGISTER and INVITE messages, logs credentials from the
Authorization header and call metadata, then responds with 401 Unauthorized.
"""
@@ -12,7 +12,7 @@ import re
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "pbx")
NODE_NAME = os.environ.get("NODE_NAME", "pbx")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
_401 = (
@@ -42,7 +42,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "sip",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -92,7 +92,7 @@ def _handle_message(data: bytes, src_addr) -> bytes | None:
to=headers.get("to", ""),
call_id=headers.get("call-id", ""),
cseq=headers.get("cseq", ""),
host=HONEYPOT_NAME,
host=NODE_NAME,
)
return response.encode()
return None
@@ -134,7 +134,7 @@ class SIPTCPProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"SIP honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"SIP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
udp_transport, _ = await loop.create_datagram_endpoint(
SIPUDPProtocol, local_addr=("0.0.0.0", 5060)

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir impacket jinja2
COPY smb_honeypot.py /opt/smb_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Minimal SMB honeypot using Impacket's SimpleSMBServer.
Minimal SMB server using Impacket's SimpleSMBServer.
Logs all connection attempts, optionally forwarding them as JSON to LOG_TARGET.
"""
@@ -11,7 +11,7 @@ from datetime import datetime, timezone
from impacket import smbserver
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "WORKSTATION")
NODE_NAME = os.environ.get("NODE_NAME", "WORKSTATION")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -30,7 +30,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "smb",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -39,7 +39,7 @@ def _log(event_type: str, **kwargs) -> None:
if __name__ == "__main__":
_log("startup", msg=f"SMB honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"SMB server starting as {NODE_NAME}")
os.makedirs("/tmp/smb_share", exist_ok=True)
server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=445)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY smtp_honeypot.py /opt/smtp_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
SMTP honeypot emulates a realistic ESMTP server (Postfix-style).
SMTP server emulates a realistic ESMTP server (Postfix-style).
Logs EHLO/AUTH/MAIL FROM/RCPT TO attempts as JSON, then denies auth.
"""
@@ -10,8 +10,10 @@ import os
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mailserver")
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
_SMTP_BANNER = os.environ.get("SMTP_BANNER", f"220 {NODE_NAME} ESMTP Postfix (Debian/GNU)")
_SMTP_MTA = os.environ.get("SMTP_MTA", NODE_NAME)
def _forward(event: dict) -> None:
@@ -29,7 +31,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "smtp",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -47,7 +49,7 @@ class SMTPProtocol(asyncio.Protocol):
self._transport = transport
self._peer = transport.get_extra_info("peername", ("?", 0))
_log("connect", src=self._peer[0], src_port=self._peer[1])
transport.write(f"220 {HONEYPOT_NAME} ESMTP Postfix (Debian/GNU)\r\n".encode())
transport.write(f"{_SMTP_BANNER}\r\n".encode())
def data_received(self, data):
self._buf += data
@@ -62,7 +64,7 @@ class SMTPProtocol(asyncio.Protocol):
domain = line.split(None, 1)[1] if " " in line else ""
_log("ehlo", src=self._peer[0], domain=domain)
self._transport.write(
f"250-{HONEYPOT_NAME}\r\n"
f"250-{_SMTP_MTA}\r\n"
f"250-PIPELINING\r\n"
f"250-SIZE 10240000\r\n"
f"250-VRFY\r\n"
@@ -106,7 +108,7 @@ class SMTPProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"SMTP honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"SMTP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(SMTPProtocol, "0.0.0.0", 25)
async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY snmp_honeypot.py /opt/snmp_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
SNMP honeypot (UDP 161).
SNMP server (UDP 161).
Parses SNMPv1/v2c GetRequest PDUs, logs the community string and OID list,
then responds with a GetResponse containing plausible system OID values.
Logs all requests as JSON.
@@ -13,16 +13,16 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "switch")
NODE_NAME = os.environ.get("NODE_NAME", "switch")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# OID value map — fake but plausible
_OID_VALUES = {
"1.3.6.1.2.1.1.1.0": f"Linux {HONEYPOT_NAME} 5.15.0-76-generic #83-Ubuntu SMP x86_64",
"1.3.6.1.2.1.1.1.0": f"Linux {NODE_NAME} 5.15.0-76-generic #83-Ubuntu SMP x86_64",
"1.3.6.1.2.1.1.2.0": "1.3.6.1.4.1.8072.3.2.10",
"1.3.6.1.2.1.1.3.0": "12345678", # sysUpTime
"1.3.6.1.2.1.1.4.0": "admin@localhost",
"1.3.6.1.2.1.1.5.0": HONEYPOT_NAME,
"1.3.6.1.2.1.1.5.0": NODE_NAME,
"1.3.6.1.2.1.1.6.0": "Server Room",
"1.3.6.1.2.1.1.7.0": "72",
}
@@ -43,7 +43,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "snmp",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -180,7 +180,7 @@ class SNMPProtocol(asyncio.DatagramProtocol):
async def main():
_log("startup", msg=f"SNMP honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"SNMP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint(
SNMPProtocol, local_addr=("0.0.0.0", 161)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY tftp_honeypot.py /opt/tftp_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
TFTP honeypot (UDP 69).
TFTP server (UDP 69).
Parses RRQ (read) and WRQ (write) requests, logs filename and transfer mode,
then responds with an error packet. Logs all requests as JSON.
"""
@@ -12,7 +12,7 @@ import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "tftpserver")
NODE_NAME = os.environ.get("NODE_NAME", "tftpserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# TFTP opcodes
@@ -40,7 +40,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "tftp",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -81,7 +81,7 @@ class TFTPProtocol(asyncio.DatagramProtocol):
async def main():
_log("startup", msg=f"TFTP honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"TFTP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint(
TFTPProtocol, local_addr=("0.0.0.0", 69)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY vnc_honeypot.py /opt/vnc_honeypot.py
COPY server.py /opt/server.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
VNC (RFB) honeypot.
VNC (RFB)server.
Performs the RFB 3.8 handshake, offers VNC authentication, captures the
24-byte DES-encrypted challenge response, then rejects with "Authentication
failed". Logs the raw response for offline cracking.
@@ -12,7 +12,7 @@ import os
import socket
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "desktop")
NODE_NAME = os.environ.get("NODE_NAME", "desktop")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# RFB challenge — fixed so captured responses are reproducible
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "vnc",
"host": HONEYPOT_NAME,
"host": NODE_NAME,
"event": event_type,
**kwargs,
}
@@ -100,7 +100,7 @@ class VNCProtocol(asyncio.Protocol):
async def main():
_log("startup", msg=f"VNC honeypot starting as {HONEYPOT_NAME}")
_log("startup", msg=f"VNC server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(VNCProtocol, "0.0.0.0", 5900)
async with server:

View File

@@ -20,13 +20,13 @@ APT_COMPATIBLE = {
}
BUILD_SERVICES = [
"http", "rdp", "smb", "ftp", "smtp", "elasticsearch",
"ssh", "http", "rdp", "smb", "ftp", "smtp", "elasticsearch",
"pop3", "imap", "mysql", "mssql", "redis", "mongodb", "postgres",
"ldap", "vnc", "docker_api", "k8s", "sip",
"mqtt", "llmnr", "snmp", "tftp",
]
UPSTREAM_SERVICES = ["ssh", "telnet", "conpot"]
UPSTREAM_SERVICES = ["telnet", "conpot"]
def _make_config(services, distro="debian", base_image=None, build_base=None):
@@ -95,6 +95,86 @@ def test_upstream_service_has_no_build_section(svc):
assert "image" in fragment
# ---------------------------------------------------------------------------
# service_config propagation tests
# ---------------------------------------------------------------------------
def test_service_config_http_server_header():
"""service_config for http must inject SERVER_HEADER into compose env."""
from decnet.config import DeckyConfig, DecnetConfig
from decnet.distros import DISTROS
profile = DISTROS["debian"]
decky = DeckyConfig(
name="decky-01", ip="10.0.0.10",
services=["http"], distro="debian",
base_image=profile.image, build_base=profile.build_base,
hostname="test-host",
service_config={"http": {"server_header": "nginx/1.18.0"}},
)
config = DecnetConfig(
mode="unihost", interface="eth0",
subnet="10.0.0.0/24", gateway="10.0.0.1",
deckies=[decky],
)
compose = generate_compose(config)
env = compose["services"]["decky-01-http"]["environment"]
assert env.get("SERVER_HEADER") == "nginx/1.18.0"
def test_service_config_ssh_kernel_version():
"""service_config for ssh must inject COWRIE_HONEYPOT_KERNEL_VERSION."""
from decnet.config import DeckyConfig, DecnetConfig
from decnet.distros import DISTROS
profile = DISTROS["debian"]
decky = DeckyConfig(
name="decky-01", ip="10.0.0.10",
services=["ssh"], distro="debian",
base_image=profile.image, build_base=profile.build_base,
hostname="test-host",
service_config={"ssh": {"kernel_version": "5.15.0-76-generic"}},
)
config = DecnetConfig(
mode="unihost", interface="eth0",
subnet="10.0.0.0/24", gateway="10.0.0.1",
deckies=[decky],
)
compose = generate_compose(config)
env = compose["services"]["decky-01-ssh"]["environment"]
assert env.get("COWRIE_HONEYPOT_KERNEL_VERSION") == "5.15.0-76-generic"
def test_service_config_for_one_service_does_not_affect_another():
"""service_config for http must not bleed into ftp fragment."""
from decnet.config import DeckyConfig, DecnetConfig
from decnet.distros import DISTROS
profile = DISTROS["debian"]
decky = DeckyConfig(
name="decky-01", ip="10.0.0.10",
services=["http", "ftp"], distro="debian",
base_image=profile.image, build_base=profile.build_base,
hostname="test-host",
service_config={"http": {"server_header": "nginx/1.18.0"}},
)
config = DecnetConfig(
mode="unihost", interface="eth0",
subnet="10.0.0.0/24", gateway="10.0.0.1",
deckies=[decky],
)
compose = generate_compose(config)
ftp_env = compose["services"]["decky-01-ftp"]["environment"]
assert "SERVER_HEADER" not in ftp_env
def test_no_service_config_produces_no_extra_env():
"""A decky with no service_config must not have new persona env vars."""
config = _make_config(["http", "mysql"])
compose = generate_compose(config)
for svc in ("http", "mysql"):
env = compose["services"][f"decky-01-{svc}"]["environment"]
assert "SERVER_HEADER" not in env
assert "MYSQL_VERSION" not in env
# ---------------------------------------------------------------------------
# Base container uses distro image, not build_base
# ---------------------------------------------------------------------------

Some files were not shown because too many files have changed in this diff Show More