You are viewing archived documentation for v0.48. Go to latest →

Security

How BirdNET-NG protects tenant data, and the hardening applied before the platform was opened to unknown users (vhub-0.47.0).

Trust model

  • The application owns all authentication and authorization. The reverse proxy terminates TLS and routes; it does not gate app routes (vhub-0.46).
  • Tenants are isolated by tenant_id on every data table. Every counting or species-facing endpoint applies the tenant scope server-side; an omitted tenantId means "the caller's tenant", not "all tenants" (platform admins excepted).
  • Roles: viewer < member < admin < owner, per tenant; plus a global platform admin. Routes that take a target tenant from the body re-anchor on that tenant, so an admin of one tenant cannot act on another.

Sessions and tokens

  • Session JWTs are httpOnly, secure, SameSite=Lax, host-only cookies, signed HS256 (pinned on verify), 7-day expiry.
  • Each JWT carries the token_version it was minted with. A password change or reset bumps users.token_version, so every older session stops resolving immediately, and remembered MFA devices are dropped at the same time.
  • Password reset tokens: 256-bit random, SHA-256 at rest, single-use, 60-minute expiry. Invite tokens: 192-bit random, bound to an email when one is given (only that address can redeem), TTL clamped to a week. Share links: truncated HMAC of the detection id, promoted detections only, compared in constant time.
  • API keys: 256-bit random, SHA-256 at rest. Tenant keys expire after a year; satellite keys are per-device and pinned to their satellite.
  • MFA: TOTP (AES-256-GCM at rest) + WebAuthn passkeys + bcrypt-hashed backup codes.

Abuse resistance

  • @fastify/rate-limit guards /api/auth/*, tenant creation and invites. request.ip is derived from a bounded proxy hop count, so a spoofed X-Forwarded-For cannot reset the limiter or forge audit rows.
  • Login is uniform: a locked account and a wrong password return the same response, and the unknown-user path burns the same bcrypt work as a real one, so registered emails cannot be enumerated.
  • Self-registration and tenant creation are platform-setting toggles; invite links bypass the registration toggle by design.

Browser hardening

  • Security headers (HSTS, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy) are set at the edge (Traefik), by nginx for the SPA, and by @fastify/helmet for the API. The API sends a strict CSP; the SPA ships a Report-Only CSP that becomes enforcing once validated.
  • CORS and the WebSocket upgrade accept only the app origin and the Capacitor WebView origins, never a reflected origin.

Outbound requests (SSRF)

User-supplied URLs (webhook alert channels, scheduled-export delivery) are validated before every send: https only, the hostname is DNS-resolved and rejected if it maps to a private, loopback or link-local address (or is a bare, non-qualified name), and redirects are not followed. This keeps a webhook from reaching internal services (postgres, redis, minio, the cloud metadata endpoint).

Ingest integrity

The MQTT broker pins each device to its own topic, but the tenant segment is a wildcard, so the hub does not trust the message body: tenant and satellite identity come from the topic and the database, and identifiers are validated before a storage key is built. A device cannot write into another tenant or forge an object key.

Storage

  • MinIO buckets are private; no bucket is public-read and no presigned URLs are issued. Every object is streamed through an authenticated hub route that checks the object's tenant first.
  • Uploaded images are validated by content (magic bytes), stored and served with the sniffed type plus nosniff and Content-Disposition. CSV exports neutralise spreadsheet formula injection.

Mobile app

  • Release APKs are signed with an offline keystore kept out of the repository; the build produces a non-debuggable release build with backups disabled, so device backups do not expose the stored hub key or MQTT password.
  • The in-app updater downloads only over https from the configured hub, follows no redirects, and verifies the APK's SHA-256 before handing it to the system installer.

Operator checklist before going public

  • Set strong values for POSTGRES_PASSWORD, REDIS_PASSWORD, MINIO_ROOT_PASSWORD, MQTT_ADMIN_PASSWORD, MQTT_HUB_PASSWORD and JWT_SECRET (32-byte random). These reach only the internal Docker networks, but should not be defaults.
  • Set MFA_ENCRYPTION_KEY (32-byte hex) so TOTP and webhook secrets are encrypted at rest.
  • Generate the Android release keystore once and back it up with the volume tree; losing it means installed apps can never be updated again.
  • Decide the registration posture: invite-only, or self-registration on.