Help Center
Server Configuration

Server Configuration

vaultctl is configured entirely through environment variables, all prefixed with VAULTCTL_. This page documents every variable the server reads, key rotation procedures, and a production readiness checklist.

A complete, commented template ships in the repository as .env.example (opens in a new tab). Copy it to .env and fill in the secrets.

Required in production

When VAULTCTL_ENV=production, the server refuses to start unless every one of these is set (fail-closed). In development they fall back to insecure defaults so you can boot quickly.

VariableDescription
VAULTCTL_BASE_URLPublic base URL of the deployment, e.g. https://vault.example.com. Used for links and cookie scoping.
VAULTCTL_DB_PASSWORDPostgreSQL password.
VAULTCTL_JWT_SECRET_CURRENTSecret for signing JWT access/refresh tokens. Use a 64-byte random value.
VAULTCTL_DATA_ENCRYPTION_KEYAES-256 key (32 bytes) for server-side encryption of TOTP secrets and password hints.
VAULTCTL_SERVER_PEPPERSecret for server-side Argon2id re-hashing of auth hashes (32 bytes).
VAULTCTL_ENUMERATION_PEPPERSecret used to derive deterministic fake KDF params for unknown emails (32 bytes), defeating account enumeration.
⚠️

Generate every secret from a cryptographically secure source. Never reuse a secret across environments, and never commit one to source control.

openssl rand -base64 32   # 32-byte values (data key, peppers)
openssl rand -base64 64   # JWT secret (recommended)

Server

VariableDefaultDescription
VAULTCTL_PORT8080TCP port the server listens on.
VAULTCTL_HOST0.0.0.0Bind address.
VAULTCTL_BASE_URL(required in prod)Public base URL.
VAULTCTL_ENVdevelopmentdevelopment or production. Production enables fail-closed secret checks and strict cookie/security defaults.

Database

VariableDefaultDescription
VAULTCTL_DB_HOSTlocalhostPostgreSQL host.
VAULTCTL_DB_PORT5432PostgreSQL port.
VAULTCTL_DB_NAMEvaultctlDatabase name.
VAULTCTL_DB_USERvaultctlDatabase user.
VAULTCTL_DB_PASSWORD(required in prod)Database password.
VAULTCTL_DB_SSL_MODErequireOne of disable, require, verify-ca, verify-full.
VAULTCTL_DB_SSL_INSECURE_OKfalseMust be true to allow VAULTCTL_DB_SSL_MODE=disable in production. Set automatically by the bundled compose where Postgres lives on a private bridge network; leave false for any cross-host deploy.

vaultctl connects with discrete VAULTCTL_DB_* variables, not a single DATABASE_URL connection string.

JWT signing keys

Durations use Go's format (15m, 1h, 24h, 168h), not 7d.

VariableDefaultDescription
VAULTCTL_JWT_SECRET_CURRENT(required in prod)Active signing secret.
VAULTCTL_JWT_SECRET_NEXT(none)Next secret for zero-downtime rotation.
VAULTCTL_JWT_KID_CURRENTk1Key ID written into JWT headers for rotation tracking.
VAULTCTL_JWT_ACCESS_TTL15mAccess-token lifetime.
VAULTCTL_JWT_REFRESH_TTL168hRefresh-token lifetime (168h = 7 days).

Data encryption key

VariableDefaultDescription
VAULTCTL_DATA_ENCRYPTION_KEY(required in prod)Active AES-256 key for server-side field encryption.
VAULTCTL_DATA_ENCRYPTION_KEY_NEXT(none)Next key for zero-downtime rotation.

Server peppers

VariableDefaultDescription
VAULTCTL_SERVER_PEPPER(required in prod)Pepper for server-side auth-hash re-hashing. Rotating it invalidates all stored auth hashes, so treat it as permanent.
VAULTCTL_ENUMERATION_PEPPER(required in prod)Pepper for fake KDF params on unknown emails.

Security

VariableDefaultDescription
VAULTCTL_REGISTRATION_MODEinviteopen (anyone can register), invite (invite only), or disabled. The very first registration is always allowed and becomes the owner, regardless of this setting.
VAULTCTL_REQUIRE_2FAfalseRequire every user to enrol a second factor.
VAULTCTL_MAX_LOGIN_ATTEMPTS5Failed logins before an account is locked.
VAULTCTL_LOCKOUT_DURATION15mHow long an account stays locked.
VAULTCTL_RATE_LIMIT_RPM60Global per-IP request rate limit (per minute).
VAULTCTL_AUTH_RATE_LIMIT_PER_EMAIL5Max auth attempts per email within the window.
VAULTCTL_AUTH_RATE_LIMIT_WINDOW15mWindow for per-email auth rate limiting.
VAULTCTL_AUTH_GLOBAL_ALERT_THRESHOLD1000Failed-auth count across the server that triggers an alert log line (credential-stuffing signal).
VAULTCTL_TRUSTED_PROXIESloopback + RFC1918Comma-separated CIDRs trusted to set X-Forwarded-For. An empty value disables XFF entirely. Set to your proxy's IP/CIDR behind a public-IP load balancer.
VAULTCTL_STEP_UP_MAX_AGE5mHow long a step-up (re-auth) confirmation is honoured for sensitive operations.
VAULTCTL_CORS_ALLOWED_ORIGINS(none)Comma-separated extra origins allowed for CORS (for example, a separately hosted browser-extension origin).
VAULTCTL_MIN_PASSWORD_LENGTH10Minimum master-password length enforced at registration and password change.

