Skip to content

Environment Variables

Config file alternative: All KLANGK_* settings below can also be set in a YAML config file via klangkd --config. The config file is the recommended substrate for production deployments; env vars override file values. See Configuration File.

Applying configuration changes: After editing KLANGK_* env vars or the YAML config file, send SIGHUP to the klangkd process to reload and apply the new values without a full process restart. The HTTP listener and DB stay up; WebSocket clients reconnect automatically. A small set of settings (KLANGKD_PORT, KLANGKD_LISTEN, KLANGKD_DATA_DIR, KLANGKD_STATE_DIR) are bound for the life of the process and require a full restart — SIGHUP logs a warning if one of these changed. An invalid configuration denies the reload (runtime left on last-known-good config, reason logged at ERROR). See Process Signals.

$DEVENV_STATE refers to <project root>/.devenv/state — this is where devenv stores runtime data.

For local development, settings live in klangkd.yaml (gitignored; devenv.nix seeds it from klangkd.yaml.devenv on first shell entry). Environment variables override config-file values. Both env vars and config-file values support file: and cmd: prefixes for secret indirection — see Configuration File — file: and cmd: resolution for the full precedence rules and resolution semantics.

Removed in 2.X: KLANGKD_PROXY_ENGINE is gone — Caddy is the sole reverse-proxy engine. The env var is no longer recognized (silently ignored); remove it from your config.

