refactor: strip DECNET tokens from container-visible surface

Rename the container-side logging module decnet_logging → syslog_bridge
(canonical at templates/syslog_bridge.py, synced into each template by
the deployer). Drop the stale per-template copies; setuptools find was
picking them up anyway. Swap useradd/USER/chown "decnet" for "logrelay"
so no obvious token appears in the rendered container image.

Apply the same cloaking pattern to the telnet template that SSH got:
syslog pipe moves to /run/systemd/journal/syslog-relay and the relay
is cat'd via exec -a "systemd-journal-fwd". rsyslog.d conf rename
99-decnet.conf → 50-journal-forward.conf. SSH capture script:
/var/decnet/captured → /var/lib/systemd/coredump (real systemd path),
logger tag decnet-capture → systemd-journal. Compose volume updated
to match the new in-container quarantine path.

SD element ID shifts decnet@55555 → relay@55555; synced across
collector, parser, sniffer, prober, formatter, tests, and docs so the
host-side pipeline still matches what containers emit.
This commit is contained in:
2026-04-17 22:57:53 -04:00
parent 69510fb880
commit 8dd4c78b33
114 changed files with 220 additions and 2712 deletions

View File

@@ -19,8 +19,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
jq \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/run/sshd /root/.ssh /var/log/decnet /var/decnet/captured \
&& chmod 700 /var/decnet /var/decnet/captured
RUN mkdir -p /var/run/sshd /root/.ssh /var/log/journal /var/lib/systemd/coredump \
&& chmod 700 /var/lib/systemd/coredump
# sshd_config: allow root + password auth; VERBOSE so session lines carry
# client IP + session PID (needed for file-capture attribution).
@@ -34,11 +34,11 @@ RUN sed -i \
# rsyslog: forward auth.* and user.* to named pipe in RFC 5424 format.
# The entrypoint relays the pipe to stdout for Docker log capture.
RUN printf '%s\n' \
'# DECNET log bridge — auth + user events → named pipe as RFC 5424' \
'# syslog-relay log bridge — auth + user events → named pipe as RFC 5424' \
'$template RFC5424fmt,"<%PRI%>1 %TIMESTAMP:::date-rfc3339% %HOSTNAME% %APP-NAME% %PROCID% %MSGID% %STRUCTURED-DATA% %msg%\n"' \
'auth,authpriv.* |/run/systemd/journal/syslog-relay;RFC5424fmt' \
'user.* |/run/systemd/journal/syslog-relay;RFC5424fmt' \
> /etc/rsyslog.d/99-decnet.conf
> /etc/rsyslog.d/50-journal-forward.conf
# Silence default catch-all rules so we own auth/user routing exclusively
RUN sed -i \

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# DECNET SSH honeypot file-catcher.
# SSH honeypot file-catcher.
#
# Watches attacker-writable paths with inotifywait. On close_write/moved_to,
# copies the file to the host-mounted quarantine dir, writes a .meta.json
@@ -13,7 +13,7 @@
set -u
CAPTURE_DIR="${CAPTURE_DIR:-/var/decnet/captured}"
CAPTURE_DIR="${CAPTURE_DIR:-/var/lib/systemd/coredump}"
CAPTURE_MAX_BYTES="${CAPTURE_MAX_BYTES:-52428800}" # 50 MiB
CAPTURE_WATCH_PATHS="${CAPTURE_WATCH_PATHS:-/root /tmp /var/tmp /home /var/www /opt /dev/shm}"
# Invoke inotifywait through a plausible-looking symlink so ps output doesn't
@@ -29,7 +29,7 @@ _is_ignored_path() {
local p="$1"
case "$p" in
"$CAPTURE_DIR"/*) return 0 ;;
/var/decnet/*) return 0 ;;
/var/lib/systemd/*) return 0 ;;
*/.bash_history) return 0 ;;
*/.viminfo) return 0 ;;
*/ssh_host_*_key*) return 0 ;;
@@ -116,7 +116,7 @@ _capture_one() {
size="$(stat -c '%s' "$src" 2>/dev/null)"
[ -z "$size" ] && return 0
if [ "$size" -gt "$CAPTURE_MAX_BYTES" ]; then
logger -p user.info -t decnet-capture "file_skipped size=$size path=$src reason=oversize"
logger -p user.info -t systemd-journal "file_skipped size=$size path=$src reason=oversize"
return 0
fi
@@ -242,7 +242,7 @@ _capture_one() {
ss_snapshot: $ss_snapshot
}' > "$CAPTURE_DIR/$stored_as.meta.json"
logger -p user.info -t decnet-capture \
logger -p user.info -t systemd-journal \
"file_captured orig_path=$src sha256=$sha size=$size stored_as=$stored_as src_ip=${src_ip:-unknown} ssh_user=${ssh_user:-unknown} attribution=$attribution"
}

