Network service honeypot that captures attacker interactions and delivers them to your webhook.
github.com/evait-security/resin
resin deploys a set of realistic network services that look like a building automation controller sitting on your network. When someone touches these services (port scans, login attempts, data exfiltration), every interaction gets logged with full detail and pushed to your webhook endpoint as JSON.
It runs entirely in Docker. No agents, no cloud dependencies. Clone, configure one environment variable, and start the containers.
From the outside, resin looks like a Siemens DESIGO CC building automation system. It exposes FTP (firmware updates), SSH (management), HTTP/HTTPS (web panel), SMB (backup share), SNMP (monitoring), MySQL (application database), and Redis (cache). Every service responds with realistic banners and behavior. None of them grant actual access.
Every TCP connection, every authentication attempt, every command sent to these services gets written to PostgreSQL with:
- Source IP and port
- MAC address (when available via ARP)
- Service name and action type
- Credentials (username + password in cleartext)
- Full interaction data (headers, commands, payloads)
- Timestamps
Every 30 seconds, pending events get batched into a single HTTP POST and sent to your webhook URL as JSON.
- Linux system with Docker and Docker Compose
- Port 22 available (you need to move your real SSH first)
resin uses port 22 for the SSH honeypot. Move your real SSH to a different port first.
Edit your SSH daemon config:
sudo nano /etc/ssh/sshd_configFind the line #Port 22 (or Port 22) and change it:
Port 2222
If your system uses a socket-based activation (systemd), also check:
sudo systemctl edit ssh.socketAnd override the ListenStream:
[Socket]
ListenStream=
ListenStream=2222Apply the change:
# For systems using ssh.service directly:
sudo systemctl restart sshd
# For systems using ssh.socket (Ubuntu 22.04+, Debian 12+):
sudo systemctl restart ssh.socketVerify you can still connect on the new port before continuing:
ssh -p 2222 user@your-serverUpdate your firewall rules if applicable:
sudo ufw allow 2222/tcp
sudo ufw deny 22/tcpgit clone https://github.com/evait-security/resin.git
cd resin
cp .env.example .envEdit .env and set your webhook URL:
nano .envWEBHOOK_URL=https://your-endpoint.example.com/webhook
docker compose up -dThat is it. Services are live. Events flow to your webhook.
docker compose ps
docker compose logs -f resinTest from another machine:
ssh admin@your-honeypot-ip
curl http://your-honeypot-ipCheck the dashboard (only accessible from the host itself):
curl http://127.0.0.1:1337For remote dashboard access, use an SSH tunnel:
ssh -p 2222 -L 1337:127.0.0.1:1337 user@your-serverThen open http://localhost:1337 in your browser.
| Port | Protocol | Persona | Behavior |
|---|---|---|---|
| 21 | FTP | vsFTPd 3.0.5 | Anonymous read-only with fake firmware files. Logs all login attempts. |
| 22 | SSH | OpenSSH 8.9p1 Ubuntu | Accepts connections, logs credentials, always denies access. |
| 80 | HTTP | Siemens DESIGO CC v5.0 | Building automation login page. Logs all requests and POST credentials. |
| 443 | HTTPS | Siemens DESIGO CC v5.0 | Same as HTTP with self-signed TLS (Siemens cert subject). |
| 445 | SMB | SMB 3.1.1 | Responds to negotiate, logs NTLM auth attempts. Denies access. |
| 161/udp | SNMP | Siemens building controller | Responds to GET/GETNEXT with realistic OIDs. Logs community strings. |
| 3306 | MySQL | MariaDB 10.11.6 | Sends handshake, logs auth attempts, returns access denied. |
| 6379 | Redis | Redis 7.2.4 | Responds to PING/INFO, logs AUTH attempts, denies everything else. |
| 1337 | HTTP | Dashboard | Live event viewer. Localhost only. |
Every 30 seconds (configurable via DISPATCH_INTERVAL), resin sends a POST request:
{
"source": "resin",
"dispatched_at": "2026-05-26T14:30:00.000Z",
"count": 3,
"events": [
{
"id": 42,
"service": "ssh",
"source_ip": "192.168.1.100",
"source_port": 54321,
"mac_address": "aa:bb:cc:dd:ee:ff",
"action": "login_attempt",
"username": "root",
"password": "toor",
"data": {"method": "password"},
"timestamp": "2026-05-26T14:29:45.123Z"
},
{
"id": 43,
"service": "http",
"source_ip": "10.0.0.5",
"source_port": 49152,
"mac_address": null,
"action": "login_attempt",
"username": "admin",
"password": "admin",
"data": {
"method": "POST",
"path": "/api/login",
"user_agent": "Mozilla/5.0"
},
"timestamp": "2026-05-26T14:29:47.456Z"
}
]
}Events are marked as dispatched after successful delivery (HTTP 2xx). Failed deliveries are retried on the next cycle.
The web interface at http://127.0.0.1:1337 provides:
- Live event stream (Server-Sent Events, updates every 2 seconds)
- Service filter dropdown
- Full-text search across IPs, usernames, passwords, and actions
- Expandable event details showing raw interaction data
- Color-coded service indicators
No authentication required. The port is bound to localhost only. Use SSH tunneling for remote access.
attacker
|
+--------------+--------------+
| | |
port 22 port 80 port 445 ...
| | |
+-------+----------------------------+-------+
| resin container |
| |
| asyncio event loop |
| +-- SSH service (asyncssh) |
| +-- FTP service (custom protocol) |
| +-- HTTP/S service (aiohttp) |
| +-- SMB service (custom protocol) |
| +-- SNMP service (custom UDP) |
| +-- MySQL service (custom protocol) |
| +-- Redis service (custom protocol) |
| +-- Webhook dispatcher (30s loop) |
| +-- Web dashboard (:1337) |
| | |
+--------------------+-----------------------+
|
unix socket (no TCP, no auth)
|
+------+------+
| PostgreSQL |
| (events) |
+--------------+
|
webhook POST
|
your endpoint
All services run in a single Python process using asyncio. Events are logged directly to PostgreSQL via a shared Unix socket (no TCP, no passwords). The dispatcher uses SELECT ... FOR UPDATE SKIP LOCKED to batch pending events without contention, making the database act as a concurrent job queue without Redis or RabbitMQ.
| Variable | Default | Description |
|---|---|---|
WEBHOOK_URL |
(empty) | HTTP endpoint for event delivery. Leave empty to disable. |
DISPATCH_INTERVAL |
30 |
Seconds between webhook batch sends. |
WHITELIST_IPS |
(empty) | Comma/space-separated IPs and CIDR ranges to whitelist. Configured in .env. |
WHITELIST_IP_MASK |
(empty) | Newline-separated list of regular expressions matched against each source IP. A match drops the connection like a whitelisted IP. |
WHITELIST_FILE |
(empty) | Optional path to a whitelist file inside the container. Only used if set (e.g. when you bind-mount your own file); reloaded automatically on change and takes precedence over WHITELIST_IPS. |
WHITELIST_RELOAD_INTERVAL |
60 |
Poll interval in seconds for detecting whitelist file changes. Send SIGHUP for immediate reload. |
Any client whose source IP matches a whitelist entry is dropped immediately by every service before any logging happens — no events are created and no webhook payloads are sent for it.
Env-var (default): set WHITELIST_IPS in .env. Accepts individual IPv4/IPv6 addresses and CIDR ranges, comma or space separated:
WHITELIST_IPS=192.168.1.10, 10.0.0.0/24, 203.0.113.5
File-based (optional): set WHITELIST_FILE to a path you bind-mount into the container (one entry per line; comments and blank lines are ignored). It is checked every 60 seconds (configurable via WHITELIST_RELOAD_INTERVAL) and takes precedence over WHITELIST_IPS. For an immediate reload without restarting:
docker compose exec resin kill -HUP 1Invalid entries are ignored with a warning at startup.
Regex masks: set WHITELIST_IP_MASK in .env to one or more regular expressions (one per line) that are matched against the source IP string of every request. Any match drops the connection just like a whitelisted IP. This is evaluated independently of WHITELIST_IPS/WHITELIST_FILE, so exact IP/CIDR matching is never mixed with regex matching. Because each pattern is a whole line, commas inside a regex (for example quantifiers like {1,3}) are preserved.
# whitelist any source IP ending in .7
WHITELIST_IP_MASK=\.7$
For multiple patterns, wrap the value in double quotes and put one regex per line:
WHITELIST_IP_MASK="\.7$
^10\.
^192\.168\."
Invalid patterns are ignored with a warning at startup.
resin runs with network_mode: host so it can read the host's ARP table for real client MAC addresses. The web dashboard binds exclusively to 127.0.0.1:1337 - it is never exposed to the network.
The test suite validates that every service responds correctly, logs events to the database, and the web dashboard serves data. Tests run inside Docker against a dedicated test stack.
docker compose -f docker-compose.test.yml up --build --abort-on-container-exitThe test stack builds the test image target (Debian-slim with pytest and paramiko) — separate from the production image (distroless, no shell, no test tooling).
| Test class | What it validates |
|---|---|
TestSSHService |
Banner matches OpenSSH 8.9p1, password auth is rejected via paramiko |
TestFTPService |
vsFTPd banner, anonymous login succeeds, non-anonymous login rejected |
TestHTTPService |
DESIGO CC login page loads, POST credentials return 401, HTTPS works |
TestSMBService |
SMB2 negotiate handshake completes |
TestSNMPService |
sysDescr GET returns Siemens building controller string |
TestMySQLService |
MariaDB banner in handshake, auth packet returns error |
TestRedisService |
PING/PONG, AUTH rejected with WRONGPASS, INFO returns version |
TestWebUI |
Dashboard HTML loads, /api/events returns JSON array, SSE endpoint streams |
TestEventLogging |
Redis AUTH attempt appears in /api/events within 1 second |
TestDispatcher |
Events exist in database after interaction |
tests/test_webhook.py contains a full-cycle test that starts an HTTP server, triggers events, and waits for the dispatcher to POST them. Requires TEST_WEBHOOK_ENABLED=1:
docker compose -f docker-compose.test.yml run --rm \
-e TEST_WEBHOOK_ENABLED=1 tests \
pytest tests/test_webhook.py -vdocker compose up -d postgres
pip install -r requirements.txt
python -m src.mainRun tests locally (requires services to be running):
pip install pytest paramiko
RESIN_HOST=127.0.0.1 pytest tests/ -vMIT. See LICENSE.
Built by evait security.