Skip to main content

Cockpit Configuration

The Cockpit is configured entirely through environment variables, read once at startup (a .env file is supported for local use). On-premise, these variables are resolved from OpenBao via the External Secrets Operator and injected into the pod as environment variables — never baked into the image and never committed to Git.

Never commit real secrets

All values shown here are placeholders. Secrets (database DSNs, signing and encryption keys, client secrets, tokens) must never be hardcoded or committed to Git — store them in your secret engine and inject them at deploy. See Secrets sourcing and Secret Manager Integration.

Secrets sourcing​

On-premise, the environment-specific configuration is held in OpenBao and materialized at deployment, so secret values live only in your secret engine.

Secrets sourcing
OpenBaoholds the environment values
fetch at deploy
External Secrets Operator
K8s Secrettmpfs · in-memory
envFrom
Cockpit podenvironment variables

OpenBao holds the environment values; the External Secrets Operator materializes them into an in-memory Kubernetes Secret that the Cockpit pod consumes as environment variables.

  1. The complete, environment-specific variable set (with real secret values) is stored in OpenBao.
  2. At deployment, the External Secrets Operator fetches it and materializes an in-memory (tmpfs) Kubernetes Secret.
  3. That Secret is exposed to the Cockpit pod as environment variables (envFrom), not as a mounted file.
  4. To change configuration: update the value in OpenBao and redeploy — nothing is edited inside the image or stored in Git.

If your platform standardizes on ArgoCD for GitOps and a HashiCorp Vault (or OpenBao) as the secret store, see GitOps Secret Delivery for the manifest-by-manifest version of this exact flow — the SecretStore/ ExternalSecret objects your Git repository holds, and the Vault-side auth/policy setup.

Alternative: the Cockpit fetches its own boot secrets​

Instead of (or alongside) ESO materializing a Secret, the Cockpit binary can fetch its own bootstrap secrets (JWT_SECRET, ENCRYPTION_KEY, HMAC_KEY, DATABASE_URL, Graph app credentials, …) directly from a backend at process start, before AppConfig::from_env() runs, and inject them into its own environment. This is controlled by DKE_BOOT_SECRETS_BACKEND:

VariableDefaultPurpose
DKE_BOOT_SECRETS_BACKENDenvenv (no-op, read from the injected environment as today) | vault / openbao | gcp
DKE_BOOT_SECRETS_ADDR—OpenBao/Vault base URL (required for vault/openbao)
DKE_BOOT_SECRETS_TOKEN—Static Vault token. Provide this or the AppRole pair below
DKE_BOOT_SECRETS_ROLE_ID / _SECRET_ID—AppRole login credentials (alternative to a static token)
DKE_BOOT_SECRETS_APPROLE_MOUNTapproleAppRole auth mount path
DKE_BOOT_SECRETS_MOUNTsecretKV-v2 mount holding the boot secret
DKE_BOOT_SECRETS_PATHdke/bootKV-v2 path; all string values under it are injected as env vars
DKE_BOOT_SECRETS_NAMESPACE(unset)Vault Enterprise namespace header
DKE_BOOT_SECRETS_TLS_SKIP_VERIFYfalseDev only — disables TLS verification against the Vault address
DKE_BOOT_SECRETS_GCP_PROJECT—GCP project (required for gcp)
DKE_BOOT_SECRETS_GCP_SECRETdke-bootSecret Manager secret name
DKE_BOOT_SECRETS_GCP_VERSIONlatestSecret version
DKE_BOOT_SECRETS_GCP_SA_JSON(unset)Inline service-account JSON; falls back to ADC (Workload Identity) when unset
Fail-closed, never falls back to env

When DKE_BOOT_SECRETS_BACKEND=vault, openbao, or gcp, a fetch failure aborts startup — the process never silently falls back to reading secrets from its own (possibly stale/insecure) environment. This is the same fail-closed principle as the RUST_ENV=production secret gates below.

Most on-premise deployments use the ESO pattern above (DKE_BOOT_SECRETS_BACKEND left at its env default) and don't need this section — it exists for deployments that want the pod to talk to OpenBao directly instead of relying on the External Secrets Operator sidecar.


Environment reference​

Production hard-fails on weak secrets

When RUST_ENV=production (also accepts the prod alias, case-insensitively), the server refuses to start if JWT_SECRET, ENCRYPTION_KEY, HMAC_KEY, WEBAUTHN_RP_ID or WEBAUTHN_ALLOWED_ORIGINS are missing/weak, or if PWNED_PASSWORDS_ENABLED=false. Generate strong random values and store them in OpenBao — never commit them.