Retention

VariableDefaultDescription
VAULTCTL_TRASH_RETENTION_DAYS30Days a trashed item is kept before permanent deletion.
VAULTCTL_BACKUP_RETENTION_DAYS90Days a backup record is retained.

Attachments

Items can carry encrypted file attachments. Each file is sealed in the browser with its own AES-256-GCM key, which is then wrapped under the vault key, so the server only ever stores ciphertext and wrapped key material. Blobs are written to a filesystem store (no S3/MinIO dependency).

VariableDefaultDescription
VAULTCTL_ATTACHMENTS_DIR/data/attachmentsDirectory for the encrypted blob store. Back it up alongside Postgres.
VAULTCTL_ATTACHMENT_MAX_BYTES26214400 (25 MiB)Maximum size of a single uploaded file (ciphertext).
VAULTCTL_ATTACHMENT_VAULT_QUOTA_BYTES524288000 (500 MiB)Total attachment storage allowed per vault.
⚠️

The attachments directory holds encrypted blobs whose keys live only in the Postgres attachments table. Losing one without the other orphans your files - back up the attachments volume and the database together. The bundled compose files mount a named attachments-data volume at /data/attachments.

Logging

VariableDefaultDescription
VAULTCTL_LOG_LEVELinfodebug, info, warn, or error.
VAULTCTL_LOG_FORMATjsonjson or text.
VAULTCTL_LOG_IP_PRECISIONcoarsecoarse (network-truncated), full, or none.
VAULTCTL_LOG_REDACT_FIELDSsee belowComma-separated JSON fields scrubbed from logs. Default: authHash,password,refresh_token,api_key,totp,masterKey,stretchedKey.

CLI environment

The vaultctl CLI reads its own variables, separate from the server:

VariableDescription
VAULTCTL_SERVERBase URL of the server to talk to.
VAULTCTL_API_KEYAPI key for non-interactive auth (CI, scripts).
VAULTCTL_ACTIVE_VAULTDefault vault ID for commands that take one.
VAULTCTL_INSECURE_SKIP_VERIFYSet truthy to skip TLS verification (development only).

Upgrades & migrations

Database migrations are embedded in the binary but are not applied automatically on server start. After pulling a new image or binary that adds a migration, run the migration when you recreate the container:

# One-off against the live database
docker exec vaultctl-api /usr/local/bin/vaultctl migrate up
 
# or a fresh one-shot container with the same environment
docker compose run --rm vaultctl migrate up

Migrations are idempotent - running migrate up with nothing pending is a no-op.

Key rotation

vaultctl supports zero-downtime rotation for JWT secrets and the data encryption key using a "current + next" pattern.

JWT secret rotation

Set the next secret

Add VAULTCTL_JWT_SECRET_NEXT with a newly generated secret and bump VAULTCTL_JWT_KID_CURRENT.

VAULTCTL_JWT_SECRET_CURRENT=<old-secret>
VAULTCTL_JWT_SECRET_NEXT=<new-secret>
VAULTCTL_JWT_KID_CURRENT=k2

Restart the server

New tokens are signed with the next secret while tokens signed with the current secret still validate.

Wait for old tokens to expire

Wait at least VAULTCTL_JWT_ACCESS_TTL + VAULTCTL_JWT_REFRESH_TTL (default 7 days, 15 minutes).

Promote the new secret

Move the new secret to VAULTCTL_JWT_SECRET_CURRENT and remove VAULTCTL_JWT_SECRET_NEXT.

VAULTCTL_JWT_SECRET_CURRENT=<new-secret>
# VAULTCTL_JWT_SECRET_NEXT removed
VAULTCTL_JWT_KID_CURRENT=k2

Restart the server

Rotation complete.

Data encryption key rotation

The same pattern applies to VAULTCTL_DATA_ENCRYPTION_KEY and VAULTCTL_DATA_ENCRYPTION_KEY_NEXT. The server can decrypt with either key and re-encrypts with the new key on the next write.

Set the next key

VAULTCTL_DATA_ENCRYPTION_KEY=<old-key>
VAULTCTL_DATA_ENCRYPTION_KEY_NEXT=<new-key>

Restart and let writes re-encrypt

The server uses the new key for all new encryptions and decrypts with either key.

Promote the new key

VAULTCTL_DATA_ENCRYPTION_KEY=<new-key>
# VAULTCTL_DATA_ENCRYPTION_KEY_NEXT removed

Production checklist

CheckDetails
VAULTCTL_ENV=productionEnables fail-closed secret checks and strict security defaults.
All secrets set and uniqueThe six required variables above, each generated with openssl rand, never reused across environments.
Database uses SSLKeep VAULTCTL_DB_SSL_MODE=require (or stricter). Only use disable on a trusted private network, and then also set VAULTCTL_DB_SSL_INSECURE_OK=true.
TLS termination configuredPut a reverse proxy (Caddy, nginx) with a valid certificate in front.
VAULTCTL_TRUSTED_PROXIES matches your proxySo client IPs resolve correctly for rate limiting and logs.
Rate limits reviewedTune VAULTCTL_RATE_LIMIT_RPM and VAULTCTL_AUTH_RATE_LIMIT_PER_EMAIL for expected traffic.
Registration locked downVAULTCTL_REGISTRATION_MODE=invite (or disabled) once the owner account exists.
Backups scheduledSee Backup & Restore. Back up the database and the attachments directory together.
Firewall in placeExpose only the HTTPS port; block direct access to VAULTCTL_PORT.