feat(ssh): capture password attempts via pam_exec auth-helper

Real OpenSSH doesn't log attempted passwords — only success/failure
with username — leaving SSH the sole auth-bearing service in the
fleet that contributes nothing to the cred corpus FTP/MySQL/RDP/
VNC/etc. populate. Closes that gap with a tiny pam_exec shim.

A static C helper (~80 LoC, musl, ~38KB stripped) is wired into
/etc/pam.d/sshd as `auth optional pam_exec.so expose_authtok stdout
/usr/sbin/auth-helper`. pam_exec writes the attempted password to
the helper's stdin NUL-terminated; the helper formats an RFC 5424
line in the exact shape templates/syslog_bridge.py produces
(facility local0, PEN 55555, MSGID auth_attempt — same MSGID FTP
uses) and writes it to /proc/1/fd/1 so the existing collector
stdout-reader pipeline picks it up.

Two password fields ride in the SD-block:
- password=     RFC 5424 escaped, ASCII-printable only, ? for non-
                printables. FTP-compatible — existing dashboard
                rendering picks up SSH attempts unchanged.
- password_b64= base64 of the exact PAM_AUTHTOK bytes. Preserves
                NUL/0xff/control-byte fingerprinting signal that the
                plain field necessarily drops.

Fail-open by design: the PAM line is `optional` so a malfunctioning
helper never blocks sshd auth. Better to miss a cred than break the
honeypot.

Verified end-to-end inside the rebuilt image:
- 38KB static ELF, runs without a dynamic linker
- correct RFC 5424 line for `hunter2` → b64 `aHVudGVyMg==`
- NUL truncation matches pam_exec's contract
- 0xff bytes survive losslessly through password_b64
- empty password produces a well-formed line (e.g. pubkey auth path)
This commit is contained in:
2026-04-25 04:42:50 -04:00
parent c69fdbb4ac
commit d064125f61
2 changed files with 197 additions and 0 deletions

View File

@@ -1,4 +1,17 @@
ARG BASE_IMAGE=debian:bookworm-slim
# ── Stage 1: build the static auth-helper credential-capture binary ──────────
# Compiled against musl so the resulting binary is fully static — runs on
# any glibc/musl Linux without a libc version match. Stripped at link
# time via -s so `file /usr/sbin/auth-helper` reports a generic ELF.
FROM debian:bookworm-slim AS auth-helper-build
RUN apt-get update && apt-get install -y --no-install-recommends musl-tools \
&& rm -rf /var/lib/apt/lists/*
COPY auth-helper.c /tmp/auth-helper.c
RUN musl-gcc -static -O2 -s -Wall -Wextra \
-o /auth-helper /tmp/auth-helper.c
# ── Stage 2: the actual SSH decky image ──────────────────────────────────────
FROM ${BASE_IMAGE}
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -57,6 +70,17 @@ RUN sed -i \
-e 's|^\$PrivDropToGroup|#$PrivDropToGroup|' \
/etc/rsyslog.conf
# auth-helper: drop the static binary into /usr/sbin and wire pam_exec
# into the sshd PAM stack so every password attempt (success or fail) is
# captured before pam_unix runs. `optional` so a malfunctioning helper
# never blocks auth — fail-open is correct: missed creds are recoverable,
# a borked honeypot is not. expose_authtok writes the password to the
# helper's stdin, NUL-terminated.
COPY --from=auth-helper-build /auth-helper /usr/sbin/auth-helper
RUN chmod 755 /usr/sbin/auth-helper && \
sed -i '1i auth optional pam_exec.so expose_authtok stdout /usr/sbin/auth-helper' \
/etc/pam.d/sshd
# Sudo: log to syslog (auth facility) AND a local file with full I/O capture
RUN echo 'Defaults logfile="/var/log/sudo.log"' >> /etc/sudoers && \
echo 'Defaults syslog=auth' >> /etc/sudoers && \