Installation - sparkycz1/honeypot-shelf GitHub Wiki
One docker compose up, and the fleet-watching begins.
git clone https://github.com/sparkycz1/honeypot-shelf.git
cd honeypotshelf
python3 scripts/setup.pyscripts/setup.py is a self-contained, pure-stdlib wizard (needs only a
system python3 and Docker โ nothing from this project's own virtualenv):
generates every secret (SECRET_KEY, ENCRYPTION_KEY,
POSTGRES_PASSWORD, REDIS_PASSWORD, INFORM_TOKEN),
asks a handful of questions (timezone, whether to use the bundled Caddy
reverse proxy and its domain/email, whether to add the optional VPN
sidecar (docker-compose.vpn.yml โ no provider/key/config asked here,
that's all Settings โ VPN once the app is running, see
Architecture's "VPN connectivity" section), whether the
app's port should only accept local connections, the facts/reachability
check intervals, event retention, the deploy-wide default UI language
(DEFAULT_LANGUAGE โ what a fresh account/anonymous request renders in;
anyone can still switch for themselves any time in My account โ
Language), the superadmin password (typed twice, masked โ no
auto-generate option any more, so it's never echoed or printed anywhere)
and the host port), writes .env, applies the
Alembic migration, brings the stack up, waits for it to become healthy,
and creates the first superadmin account (admin). Re-running against an
existing .env just tops it up with any new .env.example variables and
restarts the stack (auto-detecting whether Caddy/the VPN sidecar were
running, same as scripts/upgrade.sh does) โ never
regenerates secrets or touches your data.
Once it finishes, log in and create at least one Company and one
Honeypot from the Companies/Honeypots pages (both superadmin-only) โ
nothing shows up on the Dashboard before that. Pin that honeypot's SSH
host key and events start arriving automatically, no setup needed on the
Pi itself โ see Honeypot Management.
If you'd rather configure everything by hand instead of using
scripts/setup.py:
cp .env.example .env
python3 scripts/generate_secrets.pyPaste the printed values (SECRET_KEY, ENCRYPTION_KEY,
POSTGRES_PASSWORD, REDIS_PASSWORD, INFORM_TOKEN)
into .env. Optionally set TZ (e.g. Europe/Prague); defaults to UTC.
Then:
docker compose up -d --buildalembic/versions/ already ships the initial schema migration; the
one-shot migrate service applies it automatically (alembic upgrade head) before web/worker/beat start. Need a new migration? See
Development.md.
The app listens on APP_PORT (default 8080, plain HTTP, all interfaces
by default โ meant to sit behind a TLS-terminating reverse proxy; set
APP_BIND_ADDRESS=127.0.0.1 in .env โ no docker-compose.yml edit
needed โ or firewall the port off if you don't want that). Point your own
nginx/Traefik/Caddy at it โ see the reverse-proxy guides:
nginx ยท Traefik ยท
Caddy (standalone). Or use the bundled Caddy
(automatic HTTPS via Let's Encrypt): set DOMAIN and ACME_EMAIL in
.env, point that domain's DNS at this host, open ports 80/tcp, 443/tcp
and 443/udp, then
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d --build.
If a honeypot sits behind a NAT with no port forwarded to it, add
-f docker-compose.vpn.yml too (combine freely with docker-compose.caddy.yml
above) and connect either NetBird or WireGuard from Settings โ VPN once
the app is running โ see Architecture's "VPN
connectivity" section for what this does, which of the two to pick, and
why it's a separate container.
The NetBird client built into the image is pinned to an exact version by
NETBIRD_VERSION in .env (see .env.example) โ docker-compose.yml
passes it to the build, and a build without it stops with a message
saying so. Bump it there deliberately, then rebuild: a newer client may
not read the identity an older one registered, and then needs a fresh
setup key once.
Then create the first superadmin account:
docker compose exec web python scripts/create_admin.py --username adminLog in, then create at least one Company and one Honeypot from the
Companies/Honeypots pages โ nothing shows up on the Dashboard before
that.
Warning
The app speaks plain HTTP only. Always put TLS termination in front
of it, and firewall its port off (or set APP_BIND_ADDRESS=127.0.0.1 in
.env) if you don't want it reachable directly.
Warning
Two features are browser-disabled outright on plain HTTP, for any origin
other than http://localhost โ entirely absent from window/
navigator, no server-side workaround: WebAuthn/passkeys (My
account โ Passkeys shows "This browser doesn't support passkeys" even in
a browser that does) and the honeypot terminal's clipboard copy/paste
(Ctrl+C/Ctrl+V and right-click copy; native Ctrl+V still works, since
that skips the Clipboard API). Both need a real "secure context" โ
HTTPS (an https:// reverse proxy, Caddy or otherwise) or
http://localhost on the machine Honeypot Shelf runs on. A plain HTTP
LAN IP/hostname (http://192.168.1.x:8080) satisfies neither, no matter
how the app or host firewall is configured.
No public domain needed on a LAN-only deployment. A browser treats
any https:// origin as secure regardless of certificate trust โ a
self-signed one is enough, at the cost of a one-time "proceed anyway"
click per client. The bundled Caddy can mint one itself: in
./Caddyfile, replace the site address with tls internal โ
:443 {
tls internal
reverse_proxy web:8080
}
then docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d --build and open https://<this-host's-LAN-IP>. DOMAIN/ACME_EMAIL
aren't needed here. To stop the browser warning permanently, install
Caddy's local CA on each client (docker compose exec caddy caddy trust
prints where to find it) โ cosmetic only, WebAuthn/clipboard work either
way once the page loads over https://.
Already have HTTPS via a reverse proxy and still seeing this? The
app also needs to know the request arrived as HTTPS โ otherwise it
builds/verifies URLs as if still plain HTTP, failing WebAuthn
("Unexpected client data origin") and OIDC login the same way. Fixed by
TRUSTED_PROXY_IPS (.env.example, default *) โ already on for every
setup above. See app/core/proxy_headers.py and
Authentication & RBAC for the full story.
Audit log / rate limiter showing the proxy's IP instead of the real
client's? A separate, off-by-default fix โ TRUST_FORWARDED_FOR
(.env.example) โ trusting it from anyone would let an attacker defeat
the login rate limiter by spoofing a new "source" every attempt. Turn it
on once TRUSTED_PROXY_IPS is narrowed to your real proxy (not *).
docker-compose.yml's db service applies a handful of Postgres tuning
flags from the very first docker compose up (shared_buffers,
effective_cache_size, work_mem, maintenance_work_mem, and two
autovacuum_*_scale_factor settings) โ sized for a fleet up to roughly
100 honeypots while staying RAM-conscious, well under Postgres' own stock
"assume a big dedicated box" numbers. Override any of them via the
matching POSTGRES_* variable in .env (see .env.example) for a
bigger fleet/host, or a smaller/constrained one.
By default, Honeypot Shelf shows its own built-in three-hexagon mark in the nav bar, login/two-factor pages, and browser tab โ an outline in the brand's own amber, which reads fine on any background so it needs no light/dark switching of its own. To replace it with your own:
# In .env โ either works for LOGO_SOURCE and FAVICON_SOURCE independently:
LOGO_SOURCE=https://example.com/my-logo.svg # a URL โ fetched by `web` itself, not the browser
LOGO_SOURCE=/app/branding/logo.svg # a file path readable inside the `web` containerA filesystem path needs the file mounted into the container first โ
uncomment the branding volume on the web service in
docker-compose.yml, drop the file(s) into a local ./branding/
directory, and point LOGO_SOURCE/FAVICON_SOURCE at
/app/branding/<filename>. Any common image format works. Restart web
after changing either โ see .env.example/app/web/branding.py for how
a URL vs. a local path is told apart.
A URL is fetched by web itself and re-served from its own origin
(/branding/logo//branding/favicon), cached in memory for that
process's lifetime โ never linked to directly. Only an image response
(image/*, or no type with an image file extension) of at most 5 MB is
accepted; anything else โ an HTML error page, a login portal โ is
ignored and the built-in logo stays, so nothing but an image is ever
served from this app's origin. This app's CSP only
allows img-src 'self' data:; ("no CDN" is a deliberate stance, see
Architecture), so a browser would otherwise just refuse
to load a third-party image URL.
./scripts/upgrade.shPulls the latest code, syncs any new .env.example variables into your
.env (scripts/env_sync.py), rebuilds, and re-applies migrations.
Auto-detects whether the bundled Caddy or the VPN sidecar are currently
running (by their Compose service label, not an .env flag) and includes
the matching overlay file(s) automatically โ nothing to pass by hand.
Afterwards it removes the images the stack no longer uses โ the previous
Honeypot Shelf build and any image a version bump replaced (only this
stack's own, never another project's); --no-cleanup keeps them.
./scripts/stop.sh
./scripts/start.shstop.sh stops every container (docker compose stop โ nothing removed,
your data stays exactly as it was); start.sh starts them again. Both
auto-detect whether Caddy and/or the VPN sidecar are part of this
deployment (by each container's own Compose service label, not .env),
so it's the same one command regardless of which overlay file(s) you're
actually running. start.sh refuses to run (pointing at scripts/ setup.py instead) if it finds no existing containers โ it only starts a
stack already set up once, it doesn't create one.
For a one-off restart of a single service (e.g. after editing
Caddyfile), plain docker compose restart <service> still works โ
these two scripts are for stopping/starting everything together.
./scripts/backup.shWrites one timestamped directory under ./backups/ (override with
BACKUP_DIR in .env) holding everything needed to rebuild this instance
from nothing on a fresh host:
-
db.sql.gzโ apg_dumpof the whole database, taken live via Postgres's own MVCC snapshot (the stack does not need to be stopped). -
env.backupโ a copy of.env. In particularENCRYPTION_KEY: every encrypted secret in the database (honeypot passwords/private keys, LDAP/OIDC client secrets, NetBird/WireGuard config, TOTP secrets โ see Architecture) is encrypted with it, so a database restored under a different key turns those permanently unreadable โ no recovery after the fact, not even by hand. (Unlike debcontrol, Honeypot Shelf keeps no separate SSH identity volume to back up โ every honeypot's credential already lives in the DB, covered bydb.sql.gz.)
Old backup directories are pruned automatically โ anything older than
BACKUP_RETENTION_DAYS (default 14, override in .env) is deleted at the
end of every run, safe to leave running unattended forever.
The backup directory holds secrets in the clear (env.backup, and
every value the DB dump can decrypt combined with it) โ created chmod 600-ish but that only protects against other local accounts on the same
host. Copy it somewhere access-controlled and off this host (object
storage, another server's backup job pulling over rsync/scp, ...)
rather than trusting a local disk alone; losing the host and ./backups/
together is the same as never having backed up at all.
Settings โ Backup & restore (superadmin) downloads one file
(honeypotshelf-backup-<time>.hsbak) with everything the database holds โ
honeypots and their credentials, companies, accounts and memberships,
scheduled tasks, notification rules, settings, the SSH identity key,
OpenCanary events, the audit log and all history. It is encrypted with a
passphrase you choose (min. 12 characters; scrypt + AES-256-GCM in
authenticated chunks โ a wrong passphrase or a damaged/truncated file is
refused). It also carries ENCRYPTION_KEY, so it restores onto a fresh
instance with another key too: every stored secret is re-encrypted under
the target's key. .env is not included (the target keeps its own) โ for
that, use scripts/backup.sh above. For scheduled off-site copies, a
superadmin API token can POST /api/v1/backup with
{"passphrase": "โฆ"} and save the response body.
Automatic backups (same tab, superadmin): switch them on, choose
"every N hours" (1โ744), how many to keep (1โ365) and a passphrase. The
worker then writes the same .hsbak file to backups/ on the app_data
volume (/app/data, DATA_DIR) and removes all but the newest N.
- The passphrase is stored encrypted in the database so backups can run unattended. It is never shown again; a restore asks for it.
- The tab lists the stored files with download and delete, shows when the last run was and why it failed, and has Back up now.
- A failed run is recorded on the tab and in the audit log
(
backup.auto.run). There is no notification for it: rules here only cover honeypot alerts and availability. - The
app_datavolume is new in 0.59.0 and is mounted intowebandworkerbydocker-compose.yml. With your own compose file, add the same mount to both, or the worker's backups are not visible to the web container. - The files sit next to the application. They cover a bad change or a damaged database, not the loss of the host โ copy them elsewhere:
# newest stored backup, through the REST API
NAME=$(curl -fsS -H "Authorization: Bearer $TOKEN" https://honeypots.example.com/api/v1/backup/auto | jq -r '.backups[0].name')
curl -fsS -H "Authorization: Bearer $TOKEN" -o "$NAME" "https://honeypots.example.com/api/v1/backup/auto/files/$NAME"REST API (superadmin token): GET/PUT /api/v1/backup/auto (schedule;
the passphrase is write-only), POST /api/v1/backup/auto/run,
GET/DELETE /api/v1/backup/auto/files/{name}.
Restore (web only): upload the file, enter its passphrase and type
RESTORE. Every table is emptied and refilled in one transaction โ
nothing changes if anything fails โ then everyone is signed out and signs
in with an account from the backup. The backup must come from the same
version (database revision); restore onto that version, then upgrade.
Behind a reverse proxy, raise its upload limit first (nginx:
client_max_body_size). Both actions are audit-logged.
Run it daily at, say, 03:15 server time โ as the same user that normally
runs docker compose here (needs Docker socket access), with output
mailed/logged rather than silently discarded so a failure doesn't go
unnoticed:
crontab -e15 3 * * * cd /path/to/honeypotshelf && ./scripts/backup.sh >> /var/log/honeypotshelf-backup.log 2>&1Adjust /path/to/honeypotshelf to the actual checkout path (pwd from
inside it), and make sure /var/log/ (or wherever you point the log) is
writable by that user โ touch /var/log/honeypotshelf-backup.log && chown that-user /var/log/honeypotshelf-backup.log if it isn't yet. Check the log
after the first scheduled run to confirm it actually succeeded, and
periodically after that โ a cron job that silently stopped working is
worse than no backup job, since it looks like there's one until the day
you need it.
If you'd rather ship backups straight off the host instead of relying on
someone to sync ./backups/ separately, append a second line to the same
cron entry (or a follow-up cron job a few minutes later) that
rsync/scp/aws s3 syncs the freshly-created directory (or the whole
BACKUP_DIR) to wherever your off-host storage is.
./scripts/restore.sh backups/20260909T031500ZDestructive โ replaces the current database and .env outright (the
current .env is saved as .env.pre-restore first, never silently
discarded). Requires typing restore to confirm (--yes skips that, for
a scripted DR runbook). Stops web/worker/beat, drops and recreates
the database from db.sql.gz, replaces .env, then starts the stack back
up. Restore onto a checkout already on the version the backup was taken
from โ run upgrade.sh afterward if you need to move it forward.
Every secret this app stores (honeypot passwords/private keys, LDAP/OIDC client secrets, NetBird/WireGuard config, TOTP secrets) has used AES-256-GCM since v0.17.0 โ see Architecture โ FIPS alignment. A value encrypted by an older version is still read transparently forever (nothing breaks by doing nothing), but a deployment that would rather not carry any of the older AES-128 (Fernet) ciphertext going forward can upgrade every remaining one in a single optional pass:
docker compose exec web python scripts/reencrypt_secrets.py --dry-run # see what would change
docker compose exec web python scripts/reencrypt_secrets.py # actually upgrade itRe-encrypts under the same ENCRYPTION_KEY โ this is a format upgrade,
not a key rotation, and it's safe to run repeatedly (idempotent).
scripts/reset_account.py (console-only, same idea as debcontrol's) resets
a password and/or disables TOTP for an account already locked out of the
web UI:
docker compose exec web python scripts/reset_account.py --username adminNon-interactively, set HONEYPOTSHELF_RESET_PASSWORD in the environment
instead of being prompted (same reasoning as create_admin.py's
HONEYPOTSHELF_ADMIN_PASSWORD โ it never shows up in a process listing the
way a --password flag would).
See Development.md for running the test suite, linting/type-checking, and adding a migration.