Skip to content

Security

The Settings → Security tab consolidates the runtime security controls that don’t fit cleanly under another tab. Every read and every mutation goes through verifyAuthToken (centralized after the v0.1.3 audit).

Every successful admin login records a row in vaultbase_admin_sessions:

CREATE TABLE vaultbase_admin_sessions (
jti TEXT PRIMARY KEY,
admin_id TEXT NOT NULL,
admin_email TEXT NOT NULL,
issued_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
ip TEXT, -- only set when trusted_proxies is configured
user_agent TEXT
);

The Security tab lists currently-active sessions (jti not yet expired and not revoked). Each row exposes a Revoke button that inserts the jti into vaultbase_token_revocations; the next request carrying that token gets a uniform 401.

The Force logout all button bumps password_reset_at = unixepoch() on every row in vaultbase_admin. verifyAuthToken rejects any token whose iat precedes the principal’s password_reset_at, so this single SQL UPDATE kills every existing admin JWT in one shot — no per-jti DB write.

The button signs you out too (your token is now older than the reset timestamp). Sign back in with your password.

Off by default. Two settings drive it:

auth.lockout.max_attempts (default 0 = off)
auth.lockout.duration_seconds (default 900, min 60)

When a login fails, vaultbase writes one row per principal:

  • email:<lowercased-email>
  • ip:<client-ip> (only if security.trusted_proxies is configured)

Either key reaching max_attempts within the window locks the principal out — the next login attempt returns 429 Too many failed attempts. Try again later. The lockout gate runs before the password verify, so a successful attempt by a different account from the same IP doesn’t leak signal.

A successful login clears both keys. Old failure rows are pruned on a periodic tick.

A safety lever for X-Forwarded-For parsing — both reverse-proxy and defensive-default behaviors live here:

  • Empty + no VAULTBASE_TRUSTED_PROXIES env → vaultbase ignores X-Forwarded-For entirely.
  • Setting filled → that CIDR list is used for real-IP extraction.
  • Env-only fallback works if the setting is left blank.

Affects: rate limiting, audit log IP column, brute-force IP keying.

Read-only SHA-256 first 8 bytes of the JWT signing secret and the AES encryption key, rendered in the Security tab. Used to:

  • Confirm a fleet of vaultbase instances shares the same secrets (otherwise JWTs from one host won’t verify on another, encrypted fields written on one are unreadable on another).
  • Spot accidental key rotation between deploys.

VAULTBASE_ENCRYPTION_KEY missing surfaces an explicit warning:

No VAULTBASE_ENCRYPTION_KEY set — fields marked encrypted store plaintext.

The fingerprint is crypto.subtle.digest("SHA-256", utf8(secret)) then the first 16 hex characters (8 bytes). Collision-resistant for fingerprinting purposes; doesn’t reveal the secret.

The Security tab renders the headers vaultbase sends per response, split between API responses and admin UI / non-API responses (CSP applies to the latter only — JSON responses don’t need it).

These come from core/sec.ts → securityHeaders({ isApi }). Common ones:

  • Strict-Transport-Security
  • Content-Security-Policy
  • X-Frame-Options: DENY
  • Referrer-Policy: no-referrer
  • X-Content-Type-Options: nosniff

The preview is read-only. To customize: edit core/sec.ts and rebuild. Most operators won’t need to.

The default Content-Security-Policy is restrictive:

default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
img-src 'self' data: blob: https://*.tile.openstreetmap.org;
style-src 'self' 'unsafe-inline';
connect-src 'self' ws: wss:;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'

The single externally-allowed source is https://*.tile.openstreetmap.org, needed by the admin’s Leaflet preview when a collection has a geoPoint field. All other admin assets (marker icons, fonts, scripts) ship inline or from 'self'.

If you front vaultbase with a stricter outer CSP layer (Cloudflare Page Rules, an organization-wide WAF policy, etc.), mirror the same img-src allowance there or geoPoint maps will render as blank tiles. Disable map tiles entirely by removing the entry from core/sec.ts and rebuilding — markers and the admin chrome continue to work, only the OSM raster background drops out.

Programmatic access for the same data:

GET /api/v1/admin/security/sessions[?activeOnly=0]
DELETE /api/v1/admin/security/sessions/:jti
POST /api/v1/admin/security/force-logout-all
GET /api/v1/admin/security/fingerprints
GET /api/v1/admin/security/headers-preview

All require an admin JWT. Mutations are picked up by the audit log (security.* action labels).