4.2 KiB
Security
AvianVisitors is designed for a Raspberry Pi listening on a trusted home network. If anyone other than you can reach the Pi at the network layer — a shared apartment LAN, dorm wifi, an open hotspot, a forwarded port — read this first.
What the default install leaves open
The public-facing endpoints (the collage HTML, the read-only birdnet-api.php aggregations, cutout/spectrogram/recording PHPs, the wiki proxy) are intentionally unauthenticated. They serve display data and a few thousand bytes of JSON each. Treat them as public.
The admin endpoints are not safe to expose to untrusted networks:
/avian/api/config.php— reads and writes a whitelisted slice ofbirdnet.confand restarts the analyzer when settings change./avian/api/birdnet-status.php— returns system metrics (CPU, mem, disk, uptime), service state, journalctl output, and accepts?action=restartto bounce a whitelisted unit.
By default these are reachable on the same http://birdnet.local/ listener as everything else. On a shared LAN, anyone on the same network can hit them. The frontend's "lock screen" is cosmetic without a Caddy basic_auth gate in front.
What can go wrong
The upstream BirdNET-Pi installer drops a caddy ALL=(ALL) NOPASSWD: ALL sudoers rule (see scripts/install_services.sh). PHP-FPM runs as the caddy user. That means anything reachable from a PHP shim has full root via sudo.
The shims here use a tight allowlist for systemctl restart and journalctl -u, and they validate every input against a whitelist + escapeshellarg. The string fields written to birdnet.conf are escaped against $, backtick, backslash, and double-quote, and rejected outright if they contain anything outside [A-Za-z0-9 _.,'-]. So even on a flat-trust LAN, the public-by-default admin endpoints don't currently offer an obvious path to RCE.
But the surface is wide. New shims, future config keys, or a regression in quote_val would be enough. The safe default is: lock the admin endpoints down on every install that isn't a fully trusted home network.
Locking it down
Add a Caddy basic_auth block in front of the admin shims. Example, dropped into the bottom of /etc/caddy/Caddyfile (or a snippet in conf.d/):
basicauth /avian/api/config.php* /avian/api/birdnet-status.php* {
birdnet $2a$14$... # caddy hash-password --plaintext '<your-password>'
}
Then in /etc/avian/env (or your php-fpm pool env block):
AV_REQUIRE_AUTH=1
The env flag makes the PHP shims refuse to respond unless an Authorization header reached them — which it will, when basic_auth in Caddy passes the request through, and won't, when basic_auth rejects it.
Reload Caddy and php-fpm:
sudo systemctl reload caddy
sudo systemctl reload "$(systemctl list-unit-files 'php*-fpm.service' --no-legend | awk '{print $1; exit}')"
After that:
- The collage and read-only APIs still serve to anyone.
- The admin overlay's settings/system/logs/tools panels prompt for the password the first time the drawer opens, then the browser caches it.
curl http://birdnet.local/avian/api/birdnet-status.php?action=diagreturns 401 without credentials.
If you also need the Pi reachable from outside the home network (Cloudflare Tunnel, reverse proxy, port-forward), the basic_auth gate above is the bare minimum — consider also limiting /avian/api/config.php and /avian/api/birdnet-status.php to specific source IPs at the Caddy layer, or moving them onto a separate listener that's only bound to a Tailscale interface.
What I'd love a contribution on
- Auto-generating a unique basic_auth password during
install_services.sh, writing it to a file the user reads once after install, and emitting the matching Caddy block — so the secure default is the path of least resistance. - A smaller-radius sudoers replacement that drops the blanket
caddy ALL=(ALL) NOPASSWD: ALLrule and grants only the specific commands AvianVisitors actually needs.
Reporting
If you find a security issue, open a GitHub issue with the security label or email teddy@theodore.net. There's no bug bounty — this is a side project — but I'll prioritize a fix and credit you in the release notes.