fix(telnet): replace Cowrie with real busybox telnetd + rsyslog logging

Cowrie was exposing an SSH daemon on port 22 alongside the telnet service
even when COWRIE_SSH_ENABLED=false, contaminating deployments that did not
request an SSH service.

New implementation mirrors the SSH service pattern:
- busybox telnetd in foreground mode on port 23
- /bin/login for real PAM authentication (brute-force attempts logged)
- rsyslog RFC 5424 bridge piped to stdout for Docker log capture
- Configurable root password and hostname via env vars
- No Cowrie dependency
This commit is contained in:
2026-04-12 00:34:45 -04:00
parent c384a3103a
commit 65d585569b
68 changed files with 142 additions and 271 deletions

View File

@@ -1,31 +1,47 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "telnet"
class TelnetService(BaseService):
"""
Real telnetd using busybox telnetd + rsyslog logging pipeline.
Replaced Cowrie emulation (which also started an SSH daemon on port 22)
with a real busybox telnetd so only port 23 is exposed and auth events
are logged as RFC 5424 via the same rsyslog bridge used by the SSH service.
service_cfg keys:
password Root password (default: "admin")
hostname Override container hostname
"""
name = "telnet"
ports = [23]
default_image = "cowrie/cowrie"
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | 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 = {
"COWRIE_HONEYPOT_HOSTNAME": decky_name,
"COWRIE_TELNET_ENABLED": "true",
"COWRIE_TELNET_LISTEN_ENDPOINTS": "tcp:23:interface=0.0.0.0",
# Disable SSH so this container is telnet-only
"COWRIE_SSH_ENABLED": "false",
"TELNET_ROOT_PASSWORD": cfg.get("password", "admin"),
}
if log_target:
host, port = log_target.rsplit(":", 1)
env["COWRIE_OUTPUT_TCP_ENABLED"] = "true"
env["COWRIE_OUTPUT_TCP_HOST"] = host
env["COWRIE_OUTPUT_TCP_PORT"] = port
if "hostname" in cfg:
env["TELNET_HOSTNAME"] = cfg["hostname"]
return {
"image": "cowrie/cowrie",
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-telnet",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": env,
}
def dockerfile_context(self):
return None
def dockerfile_context(self) -> Path:
return TEMPLATES_DIR