Server & TLS​

VariableDefaultPurpose
HOST127.0.0.1Bind address (set 0.0.0.0 in a container)
PORT3000HTTP port
TLS_PORT3443HTTPS port (0 disables; dev certs auto-generated)
TLS_CERT_PATH / TLS_KEY_PATH(unset)TLS certificate / key PEM paths
KMIP_TLS_PORT5696KMIP (OASIS TTLV over TLS) listener; 0 disables
PUBLIC_BASE_URLhttps://{host}:{tls_port}API/machine-facing origin (DKE service URLs, JWKS, ACME/EST)
FRONTEND_BASE_URL(derived)SPA/browser-facing origin (OIDC redirect, email links)
CORS_ORIGINShttp://localhost:5180Allowed CORS origins (comma-separated)
COOKIE_DOMAIN(auto-derived)Explicit Domain= attribute for session cookies. Only needed when the SPA and API live on different hosts under a shared registrable parent (e.g. cockpit-dev.duokey.cloud + cockpit-api-dev.duokey.cloud → auto-derives duokey.cloud); set explicitly for multi-label public suffixes (co.uk-style) the heuristic can't detect

Database & cache​

VariableDefaultPurpose
DATABASE_URLpostgres://…/dke_cockpitPostgreSQL DSN (sslmode=require auto-added for non-localhost)
REDIS_URLmemorymemory (in-process) or redis://host:port — Redis is required when running more than one replica
PostgreSQL only

The Cockpit uses PostgreSQL as its only datastore. Database schema migrations run automatically at startup — the database role needs DDL rights on first boot; no separate migration job is required.

Secrets & crypto (required in production)​

VariablePurpose
JWT_SECRETHS256 JWT signing key (≥ 64 chars)
JWT_SECRET_KID / JWT_SECRETS_PREVIOUSCurrent key id and historical keys (rotation)
ENCRYPTION_KEYAES-256 key (32-byte hex) for credentials/data at rest
DKE_ENCRYPTION_KEY_KIDKey-encryption-key id tag stamped into newly-written credential blobs, so a future ENCRYPTION_KEY rotation can tell old blobs from new. Default k1 — most deployments never need to change it
HMAC_KEYHMAC-SHA256 key (32-byte hex) for audit-log integrity
RUST_ENVproduction (or prod) enables the strict fail-closed gates

Authentication & passkeys​

