Skip to content

Security

Active sessions, brute-force lockout, trusted proxies, secrets fingerprints, security headers — visible from Settings → 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, -- client IP (X-Forwarded-For only via a trusted proxy)
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.

On by default. Two settings drive it:

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

Each sign-in attempt is charged, before the password is checked, against two keys:

  • the account — email:id:<admin id> when the email matches an admin, otherwise email:addr:<email as typed> (auth-collection users get their own user:<collection id>:… keys)
  • the client IP — ip:<client-ip>, with a limit of 5 × max_attempts, since one address often fronts many people

Either key reaching its limit within the window locks the principal out — the next login attempt returns 429 Too many failed attempts. Try again later. The client IP is the connecting peer, or the X-Forwarded-For client when the peer is a trusted proxy.

A successful login clears the account’s failures and withdraws that attempt’s charge on the IP; earlier IP failures are kept. 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.

X-Forwarded-For is only honoured when the connecting peer is in the list, and it is walked from the right, skipping the listed proxies — the leftmost entry is whatever the client chose to send.

Affects: rate limiting, audit log IP column, request logs, brute-force IP keying, realtime per-address limits, file-token IP binding.

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.

When VAULTBASE_ENCRYPTION_KEY is unset, Vaultbase generates a key into <dataDir>/.encryption-key on first start and uses it, so an encryption key fingerprint is always shown. The tab warns only if no key is available at all:

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 (a CDN rule, 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.

Defaults: at least 12 characters, no character-class rules (in line with NIST SP 800-63B, which favours length and breach checks over complexity). Tighten them under Settings → Password policy or with password.* settings (see the settings reference):

  • password.min_length — raise the minimum.
  • password.hibp_check = 1 — refuse passwords found in known breaches, using the Have I Been Pwned range API. Only the first 5 characters of the password’s SHA-1 leave the server (k-anonymity). Off by default because it makes an outbound request per password change; turn it on unless your server can’t reach api.pwnedpasswords.com.
  • password.require_upper / _lower / _digit / _symbol — character classes, if your policy requires them.

What Vaultbase treats as trusted, so you know what to protect:

  • Admins and admin-scoped tokens (admin, mcp:admin) can do anything, including running code on the server through hooks, routes, jobs and queue workers, which execute unsandboxed in the server process.
  • read and mcp:read tokens read all data, ignoring collection rules.
  • Integration URLs set in Settings (SMTP host, storage endpoint, OAuth / OIDC endpoints, push providers) are trusted and not passed through the egress guard, since they often point at internal services on purpose. Webhooks and code in hooks use the guard.

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; API tokens are not accepted on admin REST routes (automation goes through MCP or an admin session). Mutations are picked up by the audit log (security.* action labels).