Tripwires

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 aggregates
    • GET /events?severity=high&limit=100 — filter events
    • GET /events/stream — live server-sent-events feed
    • GET /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/notifications
  • PUT /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 .env and VPN keys. Encrypt it at rest — pipe through age/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 /stats total_events climbing = ingestion is alive.
  • Disk: harden.sh installs a disk-space timer; honeypot logs are the main growth source. Confirm retention_days on 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; skim audit_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.