This document describes Keywarden's security features, architecture, and best practices.
- Passwords are hashed with bcrypt at the default cost factor (10)
- Password policy is configurable by the owner (see below)
- Accounts are locked after configurable failed login attempts
The owner can configure password requirements in Admin Settings:
| Setting | Default | Description |
|---|---|---|
| Minimum length | 8 | Minimum number of characters |
| Require uppercase | Yes | At least one uppercase letter (A-Z) |
| Require lowercase | Yes | At least one lowercase letter (a-z) |
| Require digit | Yes | At least one number (0-9) |
| Require special character | No | At least one non-alphanumeric character |
The policy is enforced on:
- User registration (invitation acceptance)
- Password changes (manual and forced)
- Admin password resets
After a configurable number of failed login attempts (default: 5), an account is locked for a configurable duration (default: 15 minutes).
| Setting | Default |
|---|---|
| Lockout threshold | 5 attempts |
| Lockout duration | 15 minutes |
Setting lockout attempts to 0 disables the lockout feature.
Admins can manually unlock accounts from the user management page.
Admins can flag any user to require a password change. The user will be redirected to the password change page on every request until they set a new password. This is automatically enabled for:
- Newly created accounts
- Accounts where an admin reset the password
Keywarden supports TOTP (Time-based One-Time Password) for MFA, compatible with:
- Google Authenticator
- Authy
- Microsoft Authenticator
- Any RFC 6238 compliant app
- Algorithm: HMAC-SHA1
- Code length: 6 digits
- Period: 30 seconds
- Clock tolerance: ±1 time step (allows 30 seconds of clock skew)
- Secret generation: 20 bytes of cryptographic random data
- Secret encoding: Base32 (unpadded)
The owner can enable system-wide MFA enforcement in Admin Settings. When enabled:
- Users without MFA are redirected to the MFA setup page on every request
- Users cannot disable MFA while enforcement is active
- The owner can always access Admin Settings (even without MFA) to prevent lockout
All SSH private keys stored in the database are encrypted with AES-256-GCM:
- The
KEYWARDEN_ENCRYPTION_KEYis hashed with SHA-256 → 32-byte AES key - A random 12-byte nonce is generated for each encryption operation
- Plaintext is encrypted with AES-256-GCM (provides confidentiality + integrity)
- Result:
nonce || ciphertext || GCM-tag→ base64-encoded → stored in DB
Critical: If
KEYWARDEN_ENCRYPTION_KEYis changed or lost, all stored private keys become permanently inaccessible.
Database exports are encrypted with a user-provided password using the same AES-256-GCM scheme. The password is required for both export and import.
Keywarden implements the Double-Submit Cookie pattern:
- A
_csrfcookie is set on every request (32 bytes, hex-encoded, 64 chars) - On state-changing methods (POST, PUT, DELETE, PATCH), the request must include a matching token as:
- A form field named
_csrf, or - An
X-CSRF-Tokenrequest header
- A form field named
- Tokens are compared using constant-time comparison to prevent timing attacks
The cookie is not HttpOnly (JavaScript must read it to inject into forms), but is:
SameSite=StrictSecurewhen HTTPS is enabled- Expires after 24 hours
Every response includes:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options |
DENY |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME sniffing |
Referrer-Policy |
strict-origin-when-cross-origin |
Controls Referer leakage |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=() |
Disables unused APIs |
Content-Security-Policy |
See below | Restricts resource loading |
X-Permitted-Cross-Domain-Policies |
none |
Blocks cross-domain policy files |
Cache-Control |
no-store, no-cache, must-revalidate, private |
Prevents caching of authenticated pages |
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self'
Login endpoints (POST /login, POST /login/mfa) are rate-limited per IP address.
- Default limit: 10 attempts per IP per minute
- Algorithm: Fixed-window counter
- Response when exceeded: HTTP 429 (Too Many Requests)
- Configuration:
KEYWARDEN_RATE_LIMIT_LOGIN(0 = disabled)
A background goroutine cleans up expired rate limit entries every 5 minutes.
HTTP responses are compressed using gzip for clients that send Accept-Encoding: gzip. Only compressible content types are compressed (HTML, CSS, JS, JSON, SVG). Already-compressed formats (woff2, images) are passed through unchanged.
The middleware uses a sync.Pool of gzip writers for efficient memory reuse.
Request bodies are limited to prevent denial-of-service via large uploads.
- Default limit: 10 MB (
10485760bytes) - Response when exceeded: HTTP 413 (Request Entity Too Large)
- Configuration:
KEYWARDEN_MAX_REQUEST_SIZE(0 = no limit)
When Keywarden runs behind a reverse proxy, the real client IP must be extracted from X-Forwarded-For or X-Real-IP headers. However, these headers can be spoofed by clients.
Set KEYWARDEN_TRUSTED_PROXIES to the CIDR range(s) of your reverse proxy:
KEYWARDEN_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12In strict mode:
- Proxy headers are only trusted when the direct TCP peer is from a trusted network
X-Forwarded-Foris walked right-to-left, and the first non-trusted IP is used- This prevents client-side IP spoofing
If KEYWARDEN_TRUSTED_PROXIES is not set, Keywarden trusts all proxy headers unconditionally. A warning is logged at startup:
WARN: KEYWARDEN_TRUSTED_PROXIES not set – proxy headers (X-Forwarded-For) are trusted unconditionally
- Session tokens: 32 bytes, cryptographically random, hex-encoded
- Cookie name:
keywarden_session - Cookie flags:
HttpOnly— Not accessible via JavaScriptSameSite=Strict— Prevents CSRF from external sitesSecure— Only over HTTPS (when enabled)MaxAge=86400— 24 hours
- Sessions stored in-memory (not persisted across restarts)
- Configurable inactivity timeout (default: 60 minutes)
- Background cleanup runs every minute
When deploying keys to servers, Keywarden:
- Uses the system master key (Ed25519) for SSH authentication
- Connects with a 10-second timeout
- Does not verify host keys (
InsecureIgnoreHostKey) — this is a known limitation
Note: Host key verification is not yet implemented. This means Keywarden is susceptible to man-in-the-middle attacks during SSH connections. Only use Keywarden in trusted network environments.
- Change the default secrets: Set unique values for
KEYWARDEN_SESSION_KEYandKEYWARDEN_ENCRYPTION_KEY - Use HTTPS: Run behind a reverse proxy with TLS termination
- Configure trusted proxies: Set
KEYWARDEN_TRUSTED_PROXIESfor accurate IP logging - Enable secure cookies: Set
KEYWARDEN_SECURE_COOKIES=true(auto-derived from HTTPS base URL) - Enable MFA enforcement: Require all users to use two-factor authentication
- Use strong passwords: Configure a strict password policy
- Regular backups: Export encrypted backups regularly
- Network isolation: Restrict access to Keywarden and managed servers to trusted networks
- Keep the encryption key safe: Back up
KEYWARDEN_ENCRYPTION_KEYsecurely — losing it means losing all private keys - Monitor the audit log: Review login activity and deployment actions regularly
- Enable key enforcement: Use enforce mode to ensure only Keywarden-managed keys exist on your servers
Keywarden includes an enforced key management feature inspired by Bastillion. When enabled, a background worker periodically connects to all managed servers and ensures that only authorized SSH keys are present in authorized_keys files.
- The enforcement worker runs at a configurable interval (default: 15 minutes)
- For each managed server and system user, it reads the current
authorized_keys - It compares the keys against the desired state derived from:
- All active access assignments (desired_state = "present")
- All active cron jobs (temporary access that has not yet expired)
- All direct key deployments (via the Deploy page)
- The system master key (always authorized)
- Unauthorized keys (not managed by Keywarden) are detected
- Depending on the mode, unauthorized keys are either logged or removed
| Mode | Behavior |
|---|---|
| Disabled | No enforcement checks (default) |
| Monitor | Detects unauthorized keys and logs them in the audit log, but does not remove them |
| Enforce | Detects unauthorized keys and removes them automatically, replacing authorized_keys with only the authorized set |
Key enforcement is configured in Admin Settings → Key Enforcement:
- Enforcement Mode: Disabled / Monitor / Enforce
- Check Interval: How often the worker checks servers (1–1440 minutes)
- Run Now: Trigger an immediate enforcement check
All enforcement actions are recorded in the audit log:
| Action | Description |
|---|---|
enforcement_run |
An enforcement cycle completed (with summary) |
enforcement_drift |
Unauthorized keys detected on a server |
enforcement_applied |
Unauthorized keys were removed from a server |
enforcement_failed |
An enforcement action failed (connection error, etc.) |
enforcement_settings_changed |
Enforcement settings were modified |
- The system master key is always considered authorized and will never be removed
- Enforcement covers all system users that have active access assignments, cron jobs, or direct deployments in Keywarden
- The server's admin user (used for SSH connections) is always checked
- Enforcement requires the system master key to be deployed on target servers
- In enforce mode,
authorized_keysis atomically replaced (write to temp file, then move) - Manual runs can be triggered from the Admin Settings page