Variable Default Description
KLANGKD_PROXY_PORT deprecated Deprecated alias for KLANGKD_EGRESS_PORT. If KLANGKD_EGRESS_PORT is unset, this value is used as the egress port and a deprecation warning is logged; if both are set, this is ignored (KLANGKD_EGRESS_PORT wins) and a warning is logged. A future release will stop recognizing it — rename to KLANGKD_EGRESS_PORT. Renamed from KLANGKD_NGINX_PORT; the old KLANGKD_NGINX_PORT name is no longer recognized.
KLANGKD_EGRESS_PORT 8995 Container-egress port — the proxy listener for container→backend traffic. Three locations are served on it: /llm-proxy/*, /api/v1/browser-delegate, and /ws/egress-sidecar (the network sidecar's blocked-egress event channel). (Two more, /api/v1/auth/verify-workspace-token and @token_auth_failed, live on the same listener but are internal — reachable only as forward_auth subrequests, never callable directly.) Served in both headless and full/browser modes. Must differ from KLANGKD_PORT so ingress vs egress can be firewalled separately.
KLANGKD_EGRESS_LISTEN 0.0.0.0 Interface the proxy binds for the egress listener ({KLANGKD_EGRESS_LISTEN}:{KLANGKD_EGRESS_PORT}). Defaults to 0.0.0.0 (all interfaces) because the host interface container traffic arrives on is environment-specific (host.containers.internal resolves to a netavark/pasta virtual gateway that isn't bindable; the real interface traffic lands on varies by podman network mode and host IP) and cannot be detected reliably at render time. The all-interfaces bind is not a security hole: every one of the served locations is double-gated — CONTAINER_ACL (a container-source allowlist that returns 403 for any IP outside the container subnet set) plus forward_auth to /api/v1/auth/verify-workspace-token (→ 401 without a valid workspace JWT, even from an allowed container IP). So the network sees 403 on every location; a container IP without a token sees 401. Pin to a specific host IP (e.g. 192.168.1.5) to drop every other interface from the egress surface if your deployment's container-facing IP is stable. The browser listener (KLANGKD_LISTEN/KLANGKD_PORT, default loopback) is a separate server block and is not affected. Accepts a bare bind address only — an IPv4/IPv6 literal or a host name (a bracketed IPv6 literal normalizes to the bare form); a value carrying a port, CIDR suffix, or embedded whitespace fails startup with the setting named, because the value renders verbatim into the Caddyfile bind directive (#3275). A host name must be resolvable and bindable by Caddy: an interface name (eth0) is a valid host name that passes validation, and Caddy refusing to bind it aborts the proxy with Caddy's error instead of an endless restart loop.
KLANGKD_PORT unset Browser/proxy port (UI, API, WebSocket, hosted apps). Unset ⇒ headless mode (no browser listener rendered; only the container-egress listener on KLANGKD_EGRESS_PORT is served). Set ⇒ full/browser mode. Suggested 8997.
KLANGKD_LISTEN 127.0.0.1 Address the proxy binds for the browser listener ({KLANGKD_LISTEN}:{KLANGKD_PORT}). Rendered only when KLANGKD_PORT is set (full/browser mode). Default 127.0.0.1 (loopback); set to 0.0.0.0 to expose the browser listener on all interfaces. Accepts a bare bind address only — an IPv4/IPv6 literal or a host name (a bracketed IPv6 literal normalizes to the bare form); the port lives in KLANGKD_PORT (Caddy ignores a port written inside a bind address), and a CIDR suffix or embedded whitespace fails startup with the setting named, because the value renders verbatim into the Caddyfile bind directive (#3275). A host name must be resolvable and bindable by Caddy; an unbindable one aborts the proxy with Caddy's error instead of an endless restart loop.
KLANGKD_TLS_HOSTNAME The public FQDN klangkd serves, arming automatic TLS on the built-in Caddy proxy (#3192). Unset (the default) keeps plain HTTP with auto_https off — the outer-proxy model, byte-identical to before. Set to a public DNS name (e.g. klangk.example.com) and the browser listener is rendered as https://<fqdn>:<KLANGKD_PORT> with a CA-issued, auto-renewed certificate (ACME HTTP-01 / TLS-ALPN via Let's Encrypt + ZeroSSL) and an HTTP→HTTPS redirect on port 80. Requires KLANGKD_PORT set (arming without it refuses to boot), a public DNS record pointing at the host, and ports 80/443 reachable from the internet. Validated at construction: with the default acme issuer it must be a syntactically valid public FQDN (not an IP, URL, or single-label name); with KLANGKD_TLS_ISSUER=internal any RFC 1123 host name or IPv4 literal is accepted. Certificate material persists under <state_dir>/caddy-storage so restarts do not re-issue. Reloadable on SIGHUP (the re-rendered config is pushed to the running Caddy). See HTTPS Hosting.
KLANGKD_TLS_ISSUER acme How the armed listener's certificate is obtained (#3192). acme (the default, also when unset): ACME issuance (Let's Encrypt + ZeroSSL) for the public FQDN. internal: a self-generated certificate from the built-in proxy's internal CA — the TLS hop behind an outer proxy. Any host name or IPv4 literal arms (klangkd.internal, localhost, 10.0.0.5). The certificate is generated on the host: klangkd contacts no certificate authority and needs no public DNS record. The HTTPS listener binds KLANGKD_LISTEN:KLANGKD_PORT, and that is the only port the TLS setup involves; the automatic HTTP→HTTPS redirect stays off because the outer proxy already sends browsers to HTTPS (an enabled redirect would try to bind port 80). Certificates renew automatically and are stored in <state_dir>/caddy-storage — back that directory up (a lost root means a new CA the outer proxy must re-trust). Setting it without KLANGKD_TLS_HOSTNAME logs a warning (inert). Reloadable on SIGHUP. See HTTPS Hosting.
KLANGKD_ACME_EMAIL The ACME account email (certificate expiry / renewal-failure notices, CA account registration) used when KLANGKD_TLS_HOSTNAME arms automatic TLS (#3192). Rendered as Caddy's global email directive — must be a plain address like ops@example.com (a display-name form is rejected at construction, since the value is passed verbatim into the proxy config). Strongly recommended whenever automatic TLS is armed. Reloadable on SIGHUP.
KLANGKD_SOCKET <state_dir>/klangk.sock Backend UDS path that klangkd binds. Override when the default overflows the AF_UNIX sun_path limit (≤104 chars). Resolved paths exceeding the limit fail at construction with a diagnostic directing the deployer to shorten KLANGKD_SOCKET or move KLANGKD_STATE_DIR shallower.
KLANGKD_CADDY_ADMIN_SOCKET <state_dir>/caddy-admin.sock The admin-API Unix domain socket for the Caddy engine. Same length-limit story as KLANGKD_SOCKET: a deep KLANGKD_STATE_DIR can push the default path over the portable AF_UNIX sun_path bound (≤104 chars), and since the admin UDS is the Caddy engine's only config-delivery path, a too-long path makes the engine unstartable. Override to a shorter absolute path when that happens; resolved paths exceeding the limit fail at construction (regardless of engine) with a diagnostic naming KLANGKD_CADDY_ADMIN_SOCKET.
KLANGKD_PROXY_BIN Path to the proxy (Caddy) executable the renderer spawns; renamed from KLANGKD_NGINX_BIN (the old name is no longer recognized). Unset → shutil.which("caddy"), then /usr/bin/caddy at render time.
KLANGKD_DATA_DIR <state_dir>/data Database, workspaces, Pi sessions. Defaults to <state_dir>/data (so it tracks the KLANGKD_STATE_DIR default); an explicit value always wins. In dev it resolves to $DEVENV_STATE/klangk/data via klangkd.yaml; the host container sets it to /home/klangk/data.
KLANGKD_STATE_DIR $XDG_STATE_HOME/klangkd Runtime state — the UDS socket (<state_dir>/klangk.sock), rendered proxy config, pid file, SSL trust bundle (<state_dir>/ssl/ca-bundle.crt when custom CAs are configured). Defaults to $XDG_STATE_HOME/klangkd (→ ~/.local/state/klangkd when the var is unset, incl. macOS) when no explicit value is supplied; explicit KLANGKD_STATE_DIR / config-file values still win. Construction fails fast only in the genuinely-unconfigured case (neither $XDG_STATE_HOME nor $HOME set), preserving the intent. In dev it resolves to $DEVENV_STATE/klangk via klangkd.yaml; the host container sets it to /tmp/klangk-state. klangkd no longer mutates os.environ to inject a default. The server's XDG subdir is klangkd (the binary name); the CLI's is klangk — different audiences, different trees.
KLANGKD_CONFIG_DIR $XDG_CONFIG_HOME/klangkd The config-tree root for user-edited, durable intent — the config-tree analogue of KLANGKD_STATE_DIR. Defaults to $XDG_CONFIG_HOME/klangkd (→ ~/.config/klangkd, incl. macOS when the var is unset) when no explicit value is supplied; KLANGKD_CUSTOMIZE_DIR derives from the resolved config_dir (like KLANGKD_DATA_DIR derives from state_dir). Set this to relocate the config tree with one knob; KLANGKD_CUSTOMIZE_DIR still wins over the derivation. Read at boot and on SIGHUP (reloadable).
KLANGKD_CUSTOMIZE_DIR <config_dir>/custom Root directory for deployment customization files (branding, email templates, certs). User-edited, durable intent — so it's config, not state: defaults to <config_dir>/custom (→ ~/.config/klangkd/custom) when unset, deriving from KLANGKD_CONFIG_DIR. Subsystems look for well-known subdirectories (certs/, branding/, email-templates/) here when their per-feature env var is unset; missing subdirectories are silently skipped. An explicit KLANGKD_CUSTOMIZE_DIR always wins. See Customizing a Deployment.
KLANGKD_TRUSTED_CA_DIR Operator-managed approved CA baseline restricting which custom CAs are trusted (#3198). When set (a non-empty path; an explicitly empty value behaves as unset), only CAs whose SHA-256 fingerprint appears among the .pem/.crt certs in this directory are trusted — by the backend's outbound TLS and every workspace container. klangkd stages the approved certs (one canonical PEM per unique parseable cert) under <state_dir>/ssl/approved and that staged copy — not the raw directories — is what both trust scopes consume, so a file the strict parser rejects is never trusted even if a lenient consumer might accept its raw bytes. Each cert dropped into <KLANGKD_CUSTOMIZE_DIR>/certs is audited against the baseline: approved ones log at debug with subject/issuer, non-approved or unparseable ones are refused with a warning and never reach a trust bundle. Lets locked-down deployments (e.g. a DoD-approved CA baseline) enforce a defined CA set. Unset (default) = no restriction (every cert in the customize certs dir is trusted). A missing, unreadable, or cert-less baseline fails closed — no custom CAs trusted, error logged; trust applied earlier in the process is revoked on reload. Read live on every resolution: reloadable on SIGHUP (backend trust re-applies — or revokes — on reload; containers started afterwards pick up the new baseline). See Customizing a Deployment. Also accepted via its field-name form KLANGKD_APPROVED_CA_DIR (same setting; when both are set, KLANGKD_TRUSTED_CA_DIR wins).
KLANGKBUILD_VARIANT Identifies a custom build (a variant) of klangk, independent of the upstream klangk version. Baked into version.json as variant by scripts/generate-version.sh at build time; surfaced at GET /api/v1/version and the debug pane's Variant row. Empty/absent = stock klangk (no variant reported, output byte-identical to a non-customized build). See Customizing a Deployment.
KLANGKD_IMAGE_NAME klangk-workspace Podman image name for workspace containers
KLANGKD_IMAGE_PULL_POLICY never Podman --pull policy for workspace containers (never, missing, always, newer). Default never requires the image to exist locally; missing pulls from a registry if not found
KLANGKD_ALLOWED_IMAGES Comma-separated allowlist of container images users may pick when creating a workspace (in addition to KLANGKD_IMAGE_NAME). Unset → only KLANGKD_IMAGE_NAME is allowed.
KLANGKD_USERNS keep-id:uid=1000,gid=1000 Podman --userns mode for workspace containers. Maps the host user to uid/gid 1000 inside the container so bind-mounted files are owned correctly.
KLANGKD_PODMAN_BIN podman Path to the podman binary
KLANGKD_PORT_RANGE_START 9000 First host port for workspace app allocations. Each workspace gets 5 ports starting from this value. See Hosted Apps.
KLANGKD_HOSTED_PORTS_PER_WORKSPACE 5 Server-wide ceiling on hosted-app ports per workspace. Set to 0 to disable hosted-app serving entirely — klangkd stops allocating ports and injecting the hosting env into containers, and /hosted/... returns 404. Any other value clamps each workspace down to this cap. See Hosted Apps.
KLANGKD_ALLOWED_MOUNT_ROOTS Comma-separated list of allowed host path prefixes for user bind mounts (e.g., /home,/data). Unset (the default) disables user bind mounts entirely (#3153) — only named volumes may be mounted; a mount with a host-path source is rejected at workspace create/edit and re-validated at container start (#3278). klangkd's own mounts (home, config, SSL, nix) are internal and unaffected. Protected paths are always blocked regardless. Reloadable on SIGHUP.
KLANGKD_DNS_SERVERS Comma-separated DNS server IPs for containers (e.g., 100.100.100.100,8.8.8.8 for Tailscale MagicDNS). If unset, containers use podman's default DNS.
KLANGKD_DNS_SEARCH Comma-separated DNS search domains for containers (e.g., corp.example,svc.example for short-name resolution). Passed to podman --dns-search; unset → podman's default search behavior. Applies to newly-created containers.
KLANGKD_HOSTING_HOSTNAME (auto-derived) URL authority (host[:port]) for hosted-app and user-facing URLs — the port lives here, it is never synthesized from KLANGKD_EGRESS_PORT (that is internal container wiring, not the public port). Set explicitly in production; behind a real reverse proxy it must carry the public host and port. When unset, klangkd derives the authority from a trusted X-Forwarded-Host, or from the request's Host header when that Host names the deployment's own address — loopback, the KLANGKD_TLS_HOSTNAME name, or the KLANGKD_LISTEN IP-literal address on the browser port; any other Host value falls back to localhost (localhost:<KLANGKD_PORT> when the browser listener is armed), so a client-chosen Host never lands in a reset/verify/invite link or the OIDC redirect (#3276). A deployment whose browsers reach klangkd by any other name (e.g. KLANGKD_LISTEN=0.0.0.0 with a DNS name) must pin this value for correct links.
KLANGKD_HOSTING_PROTO (auto-derived) Protocol for user-facing app URLs. Auto-derived from X-Forwarded-Proto or defaults to http.
KLANGKD_HOSTING_BASE_PATH (auto-derived) Base path prefix for user-facing app URLs (e.g., /klangk). Auto-derived from X-Forwarded-Prefix.
KLANGKD_TRUSTED_PROXY_CIDRS 127.0.0.1,::1 Comma-separated CIDRs/IPs whose connections are trusted to supply X-Forwarded-Host/Proto/Prefix headers. klangk's proxy runs on loopback, so the default works out of the box; requests from peers outside this set have forwarded headers ignored (preventing host poisoning if the backend port is exposed), and the managed Caddy drops the X-Forwarded-Host it would otherwise derive from a client-chosen Host for such peers, so a direct request with a forged Host reaches the backend with no forwarded host at all (#3276).
KLANGKD_CONTAINER_SUBNETS (auto-derived) Container source IPs/CIDRs (pasta NAT → host's own IPs). Allowlist for the three container endpoints AND denylist for the catch-all location / (deny-by-default from containers). Unset → auto-detect host IPv4s.
KLANGKD_REJECT_PROXY_HEADERS Set to 1 to ignore X-Forwarded-Host/Proto/Prefix headers unconditionally (hard override; back-compat with the old opt-out).
KLANGKD_IDLE_TIMEOUT_SECONDS 3600 Container idle timeout in seconds (check interval auto-computed as timeout/3, clamped 10-60s)
KLANGKD_QUIESCE_TIMEOUT 15 Seconds the graceful stop (TERM/INT), graceful restart (SIGHUP), and scheduled server stop/recycle wait for in-flight HTTP requests before draining workspaces. Stragglers at expiry are logged (WARNING) and left to finish against the exiting/recycling process. Keep the quiesce + drain budget inside your service manager’s stop deadline — under systemd’s default 90s TimeoutStopSec, values above ~85s are cut off. See Process Signals.
KLANGKD_LOGIN_LOCKOUT_WINDOW 300 Time window in seconds for counting failed login attempts.
KLANGKD_LOGIN_LOCKOUT_FAILURES 5 Number of failed login attempts before a lockout. Default 5 (enabled). Set to 0 to disable.
KLANGKD_LOGIN_LOCKOUT_DURATION 900 Duration of lockout in seconds (only relevant when KLANGKD_LOGIN_LOCKOUT_FAILURES > 0).
KLANGKD_MAX_SESSIONS_PER_USER 0 Concurrent login-session cap per user. 0 (the default) = no limit. Each login (password, OIDC, verification, reset, invite acceptance) counts as one session; token refresh keeps the same slot. When a new login pushes the user past the cap, the oldest session is revoked via the token blocklist — its next HTTP request gets 401 and its next WebSocket connect is rejected with 4001. Sessions whose token already expired are purged and never count. Reloadable on SIGHUP.
KLANGKD_SESSION_WORKSTATION_BINDING off Session workstation binding: replay protection for bearer JWTs. off (the default) = any token holder can use it until expiry. ip = every authenticated HTTP request, token refresh, and WebSocket connect must come from the same network the session was established from (the effective client IP; two IPv6 addresses inside one /64 count as the same). strict = additionally require the same User-Agent. A token presented from a different workstation is rejected (401 / WS close 4001) and its session revoked, with an audit record naming both workstations; the legitimate client shares the token, so it is logged out too and must re-login. Sessions with an unknown recorded IP (pre-feature rows) are never rejected. Reloadable on SIGHUP.
KLANGKD_STEP_UP_WINDOW_MINUTES 0 Step-up (sudo-mode) reauthentication window in minutes. When armed, privileged admin writes (user/group/invitation management, admin ACL rewrites, server schedules, volume deletes, and takeover-class writes on a workspace you do not own: delete, raw ACL rewrite, ownership transfer, role assignments) are refused with a machine-readable 403 step_up_required until the session's owner confirms their password via POST /api/v1/auth/step-up; the confirmation is stamped on that session's row and honored for this many minutes. Per session (not per user): it survives token refresh, dies with logout/revocation, and never unlocks a second session. The confirmation endpoint has login-grade lockout accounting. OIDC-managed accounts (no klangk password) are exempt; each exempt pass is audit-logged. 0 (the default) disables the gate; 15 is the recommended hardening value. The web client prompts and retries automatically; the CLI prompts once. Reloadable on SIGHUP.
KLANGKD_WEB_BIND_GRACE_SECONDS 300 The DPoP bind grace window for web-client sessions, in seconds — the backstop behind born-bound web mints. Sessions minted for the web app are born DPoP-bound: the SPA's minting requests carry its public binding JWK (Klangk-Binding-Jwk; an OIDC login URL carries it into the state cookie, and an OIDC web flow arriving without it is refused at the callback unless it explicitly rode none from a key-less build), so the token carries cnf.jkt from mint and no unbound window exists. A token that nonetheless mints unbound carries a signed bind deadline: mint time plus this many seconds. Still unbound past the deadline, the session is refused — every API request, token refresh, bind call, and WebSocket connect answers 401 until the user logs in again, and an established WebSocket (main or consent-decider) closes at the deadline rather than the token's natural expiry. The deadline survives token refresh and bind swaps unchanged (a rotation re-arms the live sockets for the replacement token), so a rotation can never reset it; the web client retries a transiently failed bind every 30 seconds inside the window. CLI/TUI sessions are unmarked and stay unbound indefinitely. 0 drops the deadline (born-bound minting stays on). Reloadable on SIGHUP; the deadline is read at mint time.
KLANGKD_INACTIVITY_DISABLE_DAYS 35 Dormant-account auto-disable window in days. Accounts (except the system agent and admin-group members) whose newest activity signal — last authenticated API access, last login, or creation — is older than the window are disabled by an hourly sweep; their logins, token refreshes, and authenticated requests then fail with 403 Account disabled and their live WebSocket connections are closed (4001 → client logout) until an admin re-enables them. 0 disables the sweep. Reloadable on SIGHUP.
KLANGKD_SESSION_IDLE_TIMEOUT_MINUTES 0 Idle session timeout in minutes. Minutes of inactivity — no authenticated HTTP request and no WebSocket frame; a token refresh does not count — after which a login session terminates: the next token refresh is refused with 401 (and the token blocklisted), and a quiet WebSocket is closed by the server (4001 → client logout). admins-group members get the shorter window of this setting and KLANGKD_PRIVILEGED_SESSION_IDLE_TIMEOUT_MINUTES. When armed, access-token lifetimes are capped at the window so an idle client surfaces at its next refresh within the window plus one refresh interval. Any WebSocket message (including the frontend's 60-second heartbeat) keeps a watched session alive. 0 (the default) restores age-only expiry exactly. Reloadable on SIGHUP.
KLANGKD_PRIVILEGED_SESSION_IDLE_TIMEOUT_MINUTES 10 The privileged (admins-group) idle window in minutes: admins-group members terminate after the lesser of this and KLANGKD_SESSION_IDLE_TIMEOUT_MINUTES. 0 turns the privileged split off (admins then use the general window). Read live, so a SIGHUP reload applies at the next issue/refresh/sweep. Reloadable on SIGHUP.
KLANGKD_LLM_API_KEY Default LLM API key for models that don't specify their own (supports file:/cmd: prefix)
KLANGKD_LLM_MODELS Comma-separated list of provider/model:api_base:api_key entries for the in-process LLM router (see LLM proxy)
KLANGKD_JWT_SECRET JWT signing secret. A warning is logged at startup if unset or left as the insecure dev default.
KLANGKD_AUDIT_HMAC_KEY HMAC-SHA256 key used to tag audit rows (container_events, egress_consent, audit_events) at insert time (#3174, #3205). Opt-in: when unset (the default), no tag is computed or stored. There is deliberately no derivation from KLANGKD_JWT_SECRET: that secret ships a known insecure dev default, and audit integrity should not silently ride on it. klangkd only writes tags — verification (re-computing tags against the key) is an external consumer's job, e.g. an off-host audit backup. Rotation: changing the key invalidates every tag written under the old key; export rows before rotating. Reloadable on SIGHUP.
KLANGKD_PREVENT_INSECURE_JWT_SECRET Set to 1 to fail at startup if KLANGKD_JWT_SECRET is unset or insecure. Recommended for production.
KLANGKD_ACCESS_TOKEN_HOURS 24 Lifetime in hours for user session JWTs. Tokens are automatically refreshed before expiry.
KLANGKD_WORKSPACE_TOKEN_HOURS 24 Lifetime in hours for per-workspace JWTs. Tokens are automatically renewed at 80% of their lifetime.
KLANGKD_DEFAULT_USER Auto-seeded admin email on startup
KLANGKD_DEFAULT_PASSWORD Auto-seeded password on startup (omit to generate random; supports file:/cmd: prefix)
KLANGKD_MIN_PASSWORD_LENGTH 8 Minimum password length
KLANGKD_PASSWORD_HISTORY_COUNT 0 Number of previous passwords remembered per user (max 24); a new password matching the current or any remembered one is rejected with 400. 0 disables reuse checking.
KLANGKD_PASSWORD_MIN_CHANGED 0 Minimum number of characters (edit distance, max 72 — substitutions, insertions, and deletions each count) that a self-service password change must alter from the current password. Rejected with 400 otherwise. Enforced only on POST /auth/change-password, where the current password is re-authenticated — forgot-password resets and admin-set passwords never present the old plaintext, so the rule cannot apply there (password-history reuse still does). 0 (the default) disables the gate. Reloadable on SIGHUP. Advertised via /api/v1/config (password_min_changed) so change-password forms pre-check inline.
KLANGKD_PASSWORD_MIN_AGE_HOURS 0 Minimum password age in hours (#3177). Self-service password changes and forgot-password resets made inside the window are rejected with 400; admin-forced resets bypass it. 0 (the default) disables the check; 24 hours is the recommended hardening value (max 8760). Passwords set before the column existed are dated from account creation. Reloadable on SIGHUP.
KLANGKD_PASSWORD_MAX_AGE_DAYS 0 Maximum password age in days (#3177). Login, token refresh, and the next authenticated request / WS connect refuse a session for an expired password with a machine-readable 403 (detail.error = "password_expired"); the CLI/TUI then prompt for a new password via POST /auth/change-expired-password. Local password accounts only — OIDC logins are unaffected. 0 (the default) disables expiry; 60 days is the recommended hardening value (max 3650). Reloadable on SIGHUP.
KLANGKD_FILE_UPLOAD_SIZE_MAX 524288000 Maximum upload size in bytes for file uploads and workspace imports (default 500 MB)
KLANGKD_WEBSOCKET_MSG_SIZE_MAX 16777216 Maximum WebSocket message size in bytes (default 16 MB). Increase if syncing very large files via rsync over WebSocket.
KLANGKD_API_RATE_LIMIT 0 Per-client-IP /api/* request budget (fixed 60s window, 429 + Retry-After) enforced in the backend. Default 0 = off (opt-in; 300 is the documented example budget). The client IP resolves through KLANGKD_TRUSTED_PROXY_CIDRS — behind an outer proxy not in that set, all forwarded clients share the proxy's single bucket. Static assets, /ws upgrades, /hosted/*, and /health never count. Reloadable on SIGHUP.
KLANGKD_FRONTEND_DIR (in-package klangk/frontend) Directory the built Flutter Web UI is served from. Defaults to the in-package location (klangk/frontend/, shipped in the wheel) so a pip install klangk deployment serves the UI out of the box; source-tree deployments (devenv, host container) set this to the repo's src/frontend/build/web. The UI is mounted only when the directory exists; klangkd logs a warning otherwise.
KLANGKD_FEATURES_ENABLE (unset → manifest defaults) The deploy's chosen active-feature list (canonical semantics). Comma-separated feature names; any explicit value → exactly that list, nothing implied (no * form). Unset → the manifest's defaults list (the stock known-good set: beep, bobdobbs, browser-fetch, celebrate, git-credential). The frontend reads its sibling features.json for per-feature metadata + the defaults list, and this value (forwarded via /api/config) for the deploy's chosen set; filtering happens in main.dart before feature registration. Distinct from the build-time declaration: "what's compiled in" is build-time; "what's turned on" is deploy-time. When the defaults change upstream, a pinned list doesn't pick up the change — re-pin deliberately. Read at boot and on SIGHUP (reloadable).
KLANGKD_PASSWORD_REQUIRE_UPPER 0 Minimum number of uppercase characters required in new passwords. 0 (default) = no requirement.
KLANGKD_PASSWORD_REQUIRE_LOWER 0 Minimum number of lowercase characters required in new passwords. 0 (default) = no requirement.
KLANGKD_PASSWORD_REQUIRE_DIGIT 0 Minimum number of digits required in new passwords. 0 (default) = no requirement.
KLANGKD_PASSWORD_REQUIRE_SPECIAL 0 Minimum number of special characters required in new passwords. 0 (default) = no requirement.
KLANGKD_CORS_ORIGINS Comma-separated list of allowed CORS origins (e.g., https://klangk.example.com). If unset, derived from KLANGKD_HOSTING_HOSTNAME/KLANGKD_HOSTING_PROTO, or defaults to http://localhost. Set to * to allow all origins (not recommended for public deployments).
KLANGKWS_FEATURE_GITHUB_OAUTH_CLIENT_ID GitHub OAuth App client ID for device-flow authentication. Injected into workspace containers by the git-credential feature. When set, git push to github.com triggers the OAuth device flow automatically. No client secret needed. May also be set (durable, deploy-wide) under the features_config: block of the YAML config file (as github_oauth_client_id) — env still wins per key; see Configuration File. See GitHub Authentication.
KLANGKWS_FEATURE_GITLAB_OAUTH_CLIENT_ID GitLab OAuth application client ID — the git-credential device-flow shorthand for gitlab.com (same role as KLANGKWS_FEATURE_GITHUB_OAUTH_CLIENT_ID for GitHub). Needs GitLab 17.1+ with the device flow enabled on the app; requests read_repository write_repository and reports username oauth2. Self-hosted GitLab uses KLANGKWS_FEATURE_OAUTH_PROVIDERS (its host is not gitlab.com). Also settable under features_config: (as gitlab_oauth_client_id); env wins per key. See GitHub Authentication.
KLANGKWS_FEATURE_OAUTH_PROVIDERS JSON list of OAuth providers for the git-credential feature ([{host, flow, client_id, token_url, scope, username}]) — activates an OAuth flow for any git host (GitLab, Gitea, self-hosted), not just GitHub. flow is device_code (default; requires device_code_url) or authorization_code_pkce (requires authorize_url + redirect_uri; the authorization-code browser flow with PKCE S256, for hosts like Gitea that implement no device flow — the authorize popup opens on first auth and the tab cache refreshes the token headlessly; see Gitea). An entry whose host matches the remote wins over the KLANGKWS_FEATURE_GITHUB_OAUTH_CLIENT_ID shorthand. Host matching ignores case, explicit port, trailing dot, and a www. prefix. scope (omitted when empty) and username (default oauth2) are optional. Malformed JSON disables the whole map (PAT dialog fallback). Also settable under features_config: (as oauth_providers); env wins per key. See GitHub Authentication.
KLANGKWS_FEATURE_SOLIPLEX_URL Soliplex RAG server endpoint URL. Declared by the (vendored) soliplex feature (features/soliplex/); resolved server-side and surfaced to the UI as soliplex_url via GET /api/v1/config when the feature is active. Set via the env var, or under the features_config: block as soliplex_url (see Configuration File); env wins per key. Renamed from SOLIPLEX_URL to the KLANGKWS_FEATURE_ namespace.
KLANGKD_DISABLE_REGISTRATION Set to 1/true/yes to block new user signups and hide the registration link in the UI (any other value, including empty, leaves registration open)
KLANGKD_DISABLE_INVITES Set to 1/true/yes to disable the invitation system (any other value, including empty, leaves invitations enabled)
KLANGKD_INVITE_EXPIRE_HOURS 72 How long invitation links remain valid (hours)
KLANGKD_ALLOW_AUTOSTART Set to 1/true/yes to allow workspaces to be configured for auto-start on server boot. Default: disabled — the auto-start option is hidden in the UI, CLI, and API. See Auto-start.
KLANGKD_ALLOW_SUDO true Ceiling for per-workspace sudo: permits a workspace to opt in to passwordless sudo (allow_sudo: true in its settings bag) but grants nothing by itself — sudo is off for a workspace unless it opts in. Default: enabled (1/true/yes); set 0/false/no to forbid the opt-in entirely — an explicit deny rule (!ALL) is then written even for opted-in workspaces. Configured at container start via podman exec --user root. Applies to containers started after the change.
KLANGKD_PER_HANDLE_HOME false Ceiling for per-workspace per-handle homes: true permits workspaces to choose the per-handle layout (each connecting user gets a private /home/.users/{id} directory with a /home/{handle} symlink); false — the default — gives every workspace the shared klangk home, regardless of the per-workspace per_handle_home value (POST /workspaces / PUT /workspaces/{id}): a stored true is inert while the ceiling is off, clamped at the next connect/start (no DB rewrite, no 400 — the same shape KLANGKD_ALLOW_SUDO got in #3047). With the ceiling on, an omitted create field still stores this flag's value, so an untouched create gets per-handle homes. Reloadable on SIGHUP (applies to containers started after the reload). Hardened deploys (e.g. FIPS hosts that need one auditable home) leave it off.
KLANGKD_CLASSIFICATION_BANNER Deploy-wide default classification marking shown as a persistent banner at the top and bottom of the web workspace page and as a status line in the TUI (marking sensitive/classified output). Free text (e.g. UNCLASSIFIED, CUI, SECRET). A workspace overrides it via classification_banner on POST /workspaces / PUT /workspaces/{id} (klangk create/edit --classification-banner, or the create/edit UIs); workspaces without their own marking inherit this value at display time. Empty/unset (the default) renders no banner and reserves no screen space. Reloadable on SIGHUP. Downloaded/exported files are not marked — the screen banner is the scope.
KLANGKD_DISABLE_TMUX Set to 1/true/yes to drop new terminals straight into a plain login shell instead of tmux. Only affects the default per-user terminal; shared/joined terminals always use tmux.
KLANGKD_BROWSER_DELEGATE_ENABLED true Master switch for the browser-delegate bridge — the workspace-token-gated /api/v1/browser-delegate{,/stream} endpoints that let a container drive the user's browser tab (fetch with the user's cookies, clipboard, feature actions) and read back everything it renders: a workspace-data read channel that bypasses file permissions entirely. Defaults to true (the bridge ships on); set false for hardened deploys to: return 403 from both endpoints, stop registering browser tabs for bridge routing, and stop attaching a browser ID into terminals started after the change — an already-running terminal keeps its previously attached ID (klangk-browser-id still prints it), but its bridge POSTs get the same 403. Also advertises browser_delegate_enabled: false via /api/v1/config so the web UI stops answering bridge requests. Reloadable on SIGHUP (a disable 403s new requests immediately — a stream already past the gate streams to its idle timeout; a re-enable arms the server from the next terminal_start, and browser tabs resume answering after a page reload). See Browser Bridge.
KLANGKD_BRIDGE_TIMEOUT_SECONDS Idle timeout (seconds) for browser-delegate bridge streams (Browser Bridge). Precedence: per-workspace bridge_timeout setting > this deploy default > 30s. Reloadable on SIGHUP.
KLANGKD_HEALTH_CHECK_INTERVAL 30 Seconds between workspace container health checks (Health check). Unset → 30s. Reloadable on SIGHUP.
KLANGKD_HEALTH_CHECK_TIMEOUT 10 Seconds a single health-check probe may run before it counts as failed (Health check). Unset → 10s. Reloadable on SIGHUP.
KLANGKD_HEALTH_CHECK_STARTUP_GRACE 30 Grace window (seconds) after container start during which failed health checks don't count toward unhealthy (Health check). Unset → 30s. Reloadable on SIGHUP.
KLANGKD_NETFILTER_ENABLED true Master on/off switch for per-workspace egress filtering. Defaults to true. A workspace that declares allowed_domains is filtered via the network sidecar (network_sidecar_image); set false to disable the feature entirely. When disabled, a workspace with allowed_domains fails to start (fail-closed) rather than running unrestricted. This is the operator intent; the /api/v1/config field of the same name is the resolved armed status (intent AND network_sidecar_image set). Reloadable on SIGHUP. See Egress Filtering.
KLANGKD_NETFILTER_DEFAULT_DOMAINS (unset) Deploy-wide egress allow-list applied to every workspace that doesn't declare its own. A workspace with a non-empty allowed_domains overrides (replaces) this default; one with none inherits it. Unset preserves the per-workspace-only behavior (empty = unrestricted). Comma-separated host / host:port / IPv4 CIDR specs (e.g. 10.0.0.0/8, 10.0.0.0/8:443) (env var) or a YAML list (netfilter_default_domains); validated server-side, and a malformed value aborts startup (a SIGHUP reload with a bad value is denied, leaving the runtime on last-known-good config — reversing the fall-back). Read at boot and on SIGHUP (reloadable). See Egress Filtering.
KLANGKD_NIX_ENABLED false Master on/off switch for the per-workspace /nix feature. Defaults to false: the nix toggle is absent from every create/edit surface (web, TUI, CLI — they read the resolved armed status from the authenticated-only /api/v1/config field nix_available), the API rejects a new nix: true opt-in with a clear error (an edit echoing an already-stored value is tolerated), and a workspace start with a stored nix flag proceeds without the /nix mount (logged once at info; re-enabling resumes it — the per-workspace layers persist, and workspace delete still tears them down while off). Set true (together with a nix_seed block, Nix workspaces) to arm the feature. Reloadable on SIGHUP.
KLANGKD_EGRESS_CONSENT_RETENTION_DAYS 30 Retention window for the egress_consent table: terminal rows (static policy records, expired, revoked, elapsed timed verdicts, stale pendings) older than this many days are deleted by the consent monitor's sweep (at startup, then hourly on a wall-clock deadline — event traffic never postpones it). Verdicts still in effect (forever/tilrestart, or a timed window not yet elapsed) are enforcement state and are never pruned — they leave via workspace deletion or the tilrestart reap. 0 disables retention. Reloadable on SIGHUP (applies on the next sweep). See Egress Filtering.
KLANGKD_EGRESS_CONSENT_ROW_CAP 2000 Per-workspace cap on total egress_consent rows: when a workspace exceeds it, the oldest prunable rows (never in-effect verdicts, never live pendings) are trimmed down to the cap by the same sweep — belt-and-suspenders against a flood of decided requests outpacing age-based retention. 0 disables the cap. Reloadable on SIGHUP (applies on the next sweep). See Egress Filtering.
KLANGKD_NETWORK_SIDECAR_IMAGE klangk-network-sidecar Container image for the FQDN network sidecar: filtered workspaces run two containers sharing a netns — this sidecar (NET_ADMIN, runs the DNS proxy) plus the workspace (--network container:<sidecar>). Set to an empty value to disable egress filtering entirely. Reloadable on SIGHUP. See Egress Filtering.
KLANGKD_CONTAINER_CPU_LIMIT 2.0 Deploy-wide CPU cap applied to every workspace container via podman --cpus. Non-empty default: a fresh deployment caps each container at 2 CPUs. A finite positive float (number of CPUs, e.g. 1.5 = one and a half cores). Explicitly emptied = no flag = unbounded. A malformed value (non-numeric, non-finite, or <= 0) aborts startup — a safety control that silently disables itself on a typo is worse than none — and a malformed SIGHUP reload is denied, leaving the runtime on the last-good config. Applies to containers started after the change; an existing container keeps its original cgroup limit for the rest of its life. Reloadable on SIGHUP.
KLANGKD_EGRESS_CONSENT_RATE_LIMIT 50 Interactive-mode consent monitor tuning: cap on pending egress-consent requests per workspace — an anti attention-flood bound against adversarial containers. Reloadable on SIGHUP. See Egress Filtering.
KLANGKD_NIX_SEED (nested) Seed configuration for the per-workspace /nix feature (Nix workspaces): a nested block with type and path (env form KLANGKD_NIX_SEED__TYPE / KLANGKD_NIX_SEED__PATH, config-file form nix_seed:). Omit the block or leave path unset to disable the feature; nix_enabled must also be true.
KLANGKD_EGRESS_CONSENT_TIMEOUT 120 How long (seconds) an egress-consent request stays pending before the monitor auto-expires it (DECISION_EXPIRED). The default (120s) matches the kernel's ~127s connect timeout now that consent gates the connection SYN. Reloadable on SIGHUP. See Egress Filtering.
KLANGKD_CONSENT_DECIDER_TIMEOUT 45 Consent-decider liveness window: a decider (live client that can approve/deny held egress) whose last ping is older than this many seconds is reaped, so an unclean disconnect can't leave a workspace falsely "interactive". Reloadable on SIGHUP. See Egress Filtering.
KLANGKD_CONTAINER_MEMORY_LIMIT 8g Deploy-wide RAM cap applied to every workspace container via podman --memory. A positive size matching docker/go-units ParseSize grammar: a number with an optional unit suffix — a single base unit b/k/m/g/t/p (case-insensitive) plus an optional trailing b, so 2g, 2gb, 512m, 512mb, 2t, 1024 (bare bytes), 1.5g all pass; the IEC i-forms (kib/gib/…) are not accepted. 0 is rejected (podman treats --memory=0 as “no limit”; unset the var instead). Non-empty default: a fresh deployment caps each container at 8 GB; explicitly emptied = no flag = unbounded. A malformed value aborts startup (and is denied on SIGHUP); the runtime check is a syntax guard only — podman remains the authority on what cgroups can actually apply (no cgroups v2, rootless without delegation, etc.) and fails loudly at workspace-start if it can't honour the value. Applies to containers started after the change. Reloadable on SIGHUP.
KLANGKD_CONTAINER_PIDS_LIMIT 16384 Deploy-wide process-count cap applied to every workspace container via podman --pids-limit — the best fork-bomb defense for the effort. Non-empty default: a fresh deployment caps each container at 16384 processes. A positive integer (e.g. 512). Explicitly emptied = no flag = unbounded. A malformed value (non-integer, or <= 0) aborts startup (and is denied on SIGHUP). Applies to containers started after the change. Reloadable on SIGHUP.
KLANGKD_CONTAINER_RESTART_ENABLED false Opt-in crash recovery: auto-restart a workspace whose container died unexpectedly (OOM kill, non-zero exit, external removal) after an exponential backoff, with a bounded retry count. Death events/logs carry the classified cause either way — an OOM kill names the workspace's effective memory limit. Expected stops (user stop, idle stop, delete, logout) never restart. Default false: recovery stays manual. Reloadable on SIGHUP; a pending restart re-checks the flag when it fires. See Crash recovery.
KLANGKD_CONTAINER_RESTART_MAX_RETRIES 5 Bound on restart attempts per crash episode. Exhausting the budget leaves the workspace stopped in a visible crash-loop state (surfaced on GET /workspaces/<id>/status as restart.state) instead of spinning forever. The counter resets after the container stays up 10 minutes, or on any user-driven start/stop. Positive integer; a malformed value aborts startup.
KLANGKD_CONTAINER_TMP_SIZE 2g Per-workspace /tmp tmpfs size via podman --tmpfs /tmp:...,size=<n>. Default 2g preserves the previous hardcoded mount size; a workspace may override it via its settings bag (tmp_size). Applies to containers started after the change. Reloadable on SIGHUP.
KLANGKD_CONTAINER_EVENTS_RETENTION_DAYS 90 Retention window for the container_events audit table: rows older than this many days are deleted by the retention sweep (at startup, then hourly on a wall-clock deadline) — an upgrade over a bloated table trims on the first sweep. Every row is history, so unlike egress_consent there is no in-effect exemption. 0 disables retention. Reloadable on SIGHUP (applies on the next sweep).
KLANGKD_CONTAINER_EVENTS_ROW_CAP 10000 Deploy-wide cap on total container_events rows: when the table exceeds it, the oldest rows are trimmed down to the cap keeping the newest (same created_at DESC, id DESC ordering the events API uses), belt-and-suspenders against a flood of lifecycle events outpacing age-based retention. The cap is deploy-wide, not per-workspace — an audit log bounds the whole table. 0 disables the cap. Reloadable on SIGHUP (applies on the next sweep).
KLANGKD_AUDIT_EVENTS_RETENTION_DAYS 365 Retention window for the audit_events identity/privilege audit table (#3205): rows older than this many days are deleted by the retention sweep (at startup, then hourly). The table records account create/update/delete, group/ACL/workspace-role changes, login/logout/failed-login, session revocation, and the data-level file events (#3257: workspace archive export/import, downloads and text reads, uploads, renames, and deletes through the files API, each with the path and byte size — the export's size is a pre-flight estimate) — every row with actor, target, and the request's source IP, user agent, HTTP method, and Referer (#3255). Login events are lower-volume than container transitions but are where an incident review starts, so the default is longer than container_events'. File events are the highest-frequency class and are bounded by their own row-cap bucket (see KLANGKD_AUDIT_EVENTS_ROW_CAP). 0 disables retention. Reloadable on SIGHUP (applies on the next sweep).
KLANGKD_AUDIT_EVENTS_ROW_CAP 100000 Deploy-wide cap on audit_events rows, applied per class: unauthenticated login.failed rows (the only class an anonymous caller can mint) and the high-frequency file.* rows (#3257) each get their own bucket, so a spray of failed logins or a burst of file traffic evicts at most its own class — never the account/privilege history. When a bucket exceeds the cap, the oldest rows are trimmed keeping the newest (same ordering the /events/audit API uses) — so the table can hold up to three caps' worth of rows, one per bucket. 0 disables the cap. Reloadable on SIGHUP (applies on the next sweep).
KLANGKD_AUDIT_FAIL_CLOSED false Fail-closed container auditing (#3154, security finding): when true, the interactive API container transitions — POST start/stop/restart and delete's stop — write their container_events audit row before acting and refuse the request with a 503 when it cannot be written — never start-then-rollback. Create's eager start is skipped instead (the workspace row is already committed, so it is left created-not-started with a warning — the same shape as a drain/capacity refusal there). WS-connect starts and autonomous transitions (idle timeout, eviction, logout, drain, shutdown sweep, crash teardown, boot reaps, crash-monitor restarts) are never gated: refusing those would keep containers running and lose the record. Default false keeps the best-effort behavior; every audit-write failure is counted either way and surfaced as write_failures on the /audit endpoint (an in-memory counter, zeroed on restart). Reloadable on SIGHUP (applies to transitions started after the reload).
KLANGKD_AUDIT_FORWARD_URL Opt-in native forwarding of the audit records themselves (#3252; STIG SV-222481/482). When set, klangkd ships every new row of the three audit tables — audit_events (identity/privilege events), container_events (container lifecycle), egress_consent (egress verdicts) — to this endpoint as JSON POST bodies ({"records": [...]}, one batch per few seconds, in-order per table, at-least-once; an egress_consent row ships once, at creation — typically as a pending request — and a later decision on the row is an update, not a new row, so it does not forward again). Use an https:// URL for any endpoint off the host — records name users and actions; http:// is accepted for loopback collectors. Validated at startup (a malformed URL aborts boot). A down target delays delivery (rows stay queued, retried with exponential backoff) and surfaces on /audit under forwarding. Delivery is bounded by the table's own retention and row cap: rows past KLANGKD_AUDIT_EVENTS_RETENTION_DAYS/_ROW_CAP (or the container_events/egress_consent equivalents) are pruned even if a down target has not delivered them yet — size the caps to your worst-case outage. Reloadable on SIGHUP. See Logging. Empty (the default) leaves the forwarder off.
KLANGKD_AUDIT_FORWARD_SYSLOG Second target family for #3252: an RFC 5424 receiver as tcp://host[:port] or tls://host[:port] (a bare host[:port] means tcp; ports default to 514/6514). Each record ships as one syslog line (facility audit, severity info, the source table as MSGID, the full record as the JSON message, newline-framed per the common TCP convention). tls:// verifies the server certificate against the system trust store (put a private CA in the approved baseline via KLANGKD_TRUSTED_CA_DIR). Both target families may be configured — each receives every record. Keeps its own delivery cursor, so a down URL target does not delay it; same queueing/backoff//audit behavior as the URL target; empty (the default) leaves the syslog channel off. Reloadable on SIGHUP.
KLANGKD_AUDIT_FORWARD_HEADER One optional HTTP header sent with every URL-target POST (#3252), as Name: value — the auth a collector typically requires (Splunk HEC: Authorization: Splunk <token>; bearer collectors: Authorization: Bearer <token>). Applies to the URL target only. Validated at startup (a malformed header aborts boot). Reloadable on SIGHUP. Empty (the default) sends no extra header.
KLANGKD_CONTAINER_RESTART_BACKOFF_SECONDS 5.0 Base delay for the restart backoff: attempt n waits base * 2^(n-1) seconds, capped at 60s (5s → 10s → 20s → 40s → 60s …). Positive float; a malformed value aborts startup. Reloadable on SIGHUP.
KLANGKD_FIPS_MODE false FIPS enforcement: when on, every workspace container must prove an actively-enforcing OpenSSL FIPS provider at start (distro-agnostic probe — a non-approved digest rejected on the OpenSSL fetch path, or an SHA-2-only fips=yes approved set via the openssl CLI); failure or non-verifiability fails closed (container removed, start refused). The klangkd process's own OpenSSL is probed once at startup: on a control host the result is logged (warning on failure); inside a container (docker host-container deployment) a failed probe aborts the boot. Use with the FIPS workspace image (klangk:build-fips-image) and, when containerized, the FIPS host image (klangk:build-fips-host-image). See FIPS 140-3 Mode.
KLANGKD_MEMORY_EVICTION_ENABLED true Host memory-pressure eviction: when availability stays below KLANGKD_MEMORY_EVICTION_THRESHOLD_PERCENT for KLANGKD_MEMORY_EVICTION_SUSTAIN_POLLS consecutive polls, gracefully stop the least-recently-active workspace with no connected clients (one per poll) until availability recovers to the recovery threshold. Workspaces with live clients and workspaces pinned never-stop (idle_timeout 0, e.g. auto-started boot services) are never chosen. Reloadable on SIGHUP.
KLANGKD_MEMORY_EVICTION_THRESHOLD_PERCENT 10.0 Pressure threshold (percent of total memory) that opens an eviction episode; availability below this for SUSTAIN_POLLS consecutive polls starts evictions. Platform-aware measurement: MemAvailable/MemTotal on Linux, the more-pressured of meminfo and cgroup headroom inside a memory-limited container (Docker -m), vm_stat+sysctl on macOS — capped there by the podman machine's configured memory (podman machine ls, cached 5 minutes), because containers run in that VM and its default 2048 MiB is far below the Mac's RAM. The percent scales with RAM: 10% of a 256 GB server is 25.6 GB (effectively never triggers — fine, the OOM killer is not near), while 10% of a 4 GB dev laptop is ~400 MB and may trigger during heavy local use — tune up or set KLANGKD_MEMORY_EVICTION_ENABLED=false on small dev machines. Positive float; recovery must be >= threshold. Reloadable on SIGHUP.
KLANGKD_MEMORY_EVICTION_RECOVERY_PERCENT 15.0 Availability an eviction episode must see (percent of total memory) before it ends — the gap to the threshold is the hysteresis that prevents flap-eviction at the boundary. Must be >= the threshold; equal values mean no gap (operator's explicit choice). Reloadable on SIGHUP.
KLANGKD_MEMORY_EVICTION_SUSTAIN_POLLS 3 Consecutive below-threshold polls before the first eviction; a transient spike never evicts. Integer >= 1. Reloadable on SIGHUP.
KLANGKD_MEMORY_EVICTION_POLL_INTERVAL 10.0 Seconds between availability measurements; floored at 1s — smaller configured values are clamped. Positive float. Reloadable on SIGHUP.
KLANGKD_RESOURCE_WATCHDOG_ENABLED true Resource detection (#3206, #3309): every KLANGKD_RESOURCE_WATCHDOG_POLL_INTERVAL seconds the loop checks four surfaces — statvfs the filesystems holding the data directory (where the audit records live), the state directory, the podman container-storage root, and any KLANGKD_DISK_WATCHDOG_PATHS entries, deduplicated by device; the memory utilization of the machine containers run on (see KLANGKD_MEMORY_WATCHDOG_WARN_PERCENT); CPU pressure (see KLANGKD_CPU_WATCHDOG_WARN_PERCENT); and the audit-write-failure counters. Threshold crossings send resource.disk.* / resource.memory.* / resource.cpu.* notifications to the admin channels (Notifications) on state transitions — usage hovering at a boundary holds its state (hysteresis bands below both thresholds), a metric that stays degraded refreshes its alert once per 5 minutes so a swallowed transition dispatch is late, never lost, and a measurement that cannot be taken is logged at WARNING and skipped, never blocking anything. Turning this off stops all four surfaces (the write sites' own per-failure audit.failure events keep firing). With no notification channel configured the watchdog only logs. The same last-known state drives GET /health (#3308): any warn/critical metric or an audit-write-failure episode in the latest window flips the status to degraded (HTTP stays 200) with a per-metric detail block. Reloadable on SIGHUP.
KLANGKD_DISK_WATCHDOG_WARN_PERCENT 75.0 Filesystem usage (percent) that enters the warn state — 75% mirrors the ASD STIG storage-capacity warning allocation (SV-222483). Recovery requires usage to fall to 5 percentage points below this threshold (the fixed hysteresis gap), so a boundary-hovering level sends one alert per episode rather than one per poll. Values below 5 are rejected — the gap needs room below the threshold, and a smaller warn would leave the recovery floor beneath 0% where usage can never reach. Must be ≤ the critical threshold. Reloadable on SIGHUP.
KLANGKD_DISK_WATCHDOG_CRITICAL_PERCENT 90.0 Filesystem usage (percent) that enters the critical state (SV-222668 low-resource alerting). A critical filesystem eases back to warn at 5 percentage points below this threshold (the same hysteresis gap), so usage hovering at the critical boundary holds the critical state. Must be in [KLANGKD_DISK_WATCHDOG_WARN_PERCENT, 100]; equal values mean the warn state is skipped (OK↔critical only). Reloadable on SIGHUP.
KLANGKD_DISK_WATCHDOG_PATHS Extra filesystems to monitor — any path on each filesystem suffices (e.g. a backup mount). Comma-separated env var or YAML list; a path on an already-monitored filesystem (same device) is ignored, as is a path that cannot be statvfs'd (warned once). Empty (the default) monitors the data directory and container storage only. Reloadable on SIGHUP.
KLANGKD_MEMORY_WATCHDOG_ENABLED true Turns the resource watchdog's memory surface on and off (KLANGKD_RESOURCE_WATCHDOG_ENABLED stays the master switch for the whole loop). While off, remembered memory state is dropped, so re-enabling evaluates fresh: memory that filled while the surface was off alerts on the first poll after. Reloadable on SIGHUP.
KLANGKD_MEMORY_WATCHDOG_WARN_PERCENT 80.0 Memory utilization (percent) of the machine containers run on that enters the warn state. Measured as 100 × (1 − availability) from MemAvailable/MemTotal on a Linux host (pressed by the cgroup limit when klangkd itself runs memory-capped); on macOS the podman machine VM's own /proc/meminfo is read via podman machine ssh, because containers live in that VM — the Mac host's numbers are the wrong machine. Recovery requires utilization to fall 5 percentage points below the threshold (the shared hysteresis gap). Values below 5 are rejected; must be ≤ the critical threshold. Reloadable on SIGHUP.
KLANGKD_MEMORY_WATCHDOG_CRITICAL_PERCENT 90.0 Memory utilization (percent) that enters the critical state. The default 90 aligns with the eviction loop's availability floor (KLANGKD_MEMORY_EVICTION_THRESHOLD_PERCENT, default 10%): the critical alert fires as eviction starts. A critical reading eases back to warn 5 percentage points below this threshold. Must be in [KLANGKD_MEMORY_WATCHDOG_WARN_PERCENT, 100]. Reloadable on SIGHUP.
KLANGKD_CPU_WATCHDOG_ENABLED true Turns the resource watchdog's CPU-pressure surface on and off (KLANGKD_RESOURCE_WATCHDOG_ENABLED stays the master switch). While off, remembered CPU state is dropped, so re-enabling evaluates fresh. Reloadable on SIGHUP.
KLANGKD_CPU_WATCHDOG_WARN_PERCENT 30.0 CPU pressure (PSI some avg60 percent — the share of time at least one task was stalled on CPU over the last minute) that enters the warn state. PSI is read from /proc/pressure/cpu on a Linux host and inside the podman machine VM on macOS (podman machine ssh); a kernel or VM without PSI disables the CPU check with one logged warning (re-armed if the file appears). avg60 is already a 60-second average, so a single-poll crossing is sustained pressure, not a scheduler spike. Recovery requires the reading to fall 5 percentage points below the threshold. Values below 5 are rejected; must be ≤ the critical threshold. Reloadable on SIGHUP.
KLANGKD_CPU_WATCHDOG_CRITICAL_PERCENT 60.0 CPU pressure (PSI some avg60 percent) that enters the critical state. Eases back to warn 5 percentage points below this threshold. Must be in [KLANGKD_CPU_WATCHDOG_WARN_PERCENT, 100]. Reloadable on SIGHUP.
KLANGKD_RESOURCE_WATCHDOG_POLL_INTERVAL 60.0 Seconds between watchdog polls — all four surfaces share the one loop; floored at 1s (smaller configured values are clamped). The first sweep runs at startup, so a host restarted because a resource filled alerts immediately. Positive float. Reloadable on SIGHUP.
KLANGKD_ADMISSION_MEMORY_ENABLED false Admission control: before creating a workspace container, check that available host memory fits the workspace's resolved KLANGKD_CONTAINER_MEMORY_LIMIT (per-workspace memory_limit override wins) plus KLANGKD_ADMISSION_MEMORY_MARGIN. A start that does not fit fails fast with a clear 503 / WebSocket error ("host at capacity: 1.2 GB available, workspace wants 4 GB") instead of deferring the failure to the kernel OOM killer. Runs at the single start choke point, so every path is covered (API start/restart, WS connect, create's eager start, boot auto-start, crash-recovery restart); a reconnect to an already-running workspace is never re-admitted. Skipped when no memory limit is configured; fails open (start allowed, one-time warning) when memory cannot be measured. Concurrent sibling starts have their resolved limits subtracted from the measured availability, so N simultaneous starts cannot each fit against the same stale reading. WebSocket refusals carry the machine-readable error code capacity. Caveat: when klangkd itself runs under a cgroup memory limit (Docker -m, a systemd slice MemoryMax), the measurement reads that cgroup's headroom — a slice tighter than the workspaces' needs systematically refuses starts; size the slice or disable the check. Default false because the check is advisory against the limit — with the default 8g limit, small dev/CI hosts would be refused every start; multi-user deployments should enable it with limits sized to the host. Reloadable on SIGHUP. See Workspace Admission.
KLANGKD_ADMISSION_MEMORY_MARGIN 1g Reserve kept for the server itself (klangkd, the proxy, page cache) when fitting a workspace's memory limit against available host memory. Podman size-string grammar (same as KLANGKD_CONTAINER_MEMORY_LIMIT: 1g, 512m, 1024, 1.5g …); unset/empty = no reserve (fit against the bare limit). Malformed or 0 aborts startup (and is denied on SIGHUP). Reloadable on SIGHUP.
KLANGKD_MAX_RUNNING_WORKSPACES_PER_USER 0 Per-user cap on concurrently running workspaces, the k8s ResourceQuota analogue: checked at start time; a user at the cap gets a clear "stop a workspace first" 503 / WebSocket error instead of an overloaded host. Counts the owner's workspaces that are running or mid-start/stop (which closes the two-workspaces-starting-at-once race); a workspace being restarted by its owner is not double-counted. 0 (the default) = unlimited. Reloadable on SIGHUP. See Workspace Admission.
KLANGKD_VOLUME_QUOTA_PER_WORKSPACE 0 Per-workspace cap on instance-managed named volumes (#3153 — volumes are workspace-owned): each volume is a directory tree on the host's storage, so without a cap a workspace's editors can consume unbounded disk via mounts. Enforced at BOTH doors that mint volumes — POST /api/v1/volumes and the workspace-start auto-create of mounted named volumes (mounts: entries) — and a create past the cap returns 429 (start: a clear start error) with a message naming this setting. A per-workspace lock spans count+create, so concurrent creates (API or start) cannot jointly exceed the cap. Counts the workspace's volumes carrying this instance's klangk.instance label and its klangk.workspace-id label; other workspaces' and other instances' volumes never count. 0 (the default) = unlimited, and the create path makes no extra podman call. Reloadable on SIGHUP.
KLANGKD_OIDC_CONFIG Path to OIDC provider config file (YAML or JSON). Enables OIDC authentication when set. See OIDC Configuration.
KLANGKD_OIDC_LOGIN_HOOK File path to a Python login hook script (e.g., /etc/klangk/login_hook.py). Optionally append :func_name (defaults to on_login). Called on each OIDC login to enforce custom policies. See OIDC Configuration.
KLANGKD_WORKSPACE_CREATED_HOOK File path to a Python workspace-created hook script (e.g., /etc/klangk/workspace_created.py). Optionally append :func_name (defaults to on_workspace_created). Called after every workspace creation (create, import, duplicate) to mutate the workspace and/or rewrite its ACL; failures are logged, never fatal. Reloaded on SIGHUP. See Customizing a Deployment.
KLANGKD_AUTH_MODES none Auth modes: password, oidc, both, or none. Defaults to none (no-login single-user, loopback-bound) — OIDC settings (KLANGKD_OIDC_*) never change the mode; a fresh install "just works" locally with no password. Set explicitly to pin a mode; see Auth Modes.
KLANGKD_ALLOW_INSECURE_NO_AUTH Set to 1 to allow KLANGKD_AUTH_MODES=none on a non-loopback browser bind (KLANGKD_LISTEN). By default none mode refuses to start unless KLANGKD_LISTEN is loopback (IPv4 127.0.0.0/8, IPv6 ::1, or localhost). See Auth Modes.
KLANGKD_SMTP_HOST SMTP server hostname (if set, uses SMTP; otherwise uses sendmail)
KLANGKD_SMTP_PORT 587 SMTP server port
KLANGKD_SMTP_USER SMTP auth username
KLANGKD_SMTP_PASSWORD SMTP auth password
KLANGKD_SMTP_FROM Email sender address (falls back to SMTP_USER, then noreply@localhost)
KLANGKD_SMTP_USE_TLS true Use STARTTLS for SMTP
KLANGKD_SMTP_REPLY_TO Reply-To address on outgoing email; unset uses the From address.
KLANGKD_SENDMAIL_PATH sendmail Path to sendmail binary (used when KLANGKD_SMTP_HOST is not set)
KLANGKD_EMAIL_TEMPLATES_DIR Deprecated — use <KLANGKD_CUSTOMIZE_DIR>/email-templates/ instead. Directory of Jinja2 email template overrides. When unset, falls back to <KLANGKD_CUSTOMIZE_DIR>/email-templates/. See Email Templating.
KLANGKD_ADMIN_NOTIFICATION_EMAILS Comma-separated SA/ISSO email addresses receiving security-relevant notifications (#3250): account lifecycle events (user.create, user.register, user.update, user.delete, user.unlock, user.disable, user.enable), credential and email/handle changes, group membership changes, audit-write failures, and memory-capacity start refusals. Delivery uses the same SMTP/sendmail transport as the auth emails (KLANGKD_SMTP_HOST / KLANGKD_SENDMAIL_PATH). Empty (the default) leaves the email channel off. Reloadable on SIGHUP.
KLANGKD_ADMIN_NOTIFICATION_WEBHOOK_URL URL of the endpoint that receives one JSON POST per notification (#3250; body: event name, timestamp, actor, target, detail, source IP). Use an https:// URL for any endpoint that leaves the host — the notification body names users and actions. Sends use a short timeout with no retries; a failed delivery is logged at WARNING and never blocks the action it annotates. Empty (the default) leaves the webhook channel off. Reloadable on SIGHUP.
KLANGKD_ADMIN_NOTIFY_EVENTS every supported event Allowlist of event types that trigger notifications, comma-separated (#3250). The default is every supported event; the persistent conditions audit.failure and resource.low notify at most once per 5 minutes — audit.failure per source table (audit_events vs container_events) — so a degraded state alerts once, not on every occurrence. An unknown event name aborts startup (a typo would silently disable a security notification); blanking the variable restores the default allowlist, while a config-file admin_notify_events: [] explicitly turns event notifications off (the channels stay configured). Reloadable on SIGHUP.
KLANGKD_PRODUCT_NAME Klangk White-label product name shown in the browser tab title, app-bar logo, and outgoing emails. Supports file:/cmd: prefix. See Customizing a Deployment.
KLANGKD_LOGIN_BANNER_TITLE Title shown on the consent banner page (e.g., company name). If empty, no title is displayed.
KLANGKD_LOGO_URL URL of the deployment logo shown in the browser UI (empty = built-in mark). See Customizing a Deployment.
KLANGKD_BRAND_COLOR #E65100 Theme accent color for the browser UI (hex form). See Customizing a Deployment.
KLANGKD_LOGIN_BANNER Consent banner text shown before login. Blocks all access until accepted. Supports file:/cmd: prefix. If empty, no banner is shown.
KLANGKD_LOGIN_BANNER_EVERY_VISIT false When true, the consent banner must be re-accepted on every fresh app load / login (acceptance is tracked for the session only, never persisted). When false (default), acceptance is cached permanently against the banner text hash. See Consent Banner.
KLANGKD_TERMS_URL Terms of Service link rendered on the login/registration screens and in email footers. Plain value (no file:/cmd: resolution — it is public, shown pre-auth). Empty hides it.
KLANGKD_PRIVACY_URL Privacy Policy link. Same semantics as KLANGKD_TERMS_URL (plain value, hidden when empty).
KLANGKD_AUP_URL Acceptable Use Policy link. Same semantics as KLANGKD_TERMS_URL.
KLANGKD_SUPPORT_URL Support/help link shown as a help icon in the app bar (authenticated) and on the auth screens. Plain value; hidden when empty and no KLANGKD_SUPPORT_EMAIL is set.
KLANGKD_SUPPORT_EMAIL Support email address; shown as a mailto: when KLANGKD_SUPPORT_URL is unset. Plain value; hidden when empty.
KLANGKD_TERMINAL_BANNER Text displayed in every terminal session on shell init. Supports file:/cmd: prefix. If empty (default), no banner is shown.
KLANGKD_WEBSOCKET_DEBUG Set to any non-empty value to enable verbose WebSocket message logging on the backend. Useful for diagnosing connection issues.
KLANGKD_LOG_LEVEL INFO Root logger level for the klangkd backend — a level name (DEBUG/INFO/WARNING/ERROR/CRITICAL, any case) or a numeric string. Applied at boot and re-applied on every SIGHUP reload, so the level can be changed without a restart. A malformed value aborts startup (fail-fast).
KLANGKD_LOG_FORMAT text Root logger output format for the klangkd backend — text (colored console format) or json (one JSON object per line: ISO-8601 UTC timestamp, level, logger, message, host (the emitting machine's hostname), and instance (the per-data-dir klangk instance id from <data_dir>/instance-id, the id the audit trail's app lifecycle rows will key on (#3329)) — plus exc_info when present) for SIEM/central-log ingestion. Applies to the whole stream — klangk, uvicorn (startup/error/access), and other third-party records alike — and contains no ANSI codes. Applied at boot and re-applied on every SIGHUP reload; a malformed value aborts startup (fail-fast). Lines emitted before settings load (early boot) are always text.
KLANGKD_LOG_FILE Optional path to an always-JSON log sink: when set, every record at or above KLANGKD_LOG_LEVEL is additionally written to this file as one JSON object per line (same payload as KLANGKD_LOG_FORMAT=json), while the console keeps KLANGKD_LOG_FORMAT — so stdout can stay human-readable text while the file feeds a SIEM via rsyslog imfile/fluent-bit. ~ is expanded; relative paths resolve against the process working directory (the systemd unit's). The path is probed for writability at construction (a bad path aborts startup, fail-fast). The sink follows external rotation (logrotate rename) by reopening on inode change. Reloadable on SIGHUP — a path change closes the old sink and opens the new one; a path that breaks between validation and use is logged at warning (or suspends the sink with one warning, leaving the console stream live).
KLANGKD_LOG_FILE_MAX_BYTES 0 Size trigger for in-app rotation of the KLANGKD_LOG_FILE sink: once the file reaches this many bytes it is rolled over to <path>.1 (existing .N shift up; oldest deleted per KLANGKD_LOG_FILE_BACKUP_COUNT). A file may overshoot by one record (checked before each write). 0 (the default) = never self-rotate — external rotation (logrotate/rsyslog) stays the mechanism. With any rotation trigger set the app owns rotation; keep external rotators off the same path. Reloadable on SIGHUP; a negative or non-integer value aborts startup (fail-fast). Only meaningful with KLANGKD_LOG_FILE set.
KLANGKD_LOG_FILE_ROTATE Time trigger for in-app rotation of the KLANGKD_LOG_FILE sink: hourly, daily, weekly, or monthly (case-insensitive), rolled at UTC boundaries (weekly = Monday). Empty (the default) = no time trigger; size and time triggers can be combined (either fires). Reloadable on SIGHUP; anything else aborts startup (fail-fast). Only meaningful with KLANGKD_LOG_FILE set.
KLANGKD_LOG_FILE_BACKUP_COUNT 3 How many rotated <path>.N files to keep when a rotation trigger fires (oldest deleted first). 0 discards the rotated file outright. Reloadable on SIGHUP; a negative value aborts startup (fail-fast). Only meaningful with a rotation trigger set.
KLANGKD_TEST_MODE Internal/test-harness flag: when truthy, registration auto-verifies so E2E tests get immediate access. Not for production use.
LOGFIRE_TOKEN Pydantic Logfire write token (opt-in)
KLANGKD_VERSION_FILE Path to a version.json served from GET /api/v1/version instead of the packaged one (dev/CI override).
LOGFIRE_BASE_URL https://logfire-api.pydantic.dev Logfire API base URL (for self-hosted instances)
LOGFIRE_ENVIRONMENT Logfire environment tag (e.g., production, staging) — filters traces in the dashboard.