Skip to content

Tune Security Settings

This page covers the security-related toggles and environment variables operators tune most often: CSRF, session cookies, HTTPS and HSTS, and token encryption at rest.

How does CSRF protection work?

The SPA uses a double-submit cookie. The server-side validation compares the x-csrf-token header against the cookie value. The SPA fetches the token once from GET /api/auth/csrf-token on startup and stores it in a module-level memory cache; mutations send it in the x-csrf-token header.

Variable Default Notes
CSRF_HTTP_ONLY true Default changed in v0.35. The SPA does not read the cookie directly; the httpOnly flag removes the token from the XSS-accessible cookie surface. Set false only for a custom client that needs document.cookie access.
CSRF_SAME_SITE strict Cookie SameSite policy
CSRF_SECURE true Set false only for plain HTTP development
CSRF_COOKIE_NAME bt_csrf_token Useful for multi-app deployments

bt_session is always HTTP-only and SameSite strict. Secure behavior is selected in this order:

  1. Explicit COOKIE_SECURE=true|false
  2. Explicit HTTPS=true|false
  3. Whether the current request appears to use HTTPS

How do I enable HTTPS and HSTS?

Set HTTPS=true in a production HTTPS deployment to enable HSTS. When TLS terminates at a reverse proxy, forward X-Forwarded-Proto: https.

How are secrets encrypted at rest?

SimpleFIN secrets, SMTP passwords, OIDC client secrets, TOTP secrets and recovery codes, push notification tokens, and login-history IP/UA/location columns are all encrypted at rest with AES-256-GCM (HKDF-SHA-256 with the bill-tracker-token-encryption-v1 info label).

By default, BillTracker generates a stable 48-byte key on first startup and persists it in the settings table under _auto_encryption_key. For an operator-managed override, set a value of at least 32 bytes:

TOKEN_ENCRYPTION_KEY=<strong-random-secret>

Generate one with:

node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"

Database key vs env key trade-off

The auto-generated database key is co-located with the ciphertext it protects — anyone with database read access has everything needed to decrypt. Set TOKEN_ENCRYPTION_KEY in production to keep the key separate from the data.

When TOKEN_ENCRYPTION_KEY is set, all DB-key-encrypted secrets are transparently re-encrypted with the env key in a single transaction on first startup (prefix e2:; already-e2: values are skipped). The Admin → Bank Sync card surfaces which key source is currently active.

Keep an override stable across restarts and protect it separately from database backups.

See also