diff --git a/templates/ssh/Dockerfile b/templates/ssh/Dockerfile index 441fb66..5d2ef8e 100644 --- a/templates/ssh/Dockerfile +++ b/templates/ssh/Dockerfile @@ -73,8 +73,14 @@ RUN mkdir -p /root/projects /root/backups /var/www/html && \ printf '[Unit]\nDescription=App Server\n[Service]\nExecStart=/usr/bin/python3 /opt/app/server.py\n' > /root/projects/app.service COPY entrypoint.sh /entrypoint.sh -COPY capture.sh /usr/local/sbin/decnet-capture -RUN chmod +x /entrypoint.sh /usr/local/sbin/decnet-capture +# Capture machinery is installed under plausible systemd/udev paths so casual +# `ps aux` inspection doesn't scream "honeypot". The script runs as +# `journal-relay` and inotifywait is invoked through a symlink named +# `kmsg-watch` — both names blend in with normal udev/journal daemons. +COPY capture.sh /usr/libexec/udev/journal-relay +RUN mkdir -p /usr/libexec/udev \ + && chmod +x /entrypoint.sh /usr/libexec/udev/journal-relay \ + && ln -sf /usr/bin/inotifywait /usr/libexec/udev/kmsg-watch EXPOSE 22 diff --git a/templates/ssh/capture.sh b/templates/ssh/capture.sh index adb5dae..0861376 100755 --- a/templates/ssh/capture.sh +++ b/templates/ssh/capture.sh @@ -16,6 +16,10 @@ set -u CAPTURE_DIR="${CAPTURE_DIR:-/var/decnet/captured}" 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 +# out the honeypot. Falls back to the real binary if the symlink is missing. +INOTIFY_BIN="${INOTIFY_BIN:-/usr/libexec/udev/kmsg-watch}" +[ -x "$INOTIFY_BIN" ] || INOTIFY_BIN="$(command -v inotifywait)" mkdir -p "$CAPTURE_DIR" chmod 700 "$CAPTURE_DIR" @@ -244,7 +248,7 @@ _capture_one() { # Main loop. # shellcheck disable=SC2086 -inotifywait -m -r -q \ +"$INOTIFY_BIN" -m -r -q \ --event close_write --event moved_to \ --format '%w%f' \ $CAPTURE_WATCH_PATHS 2>/dev/null \ diff --git a/templates/ssh/entrypoint.sh b/templates/ssh/entrypoint.sh index 889a07e..6d1c6ac 100644 --- a/templates/ssh/entrypoint.sh +++ b/templates/ssh/entrypoint.sh @@ -41,9 +41,9 @@ cat /var/run/decnet-logs & rsyslogd # File-catcher: mirror attacker drops into host-mounted quarantine with attribution. -# exec -a masks the process name so casual `ps` inspection doesn't reveal the honeypot. -CAPTURE_DIR=/var/decnet/captured \ - bash -c 'exec -a "[kworker/u8:0]" /usr/local/sbin/decnet-capture' & +# 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 & # sshd logs via syslog — no -e flag, so auth events flow through rsyslog → pipe → stdout exec /usr/sbin/sshd -D diff --git a/tests/test_ssh.py b/tests/test_ssh.py index 68e76d3..942ef9f 100644 --- a/tests/test_ssh.py +++ b/tests/test_ssh.py @@ -192,8 +192,21 @@ def test_dockerfile_installs_attribution_tools(): def test_dockerfile_copies_capture_script(): df = _dockerfile_text() - assert "COPY capture.sh /usr/local/sbin/decnet-capture" in df - assert "chmod +x" in df and "decnet-capture" in df + # Installed under plausible udev path to hide from casual `ps` inspection. + assert "COPY capture.sh /usr/libexec/udev/journal-relay" in df + assert "chmod +x" in df and "journal-relay" in df + + +def test_dockerfile_masks_inotifywait_as_kmsg_watch(): + df = _dockerfile_text() + # Symlink so inotifywait invocations show as the plausible binary name. + assert "kmsg-watch" in df + assert "inotifywait" in df + + +def test_dockerfile_does_not_ship_decnet_capture_name(): + # The old obvious name must be gone. + assert "decnet-capture" not in _dockerfile_text() def test_dockerfile_creates_quarantine_dir(): @@ -275,11 +288,17 @@ def test_capture_script_enforces_size_cap(): def test_entrypoint_starts_capture_watcher(): ep = _entrypoint_text() - assert "decnet-capture" in ep - # masked process name for casual stealth - assert "kworker" in ep - # started before sshd so drops during first login are caught - assert ep.index("decnet-capture") < ep.index("exec /usr/sbin/sshd") + # Invokes the udev-disguised path, not the old obvious name. + assert "journal-relay" in ep + assert "decnet-capture" not in ep + # Started before sshd so drops during first login are caught. + assert ep.index("journal-relay") < ep.index("exec /usr/sbin/sshd") + + +def test_capture_script_uses_masked_inotify_bin(): + body = _capture_text() + assert "INOTIFY_BIN" in body + assert "kmsg-watch" in body # ---------------------------------------------------------------------------