Medium-interaction SSH honeypot for SOFAH. It presents a cloned device's SSH banner,
accepts logins under a configurable weak-credential / accept-Nth policy, and drops the
attacker into a fake shell whose ls/cat/uname/busybox/wget responses are all
canned. Nothing the attacker types is ever executed; wget/curl/tftp URLs are logged
but never fetched.
It mirrors Cowrie's medium-interaction model but feeds the SOFAH log schema and takes its persona (banner, hostname, kernel, weak creds) from recon via ennorm.
src/
startup.py # entrypoint: env -> SofahLogger + Persona + FakeFilesystem -> server
ssh_honeypot.py # paramiko ServerInterface + accept loop (the only paramiko-aware file)
auth.py # AuthPolicy: planted weak creds + accept-Nth (paramiko-free)
persona.py # Persona: banner / hostname / prompt / uname, from data/persona.json
shell/
emulator.py # FakeShell: parse (shlex, never exec) + dispatch + per-session state
commands.py # canned responders: ls, cat, uname, busybox, wget(capture), ...
filesystem.py # FakeFilesystem: in-memory tree; cat/ls never touch the host FS
data/
persona.json # default persona (ennorm overwrites this per cloned device)
sandbox/ # static decoy files planted into the fake FS (/etc/passwd, ...)
All events use the ssh.honeypot.* eventid namespace and carry a per-connection session:
| eventid | when |
|---|---|
ssh.honeypot.connect |
new TCP connection |
ssh.honeypot.client_version |
client's SSH banner |
ssh.honeypot.login_attempt |
every credential pair (result: accepted/failed) |
ssh.honeypot.command |
every command line entered |
ssh.honeypot.download |
wget/curl/tftp URL (captured, never fetched) |
ssh.honeypot.direct_tcpip |
refused pivot/tunnel attempt |
ssh.honeypot.session_end |
duration + command count |
| var | default | meaning |
|---|---|---|
LOG_API |
– | log-api base URL (e.g. http://log_api:50005) |
SSH_PORT |
65022 |
in-container listen port (unprivileged; compose maps the real port to it) |
EXT_PORT |
22 |
externally published port, used as the logging dst_port |
PERSONA |
data/persona.json |
persona file path |
SANDBOX |
data/sandbox |
decoy-file tree planted into the fake FS |
- Never executes attacker input — the shell is canned; downloads are logged, not fetched.
- Non-root, read-only root FS,
cap_drop: [ALL]— the pot listens on a high port so it needs no added capabilities; it writes nothing to disk. - Unique host key per deployment — generated in memory at startup, never reused.
- Resource caps — max concurrent sessions, per-session command cap, idle timeout.
pytest -q. The shell/persona/filesystem/auth suites are paramiko-free; the
ServerInterface test importorskips paramiko (installed in CI and in the image).