VariableDefaultPurpose
WEBAUTHN_RP_IDlocalhostWebAuthn relying-party id (prod: required, not localhost)
WEBAUTHN_RP_NAMEDKE CockpitDisplay name in the passkey prompt
WEBAUTHN_ALLOWED_ORIGINS(dev list)Allowed origins (prod: all https://, no localhost)
PWNED_PASSWORDS_ENABLEDtrueHIBP breached-password check (prod: false panics)
DKE_ADMIN_PASSWORD(unset)Initial admin password on first boot; if unset, a strong random password is generated and printed once to the startup log (must be changed at first login)

External identity providers (Entra ID, Okta, Keycloak, generic OIDC) are configured per tenant in the Cockpit console, not through environment variables — see Identity & SSO.

Captcha (optional bot protection)​

VariableDefaultPurpose
RECAPTCHA_SECRET(unset)Google reCAPTCHA v3 server-side secret. Empty = reCAPTCHA disabled
RECAPTCHA_MIN_SCORE0.5Minimum reCAPTCHA v3 score (0.0–1.0) accepted as human; Google recommends 0.5, raise for high-value flows
HCAPTCHA_SECRET(unset)hCaptcha server-side secret. Empty = hCaptcha disabled

DKE 365​

VariablePurpose
DKE_BASE_DOMAINDNS base for DKE service URLs — each service is https://{slug}.{DKE_BASE_DOMAIN} (needs wildcard DNS + TLS). This is also the JWT audience / Azure AD identifier-URI apex — there is no separate audience variable; a decoupled audience broke Word/MIP token acquisition and was removed
DKE_DEFAULT_GRAPH_TENANT_ID / _CLIENT_ID / _CLIENT_SECRETFallback Microsoft Graph app to auto-provision Azure AD registrations, used when a service has no per-IdP Graph credentials of its own

Rate limits & caching​

Every cap below has a sane default and only needs overriding under unusual load; all fail open on a Redis outage so a cache blip cannot take the DKE decrypt path down.

VariableDefaultPurpose
DKE_TENANT_DECRYPT_RPS_MAX100/sPer-tenant cap on /dke/*/decrypt
DKE_TOKEN_DECRYPT_MAX_PER_MIN240/minPer-access-token (jti) decrypt replay cap — catches a stolen token driven as an unattended decryption oracle
DKE_GETKEY_MAX_PER_MIN600/minPer-service cap on the unauthenticated public GetKey path
DKE_GETKEY_PUBKEY_CACHE_TTL_SECS3600TTL of the cached GetKey public-key material (skips a vault/HSM round-trip on repeat calls)
DKE_JWKS_CACHE_TTL_SECS3600TTL of the cached Azure AD JWKS used to validate DKE decrypt tokens; 0 disables caching
DKE_JWT_LEEWAY_SECS60Clock-skew leeway applied to exp/nbf/iat checks
OIDC_JWKS_TTL_SECS3600TTL of the cached JWKS for tenant-configured OIDC identity providers (floored at 60s)

Agent-binary storage (S3 / GCS)​

Controls how GET /api/pki/scanners/agent/download/{platform} serves the PQC scanner-agent binaries. Unset (no bucket configured) → the handler streams the binary from the pod's local filesystem instead. Public buckets are not supported — only the pre-signed-URL mode is considered production-grade.

VariableDefaultPurpose
DKE_STORAGE_PROVIDERawsActive backend: aws (S3) or gcs
DKE_SCANNER_AGENT_S3_BUCKET(unset)Private S3 bucket holding the agent binaries
DKE_SCANNER_AGENT_S3_REGIONus-east-1AWS region for the signed-URL path
DKE_SCANNER_AGENT_S3_PREFIX(empty)Key prefix inside the bucket, e.g. agents/v1.4.0
DKE_SCANNER_AGENT_S3_PRESIGN_TTL_SECS300Pre-signed URL TTL, clamped to [60, 3600]
DKE_SCANNER_AGENT_S3_ACCESS_KEY_ID / _SECRET_ACCESS_KEY / _SESSION_TOKEN(unset)Explicit static credentials for the presign client — set these when the Cockpit runs outside AWS (e.g. Azure) so it never falls through to IMDS/IRSA
DKE_SCANNER_AGENT_S3_ENDPOINT_URL(unset)S3-compatible endpoint override (e.g. MinIO); forces path-style addressing
DKE_SCANNER_AGENT_GCS_BUCKET(unset)GCS bucket holding the agent binaries; required when DKE_STORAGE_PROVIDER=gcs
DKE_SCANNER_AGENT_GCS_ENV(unset)Deployment segment prepended to the object key ({env}/{prefix}/{platform}/{filename})
DKE_SCANNER_AGENT_GCS_PREFIX(empty)Key prefix inside the GCS bucket
DKE_SCANNER_AGENT_GCS_PRESIGN_TTL_SECS300V4 signed-URL TTL, clamped to [60, 604800]
DKE_SCANNER_AGENT_GCS_SA_JSON(unset)Inline service-account JSON for local V4 signing; falls back to GOOGLE_APPLICATION_CREDENTIALS, then to keyless GKE Workload Identity signing
DKE_SCANNER_AGENT_GCS_SIGNER_EMAIL(auto)Override the signing service-account email in Workload-Identity mode

Billing (Stripe)​

Empty (default) disables billing entirely — the Cockpit runs fine without a Stripe account.

VariablePurpose
STRIPE_SECRET_KEYStripe secret API key (sk_live_… / sk_test_…)
STRIPE_WEBHOOK_SECRETStripe webhook signing secret (whsec_…)
STRIPE_PUBLISHABLE_KEYPublishable key sent to the frontend for Checkout.js
BILLING_SELLER_NAMEIssuer name shown on invoices, overridable per-tenant from Settings; defaults to DKE Cockpit

Observability​

VariableDefaultPurpose
RUST_LOGdke_api=infoLog level filter
METRICS_ENABLEDfalseExpose Prometheus metrics at GET /metrics
METRICS_BEARER_TOKEN(unset)Bearer token protecting /metrics — required; in production, enabling metrics without a token disables them (fail-closed)

Hardening​

VariableDefaultPurpose
DKE_TRUSTED_PROXY_CIDRS(empty)Only these source CIDRs may set cf-connecting-ip / cf-ipcountry
DKE_FAIL_CLOSED_AUDITprod=trueReturn 503 when a DKE-decrypt / access-policy audit write fails
DKE_FAIL_CLOSED_HTTP_AUDITfalseReturn 503 when the durable global HTTP audit queue is saturated. Opt-in everywhere (including production) — the global path fires on every request, so auto-escalating to 503 is an availability trade-off operators must consciously accept
DKE_AUDIT_WORM_PATH(unset)Optional write-once (WORM) audit sink path
DKE_GEOIP_MMDB_PATH(unset)Path to a local MaxMind GeoIP database for country-based access ACLs. Unset → all lookups return unknown, so on-prem/air-gapped deployments never silently fall back to a grant
DKE_HSM_GATEWAY_URL(derived from PUBLIC_BASE_URL)Public wss:// URL that on-prem HSM proxy-agents dial to reach the Cockpit's agent gateway

Dangerous switches — never enable in production​

Every one of these defaults to the secure behaviour. They exist for local dev, CI fixtures, or air-gapped labs — flipping any of them on a production, internet-facing deployment removes a real security control.

VariableDefaultPurpose
DKE_ALLOW_PERMISSIVE_MODEoffLets DKE decrypt succeed for a service that has no Azure AD tenant/client configured, skipping JWT validation. Dev/test fixtures only
OIDC_ALLOW_HMAC_ID_TOKENSoffAccepts HS256-signed OIDC id-tokens (normally only RS256/ES256 from a JWKS are trusted)
OIDC_ALLOW_UNVALIDATED_ISSUERoffSkips issuer validation on tenant-configured OIDC providers
OIDC_ALLOW_UNVERIFIED_EMAIL_LINKoffLinks an external identity to a local account without a verified email match
EST_ALLOW_UNVERIFIED_ISSUANCEoffLets the EST endpoint issue certificates without verifying the enrollment proof
CMP_ALLOW_UNVERIFIED_ISSUANCEoffSame relaxation as above, for the CMP endpoint
ACME_AUTO_VALIDATEoffAuto-completes ACME challenge validation instead of performing the real HTTP-01/DNS-01 check
DEV_INBOX_TOKEN(unset)Bearer token required for any non-loopback caller to read the dev email-capture inbox. Loopback callers are always allowed (local dev); with no token configured, every non-loopback caller is refused — this is a safety net, not something to routinely set in production

Advanced / integration-specific​

Most deployments don't need this section. It covers narrow integration paths: reverse-proxy-terminated mTLS passthrough, Kubernetes-federated service-account auth for secret delivery, machine API keys, and the built-in code/dependency scan policy.

VariableDefaultPurpose
MCP_KEY_REST_AUTH_ENABLEDtrueWhether long-lived machine API keys (dke_mcp_…) are accepted on the standard REST surface, in addition to session JWTs — set false/0/off to force session-JWT-only REST auth (the /mcp and Kubernetes secret-delivery paths are unaffected)
K8S_SA_JWT_AUTH_ENABLEDoffMaster switch for accepting an in-cluster Kubernetes service-account JWT as a machine identity for secret delivery. All other K8S_* vars below are ignored unless this is set
K8S_OIDC_ISSUER—Expected iss — the cluster's SA-token issuer URL
K8S_OIDC_JWKS_URL{issuer}/openid/v1/jwksJWKS location, if not derivable from the issuer
K8S_SA_EXPECTED_AUDIENCE—Required aud (e.g. duokey-cockpit)
K8S_SA_TENANT_ID—Tenant every accepted token maps to (UUID)
K8S_SA_MACHINE_USER_ID—User whose live permissions authorize the read (UUID)
EKM_MTLS_CN_HEADER(unset)Header name carrying the reverse-proxy-verified mTLS client-certificate CN for the SQL EKM endpoint. Off unless set — a client-settable header must never be trusted directly, so the proxy must strip it from inbound requests
AZURE_EKM_CLIENT_CERT_HEADERx-client-certHeader carrying the proxy-terminated client leaf certificate (PEM) for Azure EKM endpoints configured with require_mtls
AZURE_EKM_CLIENT_CERT_VERIFY_HEADERx-client-cert-verifyOptional proxy verdict header; if present it must read SUCCESS
MCP_SCAN_TIMEOUT_SECS / MCP_MAX_CONCURRENT_SCANS / MCP_SCAN_STALE_SECS / MCP_SCAN_ALLOW_PRIVATE / MCP_SCAN_ROOT(sane defaults)Policy knobs for the MCP-triggered filesystem/dependency scan (timeout, concurrency, staleness, private-network access, scan root)
SECUROSYS_TSB_URL / SECUROSYS_TSB_TOKEN(unset)When both are set, seeds a sandbox Securosys CloudHSM vault on first boot for zero-config dev/demo HSM access. Has no effect once the vault exists; production tenant HSM credentials are configured from the UI, not this
DKE_REPO_ROOT(auto-detected)Override for locating the source tree used by the built-in ASVS code scanner, when the checkout isn't at the auto-detected workspace root

Example environment (production-shaped)​

# Environment
RUST_ENV=production
RUST_LOG=dke_api=info

# Database & cache
DATABASE_URL=postgres://user:pass@db-host:5432/dke_cockpit
REDIS_URL=redis://redis-host:6379

# Secrets (all required in production)
JWT_SECRET=<random string, >= 64 chars>
ENCRYPTION_KEY=<64 hex chars = 32 bytes>
HMAC_KEY=<64 hex chars = 32 bytes>

# Server / TLS
HOST=0.0.0.0
PORT=3000
TLS_PORT=3443
TLS_CERT_PATH=/etc/dke/tls/fullchain.pem
TLS_KEY_PATH=/etc/dke/tls/privkey.pem
CORS_ORIGINS=https://cockpit.example.com
PUBLIC_BASE_URL=https://cockpit-api.example.com
FRONTEND_BASE_URL=https://cockpit.example.com

# Passwords / passkeys
PWNED_PASSWORDS_ENABLED=true
WEBAUTHN_RP_ID=cockpit.example.com
WEBAUTHN_RP_NAME=DuoKey Cockpit
WEBAUTHN_ALLOWED_ORIGINS=https://cockpit.example.com

# DKE 365
DKE_BASE_DOMAIN=dke.example.com

# Observability
METRICS_ENABLED=true
METRICS_BEARER_TOKEN=<random token>

# Hardening
DKE_TRUSTED_PROXY_CIDRS=10.0.0.0/8,172.16.0.0/12
DKE_FAIL_CLOSED_AUDIT=true
HSM / vault credentials are not environment variables

Vault/HSM backends (Securosys, cloud KMS, software vault, MPC…) are stored per tenant in the database with credentials encrypted under ENCRYPTION_KEY, and are configured from the Cockpit UI — not through environment variables. The SECUROSYS_TSB_URL / SECUROSYS_TSB_TOKEN pair above is the one exception, and only seeds a sandbox vault for first-boot convenience.


Troubleshooting​

SymptomLikely causeFix
Pod exits immediately in productionA required secret is missing or weak (JWT_SECRET, ENCRYPTION_KEY, HMAC_KEY, WEBAUTHN_*)Check the startup log; generate strong values and re-inject via OpenBao
Database errors on first bootThe database role lacks DDL rights for the automatic migrationsGrant DDL on the dke_cockpit database for the first start
Login works on one replica onlyREDIS_URL=memory with multiple replicasPoint REDIS_URL at a shared Redis
/metrics returns nothingMetrics enabled without METRICS_BEARER_TOKEN (fail-closed)Set a bearer token and redeploy
Browser CORS errorsCORS_ORIGINS does not list the frontend originAdd the exact https:// origin
Session dropped / CSRF 403 right after login, only when SPA and API are on different subdomainsThe auth cookie is scoped host-only to the API origin, so the SPA never sees itSet COOKIE_DOMAIN to the shared registrable parent (e.g. duokey.cloud), or rely on the auto-derivation if both hosts already share one
DKE decrypt / GetKey calls start returning 429 under normal loadOne of the DKE_*_MAX_PER_MIN / DKE_TENANT_DECRYPT_RPS_MAX caps is too low for the tenant's real trafficRaise the relevant cap; caps fail open on a Redis outage, so this is a deliberate limit, not an outage
Pod fails to start with a Vault/OpenBao or GCP fetch error, even though DATABASE_URL/JWT_SECRET etc. are set directly in the pod envDKE_BOOT_SECRETS_BACKEND is set to vault/openbao/gcp and that fetch failed — it never falls back to the pod's own environmentFix the Vault/GCP connectivity or credentials, or unset DKE_BOOT_SECRETS_BACKEND (default env) to use the ESO-materialized Secret directly

Continue to Secure → Secrets Management for the OpenBao/ESO setup, or back to Installation.