feat: SSH log relay emits proper DECNET syslog for sshd events

New log_relay.py replaces raw 'cat' on the rsyslog pipe. Intercepts
sshd and bash lines and re-emits them as structured RFC 5424 events:
login_success, session_opened, disconnect, connection_closed, command.
Parsers updated to accept non-nil PROCID (sshd uses PID).
This commit is contained in:
2026-04-14 02:07:35 -04:00
parent a6c7cfdf66
commit 7ff5703250
8 changed files with 240 additions and 79 deletions

View File

@@ -65,6 +65,8 @@ RUN mkdir -p /root/projects /root/backups /var/www/html && \
printf 'DB_HOST=10.0.0.5\nDB_USER=admin\nDB_PASS=changeme123\nDB_NAME=prod_db\n' > /root/projects/.env && \
printf '[Unit]\nDescription=App Server\n[Service]\nExecStart=/usr/bin/python3 /opt/app/server.py\n' > /root/projects/app.service
COPY decnet_logging.py /opt/decnet_logging.py
COPY log_relay.py /opt/log_relay.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View File

@@ -34,8 +34,8 @@ fi
# Logging pipeline: named pipe → rsyslogd (RFC 5424) → stdout → Docker log capture
mkfifo /var/run/decnet-logs
# Relay pipe to stdout so Docker captures all syslog events
cat /var/run/decnet-logs &
# Relay pipe through Python log_relay — normalizes sshd/bash events to DECNET format
python3 /opt/log_relay.py &
# Start rsyslog (reads /etc/rsyslog.d/99-decnet.conf, writes to the pipe above)
rsyslogd

106
templates/ssh/log_relay.py Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
SSH log relay — reads rsyslog output from the named pipe and re-emits
matched sshd/bash events as proper DECNET RFC 5424 syslog lines to stdout.
Matched events:
- Accepted password (login_success)
- Connection closed (connection_closed)
- Disconnected from user (disconnect)
- Session opened (session_opened)
- bash CMD (command)
"""
import os
import re
import sys
from decnet_logging import syslog_line, write_syslog_file, SEVERITY_INFO, SEVERITY_WARNING
NODE_NAME = os.environ.get("NODE_NAME", "ssh-decky")
SERVICE = "ssh"
# sshd patterns
_ACCEPTED_RE = re.compile(
r"Accepted (\S+) for (\S+) from (\S+) port (\d+)"
)
_SESSION_RE = re.compile(
r"session opened for user (\S+?)(?:\(uid=\d+\))? by"
)
_DISCONNECTED_RE = re.compile(
r"Disconnected from user (\S+) (\S+) port (\d+)"
)
_CONN_CLOSED_RE = re.compile(
r"Connection closed by (\S+) port (\d+)"
)
# bash PROMPT_COMMAND pattern
_BASH_CMD_RE = re.compile(
r"CMD\s+uid=(\S+)\s+pwd=(\S+)\s+cmd=(.*)"
)
def _handle_line(line: str) -> None:
"""Parse a raw rsyslog line and emit a DECNET syslog line if it matches."""
# --- Accepted password ---
m = _ACCEPTED_RE.search(line)
if m:
method, user, src_ip, port = m.groups()
write_syslog_file(syslog_line(
SERVICE, NODE_NAME, "login_success", SEVERITY_WARNING,
src_ip=src_ip, username=user, auth_method=method, src_port=port,
))
return
# --- Session opened ---
m = _SESSION_RE.search(line)
if m:
user = m.group(1)
write_syslog_file(syslog_line(
SERVICE, NODE_NAME, "session_opened", SEVERITY_INFO,
username=user,
))
return
# --- Disconnected from user ---
m = _DISCONNECTED_RE.search(line)
if m:
user, src_ip, port = m.groups()
write_syslog_file(syslog_line(
SERVICE, NODE_NAME, "disconnect", SEVERITY_INFO,
src_ip=src_ip, username=user, src_port=port,
))
return
# --- Connection closed ---
m = _CONN_CLOSED_RE.search(line)
if m:
src_ip, port = m.groups()
write_syslog_file(syslog_line(
SERVICE, NODE_NAME, "connection_closed", SEVERITY_INFO,
src_ip=src_ip, src_port=port,
))
return
# --- bash CMD ---
m = _BASH_CMD_RE.search(line)
if m:
uid, pwd, cmd = m.groups()
write_syslog_file(syslog_line(
SERVICE, NODE_NAME, "command", SEVERITY_INFO,
uid=uid, pwd=pwd, command=cmd,
))
return
def main() -> None:
pipe_path = "/var/run/decnet-logs"
while True:
with open(pipe_path, "r") as pipe:
for line in pipe:
_handle_line(line.rstrip("\n"))
if __name__ == "__main__":
main()