Administrator guide
Operating the honeypot platform: deploy it, run it day to day, keep it safe, and recover it. Written for the person with root on the honeypot host and admin on the iOS app — no prior familiarity with the codebase assumed.
For why the network is shaped the way it is, see how it works and network setup; this guide is the how.
The one rule that matters most: the honeypot is bait. Assume every persona container is hostile. Nothing an attacker touches there may reach your real network, and no real secret may live inside a persona. Every step below exists to keep that true.
1. What you are running
| Plane | Runs | Reachable from |
|---|---|---|
| Honeypot (DMZ) | Cowrie (SSH) + OpenCanary (Windows WS/Server, NAS) persona containers | the public internet, on the trap ports only |
| Management | FastAPI API + PostgreSQL + log shippers | you, over the admin VPN only |
| Admin | your laptop / iPhone | via Tailscale or WireGuard |
Attacks hit the trap ports → each sensor logs JSON → a ship.py sidecar batches
it to the API's /ingest endpoint → the normalizer turns it into standard
events in PostgreSQL → the iOS app reads stats + a live feed and receives push.
Ports exposed on the host (dev compose defaults; change per deployment):
| Persona | Trap ports |
|---|---|
| Linux (Cowrie) | SSH 2222 |
| Windows WS | RDP 3389, SMB 10445 |
| Windows Server | RDP 13389, SMB 20445 |
| NAS | HTTP 8080, FTP 21, SMB 445 |
The API listens on 127.0.0.1:8000 — loopback only, never published to the
public interface. You reach it over the admin VPN.
2. First-time deployment
Target: a dedicated Debian/Ubuntu host (VM or physical) in an isolated VLAN/DMZ. Do not co-locate this with anything you care about.
2.1 Harden the host
sudo infrastructure/host/harden.sh
Installs unattended security upgrades, chrony, fail2ban, nftables and Docker; applies kernel-hardening sysctls; installs a disk-space monitor. SSH key-only mode is opt-in inside the script — set it before you lock yourself out.
2.2 Configure secrets
cp .env.example .env
make -C server secrets # prints three fresh random secrets
Paste the generated values into .env. At minimum set:
| Key | What it is |
|---|---|
JWT_SECRET |
signs admin login tokens |
INGEST_TOKEN |
shared secret the shippers present to /ingest |
PEPPER |
server-side pepper for one-way hashing of captured passwords |
POSTGRES_PASSWORD |
database password |
ADMIN_USERNAME / ADMIN_PASSWORD |
the first admin login |
.env is gitignored and must stay that way. Never commit it; never copy it to
removable media. Encrypt it before backup (§7).
2.3 Firewall + egress isolation
sudo nft -f infrastructure/host/nftables.conf # host firewall (default-drop)
sudo infrastructure/host/docker-egress.sh # deny honeypot containers' egress
This is the containment boundary. nftables.conf drops everything inbound
except the trap ports and admin-VPN traffic, and blocks the honeypot subnet from
reaching the management plane and the admin VPN interface. docker-egress.sh
adds a deny-by-default rule in Docker's DOCKER-USER chain so a compromised
persona cannot open outbound connections.
2.4 Bring up an admin VPN
Pick one (both are supported — see the README):
sudo infrastructure/tailscale/setup-tailscale.sh # recommended: auto-HTTPS
# or
sudo infrastructure/wireguard/setup-wireguard.sh
Tailscale is recommended because tailscale serve fronts the loopback API with
an automatic HTTPS certificate at https://<host>.<tailnet>.ts.net, so the iOS
app gets real TLS with no exception. Apply infrastructure/tailscale/acl-example.hujson
in the Tailscale admin console so only your devices can reach the host.
2.5 Start the stack
make -C server up # builds + starts postgres, api, sensors, shippers
make -C server smoke # end-to-end check: health → login → ingest → stats
make up runs python -m app.init_db in the API container on start, which
applies database migrations to head and seeds the admin user (idempotent). A
green smoke means the pipeline works end to end.
2.6 Connect the iOS app
Open Honeypot Monitor, enter the server URL (https://<host>.<tailnet>.ts.net
for Tailscale, or the WireGuard tunnel address) and your admin credentials. The
token is stored in the iPhone keychain.
3. Day-to-day operations
Everything below is reachable two ways: the iOS app (for routine viewing and
persona toggling) and the API (for scripting/automation). API base path is
/api/v1; authenticate by logging in and sending the JWT as Authorization: bearer <token>.
Get a token:
TOKEN=$(curl -s https://HOST/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"…"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
3.1 Watch what's happening
- iOS app — Dashboard (totals, last 24 h, severity mix, top source IPs/usernames), Live tab (real-time SSE feed), Search (filter by category/severity/IP/user), and per-event detail.
- API —
GET /stats— dashboard aggregatesGET /events?severity=high&limit=100— filter eventsGET /events/stream— live server-sent-events feedGET /sessions/{id}— a single attacker session's timeline
Every captured password is stored one-way hashed — you can see that a password was tried and correlate reuse, but never read the plaintext. That is by design; do not try to defeat it.
3.2 Manage personas
Personas are stored in the DB and toggled at runtime. Sample profiles live in
server/profiles/*.json.
| Action | iOS | API |
|---|---|---|
| List | Settings → Personas | GET /personas |
| Enable | toggle on | POST /personas/{id}/enable |
| Disable | toggle off | POST /personas/{id}/disable |
| Create | — | POST /personas (body = persona profile JSON) |
| Update | — | PUT /personas/{id} |
| Delete | — | DELETE /personas/{id} (must be disabled first) |
| Export | — | GET /personas/{id}/export |
An enabled persona cannot be deleted — disable it first. Every create /
update / delete / enable / disable is written to the audit_log table (§6).
To add a new persona type, author a profile JSON (copy an existing one in
server/profiles/), POST /personas it, wire its service into
server/docker/docker-compose.dev.yml (OpenCanary conf under
server/docker/opencanary/), and pick unique host ports so it doesn't clash
with the others.
3.3 Notification preferences
Per-admin, set in the iOS app (Settings) or the API:
GET /preferences/notificationsPUT /preferences/notifications— master on/off, minimum severity, muted categories, quiet-hours window.
Critical alerts always come through — they bypass the severity threshold and quiet hours, but never the master off switch or an explicit category mute. Repeated events in a burst are grouped into a single push, so a scan flood is one notification, not hundreds.
3.4 Add another admin user
There is no self-service signup (this is an admin tool). Create users directly:
make -C server shell # shell into the api container
python - <<'PY'
from app.database import SessionLocal
from app.models import User
from app.security import hash_password
db = SessionLocal()
db.add(User(username="alex", password_hash=hash_password("<strong-password>")))
db.commit()
PY
Each admin gets their own notification preferences and device registrations.
4. Database & schema changes
Schema is owned by Alembic migrations (server/migrations/), not ad-hoc
table creation — so every environment upgrades along the same path.
make -C server migrate # upgrade a running DB to the latest revision
make -C server migrate-sql # preview the upgrade SQL without applying it
make -C server migrate-down # roll back one revision
To change the schema: edit server/app/models.py, then
make -C server migration m="add geo columns to events"
Review the generated file under server/migrations/versions/, commit it, and
apply with make migrate. The test suite fails if a model change ships without
a matching migration, so drift can't sneak in.
5. Backup & restore
# Back up host configs + compose project + a PostgreSQL dump, keep 7:
sudo BACKUP_ROOT=/var/backups/honeypot RETENTION=7 \
DB_CONTAINER=honeypot-dev-postgres-1 DB_NAME=honeypot \
infrastructure/backup/backup.sh
Produces a timestamped *.tar.gz containing /etc/wireguard, /etc/nftables.conf,
sshd_config, the compose project (configs, profiles, .env), and a
pg_dump -Fc of the database. Old archives past RETENTION are pruned.
# Restore from an archive:
sudo DB_CONTAINER=honeypot-dev-postgres-1 DB_NAME=honeypot \
infrastructure/backup/restore.sh /var/backups/honeypot/<stamp>.tar.gz
Restore replaces /etc configs (saving .bak copies), the compose project, and
the database (pg_restore --clean --if-exists), then reloads the firewall and
WireGuard.
Secrets in backups: the archive contains
.envand VPN keys. Encrypt it at rest — pipe throughage/sops, or store only on an encrypted volume. Never put an unencrypted backup on shared or removable storage. Test a restore on a throwaway host before you rely on it.
Schedule it with a systemd timer or cron (e.g. nightly). Verify backups periodically — an untested backup is a hope, not a plan.
Tripwires on another VLAN? If the honeypot sits on its own segment (it should), machines elsewhere can't reach the beacon and their bait files fail silently. Open exactly one port — the beacon port, from your other VLANs to the appliance only. Network setup has the rule.
6. Audit log
Every configuration change and admin action is recorded in the audit_log
table: who did it (user_id), action, target, detail (JSON), timestamp.
Persona lifecycle changes and enable/disable are all captured. Query it:
make -C server shell
python -c "from app.database import SessionLocal; from app.models import AuditLog; \
[print(a.created_at, a.action, a.target) for a in SessionLocal().query(AuditLog).order_by(AuditLog.created_at.desc()).limit(50)]"
Include audit_log in your backups (it's in the DB dump) and review it after any
suspicious change.
7. Updates & rollback
git pull # get new code
make -C server build # rebuild images
make -C server migrate # apply any new migrations
make -C server up # recreate containers
make -C server smoke # confirm the pipeline still works
Roll back by checking out the previous commit, make build && make up, and
if a migration ran, make migrate-down to step the schema back. Pin the Cowrie
image to a digest and opencanary==<version> before hand-off so an upstream
change can't surprise you (see docs/DEPENDENCIES.md).
Recover a compromised persona: just recreate it — the containers are
stateless and rebuilt from images. make -C server down && make -C server up
returns every persona to a known-clean state; nothing an attacker did inside one
survives.
8. Push notifications (APNs)
Push is off until you supply an Apple APNs key. In .env set APNS_KEY_ID,
APNS_TEAM_ID, APNS_BUNDLE_ID (com.szakari.honeypotmonitor), and
APNS_AUTH_KEY_PATH pointing at your .p8 file. Mount the .p8 into the API
container from outside the repo — never commit it, never place it on the ExFAT
SSD. Restart the API, then use the iOS app's Send a test push (Settings) to
confirm delivery. Until configured, the app is fully usable via pull-to-refresh
and the live feed; only background push is inactive.
9. Health & monitoring
make -C server ps— container status;make -C server logs— follow logs.make -C server smoke— synthetic end-to-end check any time.GET /statstotal_eventsclimbing = ingestion is alive.- Disk:
harden.shinstalls a disk-space timer; honeypot logs are the main growth source. Confirmretention_dayson each persona profile matches your storage. - fail2ban protects the admin SSH port; the trap ports are meant to be hit.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| iOS app "No server configured" / can't connect | admin VPN down, or wrong URL | reconnect Tailscale/WireGuard; use the …ts.net HTTPS URL |
| Login fails | wrong creds, or DB not seeded | check ADMIN_* in .env; make seed |
| No events appearing | sensor or shipper down | make ps; make logs; confirm INGEST_TOKEN matches in .env and shipper env |
/ingest returns 401 |
ingest token mismatch | align INGEST_TOKEN across API and shippers |
| Persona won't delete | it's still enabled | disable it first, then delete |
| Push never arrives | APNs not configured / no .p8 |
§8; verify entitlement + key IDs |
| Migration error on start | schema drift or partial upgrade | make migrate-sql to inspect; make migrate |
Port clash on make up |
two personas share a host port | give each a unique host port in the compose file |
Escalate a genuine incident (persona breakout, unexpected egress) by pulling the host's network cable / detaching the DMZ interface first, then investigating from the captured logs — which are in the management plane, out of the attacker's reach.
11. Routine maintenance checklist
- Daily: glance at the dashboard; confirm event count is advancing.
- Weekly:
make smoke; skimaudit_log; check disk headroom. - Monthly:
git pull+ rebuild + migrate; verify a backup restores on a throwaway host; review persona set and retention. - Quarterly: rotate
JWT_SECRET/INGEST_TOKEN/admin passwords; regenerate the SBOM (python server/scripts/gen_sbom.py) and review new dependencies; re-verify containment (no honeypot→mgmt/LAN route, no persona egress).
12. Quick reference
File locations
| Path | What |
|---|---|
.env |
secrets (gitignored) |
server/profiles/*.json |
sample persona profiles |
server/docker/docker-compose.dev.yml |
the stack |
server/docker/opencanary/*.conf |
Windows/NAS persona service configs |
server/migrations/ |
database migrations |
infrastructure/host/ |
harden.sh, nftables.conf, docker-egress.sh |
infrastructure/{tailscale,wireguard}/ |
admin VPN setup |
infrastructure/backup/ |
backup.sh, restore.sh |
Make targets: up · down · logs · ps · seed · migrate · migration · migrate-down · migrate-sql · shell · test · smoke · secrets
API (/api/v1): auth/login · events · events/{id} · events/stream · sessions/{id} · stats · personas (+enable/disable/export) · preferences/notifications · devices (+test-push) · ingest/events
Golden rules: the honeypot never reaches your real network · no real secrets
inside a persona · captured passwords stay hashed · .env and .p8 never leave
the host · test your backups.