Configuration
Precedence
Section titled “Precedence”Lowest to highest:
- Built-in defaults
/etc/kuben/config.toml./kuben.toml(the nearest one: the working directory, else its parents)--config <file>orKUBEN_CONFIG(if given)KUBEN_*environment variables. Nested keys are separated by__:KUBEN_SECURITY__SESSION_TTL_HOURS. A value may be written as in TOML: a list[a, b], a table{admins = "admin"},true, a number; anything else is a string.
Keys Kuben does not read are ignored; a value of the wrong type stops the server at start.
A TOML file uses the same sections:
[server]bind = "127.0.0.1:8080"public_url = "https://kuben.example.com"
[database]url = "postgres:///kuben?host=/run/postgresql&user=kuben"
[security]trust_forwarded_for = trueserver
Section titled “server”| Variable | Default | Purpose |
|---|---|---|
KUBEN_SERVER__BIND |
0.0.0.0:8080 |
API and console |
KUBEN_SERVER__METRICS_BIND |
0.0.0.0:9090 |
Prometheus /metrics |
KUBEN_SERVER__ACTIVATOR_BIND |
0.0.0.0:8081 |
reserved for the scale-to-zero activator |
KUBEN_SERVER__PUBLIC_URL |
unset | the URL users open; used for absolute links |
KUBEN_ROLES |
all |
roles of this process: all, api, controller, activator (comma-separated) |
KUBEN_SERVER__REQUEST_TIMEOUT_SECS |
30 |
per-request timeout (streams excepted) |
KUBEN_SERVER__MAX_BODY_BYTES |
1048576 |
request body limit (1 MiB) |
KUBEN_SERVER__STATE_DIR |
see below | files of this installation: the setup token, a generated admin password, the secret keyring and backups by default |
KUBEN_ROLES (or kuben serve --roles) overrides server.roles (KUBEN_SERVER__ROLES, a list: ["api", "controller"]).
Without state_dir: /data when it exists (the container image and the chart), else systemd’s $STATE_DIRECTORY, $XDG_STATE_HOME/kuben, ~/.local/state/kuben, or the working directory.
database
Section titled “database”| Variable | Default | Purpose |
|---|---|---|
KUBEN_DATABASE__URL |
none, required | a postgres:// or postgresql:// URL, e.g. postgres://user:pass@host/db |
KUBEN_DATABASE__MAX_CONNECTIONS |
4 |
pool size |
Kuben keeps its data in PostgreSQL. There is no default URL: without one the server stops with a message that shows how to run a local PostgreSQL:
docker run -d --name kuben-postgres -e POSTGRES_PASSWORD=kuben -p 5432:5432 postgres:17-alpineexport KUBEN_DATABASE__URL=postgres://postgres:kuben@localhost:5432/postgresSQLite URLs are rejected, and the data of a 1.x install that used SQLite is not carried over.
The container image sets no database; the Helm chart sets KUBEN_DATABASE__URL, to its own PostgreSQL or to database.url. kuben setup writes the URL in the example above: the server’s own PostgreSQL over its Unix socket, with peer authentication and no password. Connect as an ordinary role: kuben doctor warns (database role) when the role is a superuser or has BYPASSRLS, because row-level security is then bypassed.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_KUBE__KUBECONFIG |
unset | path to a kubeconfig; otherwise $KUBECONFIG, then ~/.kube/config, then the pod’s service account |
KUBEN_KUBE__CONTEXT |
unset | kubeconfig context to use |
KUBEN_KUBE__WATCH_NAMESPACE |
unset (all) | restrict informers to one namespace |
KUBEN_KUBE__REQUIRED |
false (true in the chart) |
fail to start when no cluster is reachable |
KUBEN_KUBE__NAMESPACE |
the pod’s namespace | home of the controller Lease and of kuben-initial-admin |
KUBEN_KUBE__LEADER_ELECTION |
false (true in the chart) |
run the controllers only on the Lease holder; required when several processes have the controller role |
security
Section titled “security”| Variable | Default | Purpose |
|---|---|---|
KUBEN_SECURITY__COOKIE_SECURE |
auto |
Secure + __Host- on the session cookie. auto: only when KUBEN_SERVER__PUBLIC_URL is https://, so a plain-http install signs in and turns Secure once a TLS address is set. true behind a TLS proxy without a public URL (the chart’s default); false only for development |
KUBEN_SECURITY__SESSION_TTL_HOURS |
12 |
session lifetime |
KUBEN_SECURITY__SESSION_CACHE_TTL_SECS |
5 |
upper bound for a revoked session to stay valid on another replica |
KUBEN_SECURITY__PASSWORD_MIN_LENGTH |
12 |
|
KUBEN_SECURITY__ARGON2_M_KIB |
19456 |
Argon2id memory per password hash, KiB (19 MiB) |
KUBEN_SECURITY__ARGON2_T |
2 |
Argon2id iterations |
KUBEN_SECURITY__ARGON2_P |
1 |
Argon2id parallelism |
KUBEN_SECURITY__LOGIN_CONCURRENCY |
2 |
concurrent Argon2id verifications; bounds memory under a login flood |
KUBEN_SECURITY__LOGIN_MAX_FAILURES |
5 |
per email + client IP, within the window |
KUBEN_SECURITY__LOGIN_MAX_FAILURES_PER_IP |
30 |
|
KUBEN_SECURITY__LOGIN_MAX_FAILURES_PER_ACCOUNT |
100 |
|
KUBEN_SECURITY__LOGIN_WINDOW_SECS |
900 |
15 minutes |
KUBEN_SECURITY__TRUST_FORWARDED_FOR |
false (true in the chart) |
use the last X-Forwarded-For hop as the client address, and X-Forwarded-Proto for whether the request came over HTTPS. Only behind a proxy that sets them |
KUBEN_SECURITY__TRUSTED_PROXIES |
empty | addresses or CIDRs (10.42.0.0/16) whose forwarded headers are believed; empty: every peer’s, once trust_forwarded_for is on. kuben setup sets the pod network when the console sits behind Kuben’s Gateway |
KUBEN_SECURITY__INSECURE_SETUP |
false |
let /setup take the first admin’s password over plain HTTP from another machine. Otherwise only HTTPS (directly or through a trusted proxy) or a loopback client (an SSH tunnel) may; the page says how |
telemetry
Section titled “telemetry”| Variable | Default | Purpose |
|---|---|---|
KUBEN_TELEMETRY__LOG_FORMAT |
json |
json or pretty |
KUBEN_TELEMETRY__LOG_LEVEL |
info |
trace, debug, info, warn or error; RUST_LOG wins when set. A 1.x filter such as info,kuben_platform=debug is still accepted, but only its global level counts |
KUBEN_TELEMETRY__OTLP_ENDPOINT |
unset | accepted, not used: no traces are exported yet (the support bundle leaves it out) |
runtime
Section titled “runtime”The 1.x server ran on Tokio, and these keys sized its thread pools. Since 2.0 the server is written in Go: the keys are still accepted, so an existing configuration keeps loading, but they have no effect.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_RUNTIME__WORKER_THREADS |
unset | no effect since 2.0 |
KUBEN_RUNTIME__MAX_BLOCKING_THREADS |
16 |
no effect since 2.0 |
KUBEN_RUNTIME__BULKHEAD |
false |
no effect since 2.0 (ADR-013: there is no second runtime) |
The Go runtime’s own variables take their place. GOMAXPROCS (the threads running Go code at once) defaults to the CPUs available, and follows the container’s CPU limit. GOMEMLIMIT (a soft memory limit, e.g. GOMEMLIMIT=400MiB) is unset by default; set it a little below the container’s memory limit so the garbage collector works harder before the kernel kills the process.
bootstrap
Section titled “bootstrap”Used on the first start with an empty database. Outside a cluster, when no ADMIN_PASSWORD is set, nothing is seeded: the console shows /setup, where the first admin account is created (see kuben setup-token).
| Variable | Default | Purpose |
|---|---|---|
KUBEN_BOOTSTRAP__ORG_SLUG |
default |
|
KUBEN_BOOTSTRAP__ORG_NAME |
Default |
the name the /setup page asks for wins over this |
KUBEN_BOOTSTRAP__ADMIN_EMAIL |
admin@kuben.local |
for a configured or generated password; /setup asks for the email |
KUBEN_BOOTSTRAP__ADMIN_PASSWORD |
unset | set: the admin is created with it. Unset in Kubernetes: a generated password goes into the Secret kuben-initial-admin. Unset outside: the /setup page. Never logged |
These limits apply to every organization of the installation. Only the operator sets them, and unset means unlimited. A deployment that would exceed a limit is refused when Kuben accepts it.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_QUOTA__ORG_CPU |
unset | CPU requests of all apps at their peak, e.g. 32 |
KUBEN_QUOTA__ORG_MEMORY |
unset | memory requests of all apps at their peak, e.g. 64Gi |
KUBEN_QUOTA__ORG_PODS |
unset | pods of all apps at their peak |
KUBEN_QUOTA__ORG_APPS |
unset | live apps |
KUBEN_QUOTA__ORG_ENVIRONMENTS |
unset | live environments |
backup
Section titled “backup”| Variable | Default | Purpose |
|---|---|---|
KUBEN_BACKUP__DIR |
backups in the state directory |
where kuben backup writes |
KUBEN_BACKUP__KEEP |
7 |
how many backups are kept |
KUBEN_BACKUP__MAX_AGE_HOURS |
26 |
the server reports itself degraded and opens an incident when the newest good backup is older than this; 0 turns the check off |
KUBEN_BACKUP__BEFORE_UPGRADE |
true |
take a backup before a new version migrates the database |
notify
Section titled “notify”| Variable | Default | Purpose |
|---|---|---|
KUBEN_NOTIFY__ALLOW_PRIVATE_TARGETS |
false |
allow webhooks to private, loopback and link-local addresses |
KUBEN_NOTIFY__ALLOW_HTTP |
false |
allow http:// webhook endpoints |
retention
Section titled “retention”How long rows that only describe the past are kept. See Budgets.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_RETENTION__OUTBOX_DAYS |
7 |
delivered outbox messages |
KUBEN_RETENTION__WEBHOOK_DELIVERY_DAYS |
30 |
finished webhook deliveries |
KUBEN_RETENTION__RESOLVED_INCIDENT_DAYS |
180 |
resolved incidents |
KUBEN_RETENTION__USAGE_DAYS |
7 |
hourly usage of apps |
domains
Section titled “domains”| Variable | Default | Purpose |
|---|---|---|
KUBEN_DOMAINS__REQUIRE_CLAIM |
false |
apps may only use custom domains their organization verified |
KUBEN_DOMAINS__DOH_URL |
https://cloudflare-dns.com/dns-query |
the DNS-over-HTTPS resolver claims are checked with (JSON API) |
KUBEN_DOMAINS__CLOUDFLARE_API_URL |
https://api.cloudflare.com/client/v4 |
the Cloudflare API |
KUBEN_DOMAINS__CNAME_TARGET |
unset | app DNS records become a CNAME to this name instead of A/AAAA records of the Gateway |
AgentLink, where cluster agents connect to Kuben (ADR-027).
| Variable | Default | Purpose |
|---|---|---|
KUBEN_AGENT__BIND |
unset (set in the chart) | where Kuben listens for agents (host:port); unset, it does not |
KUBEN_AGENT__HUB_NAME |
hub.kuben.internal |
the name Kuben’s certificate carries; agents check it |
KUBEN_AGENT__CERTIFICATE_HOURS |
24 |
lifetime of the client certificates Kuben issues to agents |
KUBEN_AGENT__HEARTBEAT_SECS |
10 |
how often agents send a heartbeat |
KUBEN_AGENT__LOCAL |
false (true in the chart) |
enroll an agent in Kuben’s own cluster |
KUBEN_AGENT__ADVERTISE |
unset | the address agents in the cluster dial (host:port) |
KUBEN_AGENT__NAMESPACE |
Kuben’s own, else kuben-system |
namespace of the local agent and its Secrets |
The GitHub App Kuben acts as. The App is on only when the App id, its key file and the webhook secret are all set. Token connections to GitHub, GitLab and Gitea/Forgejo need no configuration: organization admins add them at runtime, and their tokens are sealed with the keyring (see Git providers).
| Variable | Default | Purpose |
|---|---|---|
KUBEN_GIT__GITHUB_APP_ID |
unset | the App’s numeric id |
KUBEN_GIT__GITHUB_PRIVATE_KEY_FILE |
unset | PEM file of the App’s private key (PKCS#1 or PKCS#8 RSA) |
KUBEN_GIT__GITHUB_WEBHOOK_SECRET |
unset | the secret GitHub signs webhook deliveries with |
KUBEN_GIT__GITHUB_API_URL |
https://api.github.com |
the REST API root, for GitHub Enterprise Server |
KUBEN_GIT__GITHUB_CLONE_URL |
https://github.com |
where build pods clone from |
Builds run as one rootless BuildKit Job per attempt (ADR-028). They read sources through the GitHub App, through Git connections, or both; see Builds.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_BUILD__ENABLED |
false |
run the build worker |
KUBEN_BUILD__NAMESPACE |
Kuben’s own, else kuben-builds |
namespace of build Jobs |
KUBEN_BUILD__BUILDKIT_IMAGE |
moby/buildkit:v0.33.0-rootless |
pin it by digest in production; the worker warns about images that are not |
KUBEN_BUILD__FETCH_IMAGE |
alpine/git:2.49.1 |
the image with git that fetches the source |
KUBEN_BUILD__RAILPACK_FRONTEND |
unset | Railpack’s BuildKit frontend image; with railpack_image, enables Railpack builds |
KUBEN_BUILD__RAILPACK_IMAGE |
unset | the image with sh and the railpack CLI; unset, only Dockerfile builds run |
KUBEN_BUILD__CPU_REQUEST |
500m |
|
KUBEN_BUILD__CPU_LIMIT |
2 |
|
KUBEN_BUILD__MEMORY |
2Gi |
memory request and limit (equal) |
KUBEN_BUILD__EPHEMERAL_STORAGE |
10Gi |
|
KUBEN_BUILD__DEADLINE_SECS |
1800 |
hard deadline of one attempt |
KUBEN_BUILD__MAX_CONCURRENT |
2 |
builds at once, over all organizations; 0 disables building |
KUBEN_BUILD__MAX_CONCURRENT_PER_ORG |
1 |
|
KUBEN_BUILD__PUSH_SECRET |
unset | a kubernetes.io/dockerconfigjson Secret in the build namespace with push access |
KUBEN_BUILD__INSECURE_REGISTRY |
false |
push over plain HTTP (an in-cluster registry without TLS) |
KUBEN_BUILD__REGISTRY_AUTH_FILE |
unset | user:password the verifier uses |
KUBEN_BUILD__NODE_POOL |
unset | node selector key=value of the build pool |
KUBEN_BUILD__SCANNER_IMAGE |
aquasec/trivy:0.74.0 |
writes the SBOM of every built image and scans it; empty disables scanning |
KUBEN_BUILD__SCANNER_MEMORY |
1Gi |
memory request and limit of the scan container |
KUBEN_BUILD__RESCAN_HOURS |
24 |
rescan the images apps run when their newest scan is older than this |
KUBEN_BUILD__LOG_TAIL_KIB |
256 |
how much of the end of a build’s log is kept when the build ends, so the log outlives the build pod; 1 to 1024, a value outside is the nearest bound |
integrations
Section titled “integrations”Git connections and organization registries (2.1). Organization admins choose the address of a self-hosted GitLab or Gitea and the server of a registry, and Kuben calls them. By default it refuses any that resolves to a private, loopback, link-local or otherwise non-public address, checked on every connection after the name is resolved. A proxy from HTTPS_PROXY/HTTP_PROXY is used as it is; the target behind it is checked first.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_INTEGRATIONS__ALLOW_PRIVATE_HOSTS |
false |
let Git connections and registry checks reach private addresses too, for a Git server or registry inside your network. Every organization admin can then reach those addresses through Kuben |
External CI trust: a GitHub Actions workflow exchanges its OIDC token for a short-lived Kuben token under an organization’s trust policy.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_CI__GITHUB_ACTIONS |
false |
accept GitHub Actions OIDC tokens |
KUBEN_CI__GITHUB_OIDC_ISSUER |
https://token.actions.githubusercontent.com |
the only issuer trusted; its keys are read from <issuer>/.well-known/jwks |
KUBEN_CI__GITHUB_OIDC_AUDIENCE |
server.public_url |
the audience workflows request |
Single sign-on with an OpenID Connect provider.
| Variable | Default | Purpose |
|---|---|---|
KUBEN_SSO__ENABLED |
false |
|
KUBEN_SSO__ISSUER |
unset | the provider’s issuer URL; its discovery document names the rest |
KUBEN_SSO__CLIENT_ID |
unset | |
KUBEN_SSO__CLIENT_SECRET_FILE |
unset | a file holding the client secret (preferred over client_secret) |
KUBEN_SSO__CLIENT_SECRET |
unset | the client secret itself |
KUBEN_SSO__DISPLAY_NAME |
Single sign-on |
shown on the sign-in button |
KUBEN_SSO__SCOPES |
["openid", "email", "profile"] |
|
KUBEN_SSO__GROUP_CLAIM |
groups |
the ID token claim that lists the person’s groups |
KUBEN_SSO__GROUPS |
empty | provider group → organization role (viewer … owner): {admins = "admin"}, or one variable per group, KUBEN_SSO__GROUPS__DEVS=developer |
KUBEN_SSO__DEFAULT_ROLE |
unset | role of people in no mapped group; unset refuses them |
KUBEN_SSO__ALLOWED_DOMAINS |
empty (any) | email domains allowed |
KUBEN_SSO__REQUIRE_VERIFIED_EMAIL |
true |
|
KUBEN_SSO__ORG |
bootstrap.org_slug |
the organization people join |
KUBEN_SSO__DISABLE_PASSWORD_FOR_LINKED |
false |
accounts linked to the provider cannot sign in with a password |
secrets
Section titled “secrets”| Variable | Default | Purpose |
|---|---|---|
KUBEN_SECRETS__KEYRING_FILE |
secrets.keyring in the state directory, created with one key when missing |
the keyring of key-encryption keys; every replica must read the same file, and it must be backed up apart from the database. See Backup and restore |