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).
Active admin sessions
Section titled “Active admin sessions”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.
Force-logout-all
Section titled “Force-logout-all”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.
Brute-force lockout
Section titled “Brute-force lockout”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, otherwiseemail:addr:<email as typed>(auth-collection users get their ownuser:<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.
Trusted proxies
Section titled “Trusted proxies”A safety lever for X-Forwarded-For parsing — both reverse-proxy and
defensive-default behaviors live here:
- Empty + no
VAULTBASE_TRUSTED_PROXIESenv → vaultbase ignoresX-Forwarded-Forentirely. - 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.
Secrets fingerprints
Section titled “Secrets fingerprints”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_KEYset — fields markedencryptedstore 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.
Security headers preview
Section titled “Security headers preview”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-SecurityContent-Security-PolicyX-Frame-Options: DENYReferrer-Policy: no-referrerX-Content-Type-Options: nosniff
The preview is read-only. To customize: edit core/sec.ts and rebuild.
Most operators won’t need to.
CSP allowances
Section titled “CSP allowances”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.
Password policy
Section titled “Password policy”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 reachapi.pwnedpasswords.com.password.require_upper/_lower/_digit/_symbol— character classes, if your policy requires them.
Trust boundaries
Section titled “Trust boundaries”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. readandmcp:readtokens 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.
REST API
Section titled “REST API”Programmatic access for the same data:
GET /api/v1/admin/security/sessions[?activeOnly=0]DELETE /api/v1/admin/security/sessions/:jtiPOST /api/v1/admin/security/force-logout-allGET /api/v1/admin/security/fingerprintsGET /api/v1/admin/security/headers-previewAll 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).