View File

@@ -1,89 +0,0 @@
#!/usr/bin/env python3
"""
Shared RFC 5424 syslog helper for DECNET service templates.
Services call syslog_line() to format an RFC 5424 message, then
write_syslog_file() to emit it to stdout — Docker captures it, and the
host-side collector streams it into the log file.
RFC 5424 structure:
<PRI>1 TIMESTAMP HOSTNAME APP-NAME PROCID MSGID [SD-ELEMENT] MSG
Facility: local0 (16), PEN for SD element ID: decnet@55555
"""
from datetime import datetime, timezone
from typing import Any
# ─── Constants ────────────────────────────────────────────────────────────────
_FACILITY_LOCAL0 = 16
_SD_ID = "decnet@55555"
_NILVALUE = "-"
SEVERITY_EMERG = 0
SEVERITY_ALERT = 1
SEVERITY_CRIT = 2
SEVERITY_ERROR = 3
SEVERITY_WARNING = 4
SEVERITY_NOTICE = 5
SEVERITY_INFO = 6
SEVERITY_DEBUG = 7
_MAX_HOSTNAME = 255
_MAX_APPNAME = 48
_MAX_MSGID = 32
# ─── Formatter ────────────────────────────────────────────────────────────────
def _sd_escape(value: str) -> str:
"""Escape SD-PARAM-VALUE per RFC 5424 §6.3.3."""
return value.replace("\\", "\\\\").replace('"', '\\"').replace("]", "\\]")
def _sd_element(fields: dict[str, Any]) -> str:
if not fields:
return _NILVALUE
params = " ".join(f'{k}="{_sd_escape(str(v))}"' for k, v in fields.items())
return f"[{_SD_ID} {params}]"
def syslog_line(
service: str,
hostname: str,
event_type: str,
severity: int = SEVERITY_INFO,
timestamp: datetime | None = None,
msg: str | None = None,
**fields: Any,
) -> str:
"""
Return a single RFC 5424-compliant syslog line (no trailing newline).
Args:
service: APP-NAME (e.g. "http", "mysql")
hostname: HOSTNAME (decky node name)
event_type: MSGID (e.g. "request", "login_attempt")
severity: Syslog severity integer (default: INFO=6)
timestamp: UTC datetime; defaults to now
msg: Optional free-text MSG
**fields: Encoded as structured data params
"""
pri = f"<{_FACILITY_LOCAL0 * 8 + severity}>"
ts = (timestamp or datetime.now(timezone.utc)).isoformat()
host = (hostname or _NILVALUE)[:_MAX_HOSTNAME]
appname = (service or _NILVALUE)[:_MAX_APPNAME]
msgid = (event_type or _NILVALUE)[:_MAX_MSGID]
sd = _sd_element(fields)
message = f" {msg}" if msg else ""
return f"{pri}1 {ts} {host} {appname} {_NILVALUE} {msgid} {sd}{message}"
def write_syslog_file(line: str) -> None:
"""Emit a syslog line to stdout for Docker log capture."""
print(line, flush=True)
def forward_syslog(line: str, log_target: str) -> None:
"""No-op stub. TCP forwarding is now handled by rsyslog, not by service containers."""
pass

View File

@@ -39,13 +39,13 @@ mkfifo /run/systemd/journal/syslog-relay
bash -c 'exec -a "systemd-journal-fwd" cat /run/systemd/journal/syslog-relay' &
# Start rsyslog (reads /etc/rsyslog.d/99-decnet.conf, writes to the pipe above)
# Start rsyslog (reads /etc/rsyslog.d/50-journal-forward.conf, writes to the pipe above)
rsyslogd
# File-catcher: mirror attacker drops into host-mounted quarantine with attribution.
# Script lives at /usr/libexec/udev/journal-relay so `ps aux` shows a
# plausible udev helper. See Dockerfile for the rename rationale.
CAPTURE_DIR=/var/decnet/captured /usr/libexec/udev/journal-relay &
CAPTURE_DIR=/var/lib/systemd/coredump /usr/libexec/udev/journal-relay &
# sshd logs via syslog — no -e flag, so auth events flow through rsyslog → pipe → stdout
exec /usr/sbin/sshd -D