Skip to content

Configuration

Everything is configured with environment variables that start with CCUI_. There is no config file. The server reads them once at start, validates all of them, and exits with status 1 and a message that names each wrong variable. A restart is needed to change anything.

Rules that apply to every variable

Rule Detail
Unknown variables fail Any CCUI_* variable the server does not read is an error (CCUI_FOO: unknown variable). A typo in a security setting cannot silently turn it off.
Empty equals unset CCUI_LISTEN= behaves as if the variable were absent.
Booleans Only the exact text true turns CCUI_COOKIE_INSECURE and CCUI_TRUST_XFF on. 1, TRUE and yes leave them off.
Durations Go syntax with a unit and greater than zero: 30s, 15m, 12h, 1h30m. 60 is rejected.
Integers Positive whole numbers.
Lists Comma separated, spaces around items are ignored: ccui-admins, platform.
Indexed groups CCUI_CLUSTERS__<n>__*, CCUI_OIDC__<n>__* and CCUI_LOCAL_USERS__<n>__* are numbered from 0 with no gaps. Each group counts on its own.
Variables of other modes CCUI_OIDC__* set while CCUI_AUTH_MODE=local is read and checked for syntax but not used.

Secrets and the _FILE form

Seven variables hold secrets. Each accepts the value directly (NAME) or the path of a file that holds it (NAME_FILE). Set one of them, not both: with the file set, the direct variable is reported as unknown variable. A trailing newline is removed. The file is read at start, so a rotated secret needs a restart.

Secret _FILE form
CCUI_SESSION_SECRET CCUI_SESSION_SECRET_FILE
CCUI_LOCAL_ADMIN_PASSWORD_HASH CCUI_LOCAL_ADMIN_PASSWORD_HASH_FILE
CCUI_LOCAL_USERS__<n>__PASSWORD_HASH CCUI_LOCAL_USERS__<n>__PASSWORD_HASH_FILE
CCUI_CLUSTERS__<n>__AUTH_PASSWORD CCUI_CLUSTERS__<n>__AUTH_PASSWORD_FILE
CCUI_CLUSTERS__<n>__AUTH_TOKEN CCUI_CLUSTERS__<n>__AUTH_TOKEN_FILE
CCUI_CLUSTERS__<n>__AUTH_OIDC_CLIENT_SECRET CCUI_CLUSTERS__<n>__AUTH_OIDC_CLIENT_SECRET_FILE
CCUI_OIDC__<n>__CLIENT_SECRET CCUI_OIDC__<n>__CLIENT_SECRET_FILE

This is how Kubernetes and Docker secrets are mounted. Secret values are never written to the log. An unreadable file fails with CCUI_SESSION_SECRET_FILE: cannot read file.

CCUI_AUTH_MODE=local
CCUI_SESSION_SECRET_FILE=/run/secrets/ccui_session_secret
CCUI_LOCAL_ADMIN_PASSWORD_HASH_FILE=/run/secrets/ccui_admin_hash
CCUI_CLUSTERS__0__NAME=prod
CCUI_CLUSTERS__0__URL=http://cc.kafka.svc:9090/kafkacruisecontrol
CCUI_CLUSTERS__0__AUTH_TYPE=basic
CCUI_CLUSTERS__0__AUTH_USER=ccui
CCUI_CLUSTERS__0__AUTH_PASSWORD_FILE=/run/secrets/cc_prod_password

Server

Variable Default Meaning
CCUI_AUTH_MODE local none, local or oidc. See Authentication.
CCUI_LISTEN :8080 Address the server binds to, for example 127.0.0.1:8080. The container healthcheck probes the port of this value.
CCUI_TLS_CERT none Path of a PEM certificate. Must be set together with CCUI_TLS_KEY. Turns on HTTPS (TLS 1.2 or newer) and the Strict-Transport-Security header.
CCUI_TLS_KEY none Path of the PEM private key.
CCUI_PUBLIC_URL none The origin users type in the browser, for example https://cc.example.com, without a path. A trailing / is removed. Required with oidc. Also fixes the origin the CSRF check accepts.
CCUI_MAX_BODY_BYTES 1048576 Largest request body the proxy accepts for Cruise Control. Larger bodies get 413. The SPA sends query parameters only, so the default is plenty.

Sessions and login protection

Variable Default Meaning
CCUI_SESSION_SECRET none At least 32 characters. Required with local and oidc. Signs the CSRF tokens. Use the same value on every replica.
CCUI_SESSION_IDLE_TIMEOUT 30m A session with no request for this long ends.
CCUI_SESSION_MAX_AGE 12h A session ends this long after login, however active. It is also the cookie's Max-Age.
CCUI_COOKIE_INSECURE false true drops the Secure flag from the session cookie. Only for plain-HTTP development.
CCUI_LOGIN_MAX_ATTEMPTS 5 Failed local logins allowed per client address and per user name inside the window.
CCUI_LOGIN_WINDOW 15m The window, counted from the first failure. After CCUI_LOGIN_MAX_ATTEMPTS failures the login answers 429 until it ends.
CCUI_TRUST_XFF false true takes the client address for the rate limit and the log from the last entry of X-Forwarded-For. Set it only behind a proxy that appends the address.

Generate a secret:

openssl rand -base64 48

Local users

Used with CCUI_AUTH_MODE=local. At least one user is required, either with the shortcut or with numbered users, or both. Names must be unique across both forms. An OIDC user whose name equals a local user is refused at sign-in. The hash must be bcrypt or argon2id, never plain text. How to make one is in Authentication.

Variable Default Meaning
CCUI_LOCAL_ADMIN_PASSWORD_HASH none Shortcut for one admin user.
CCUI_LOCAL_ADMIN_USER admin Name of the shortcut user. In local mode setting only this variable, without the hash, is an error.
CCUI_LOCAL_USERS__<n>__NAME none User name. Required for each numbered user.
CCUI_LOCAL_USERS__<n>__PASSWORD_HASH none Required for each numbered user.
CCUI_LOCAL_USERS__<n>__ROLE none viewer or admin. Required for each numbered user.

Clusters

At least one cluster is required in every mode. <n> starts at 0.

Variable Default Meaning
CCUI_CLUSTERS__<n>__NAME none Unique. Letters, digits, ., _ and -. Appears in the cluster selector and in the API path.
CCUI_CLUSTERS__<n>__URL none http or https URL of Cruise Control including its API prefix, for example http://cc.kafka:9090/kafkacruisecontrol. A trailing / is removed.
CCUI_CLUSTERS__<n>__GROUP none Groups clusters in the selector. Clusters without one appear under "Ungrouped".
CCUI_CLUSTERS__<n>__TIMEOUT 60s Time limit for each request to this cluster. A slower answer becomes 504.
CCUI_CLUSTERS__<n>__AUTH_TYPE none How the server authenticates to Cruise Control: none, basic, bearer, sigv4 or oidc.
CCUI_CLUSTERS__<n>__AUTH_USER none Required with basic.
CCUI_CLUSTERS__<n>__AUTH_PASSWORD none Required with basic.
CCUI_CLUSTERS__<n>__AUTH_TOKEN none Required with bearer.
CCUI_CLUSTERS__<n>__AUTH_SIGV4_REGION none Required with sigv4, for example eu-west-1.
CCUI_CLUSTERS__<n>__AUTH_SIGV4_SERVICE execute-api Service name used when signing.
CCUI_CLUSTERS__<n>__AUTH_OIDC_TOKEN_URL none Required with oidc. Token endpoint of the OAuth2 client-credentials grant.
CCUI_CLUSTERS__<n>__AUTH_OIDC_CLIENT_ID none Required with oidc.
CCUI_CLUSTERS__<n>__AUTH_OIDC_CLIENT_SECRET none Required with oidc.
CCUI_CLUSTERS__<n>__AUTH_OIDC_SCOPE none Scopes to request, separated by spaces or commas.
CCUI_CLUSTERS__<n>__AUTH_FORWARD_IDENTITY off jwt or doas. See Clusters.

Examples for each AUTH_TYPE are in Clusters.

OIDC providers

Used with CCUI_AUTH_MODE=oidc, which also needs CCUI_PUBLIC_URL and CCUI_SESSION_SECRET. At least one provider is required.

Variable Default Meaning
CCUI_OIDC__<n>__NAME none Unique. Lowercase letters, digits and -. Shown on the sign-in button and used in the callback path.
CCUI_OIDC__<n>__ISSUER none Issuer URL. The server fetches <issuer>/.well-known/openid-configuration.
CCUI_OIDC__<n>__CLIENT_ID none
CCUI_OIDC__<n>__CLIENT_SECRET none
CCUI_OIDC__<n>__REDIRECT_URL <CCUI_PUBLIC_URL>/api/auth/<name>/callback Override only if the provider needs a different URL than the default.
CCUI_OIDC__<n>__SCOPES openid profile email Separated by spaces or commas.
CCUI_OIDC__<n>__GROUPS_CLAIM groups Name of the claim in the ID token that holds the groups.
CCUI_OIDC__<n>__ADMIN_GROUPS none Groups that get the admin role.
CCUI_OIDC__<n>__VIEWER_GROUPS none Groups that get the viewer role.
CCUI_OIDC__<n>__DEFAULT_ROLE none viewer or admin for users who match no group. Without it they are refused.

Complete examples

One cluster, one admin:

CCUI_AUTH_MODE=local
CCUI_SESSION_SECRET=replace-with-at-least-32-random-characters
CCUI_LOCAL_ADMIN_PASSWORD_HASH=$2a$10$jDy/91QpAa9CaSz6CQ1YX.uzTI0uNvUjh0c6H2P.2w8Zqa1433ism
CCUI_CLUSTERS__0__NAME=prod
CCUI_CLUSTERS__0__URL=http://cc.kafka.svc:9090/kafkacruisecontrol

Two clusters in two groups, behind an ingress with TLS terminated there:

CCUI_AUTH_MODE=local
CCUI_SESSION_SECRET=replace-with-at-least-32-random-characters
CCUI_PUBLIC_URL=https://cc.example.com
CCUI_TRUST_XFF=true
CCUI_SESSION_IDLE_TIMEOUT=15m
CCUI_SESSION_MAX_AGE=8h
CCUI_LOCAL_ADMIN_USER=ops
CCUI_LOCAL_ADMIN_PASSWORD_HASH=$2a$10$jDy/91QpAa9CaSz6CQ1YX.uzTI0uNvUjh0c6H2P.2w8Zqa1433ism
CCUI_LOCAL_USERS__0__NAME=ana
CCUI_LOCAL_USERS__0__PASSWORD_HASH=$2a$10$jDy/91QpAa9CaSz6CQ1YX.uzTI0uNvUjh0c6H2P.2w8Zqa1433ism
CCUI_LOCAL_USERS__0__ROLE=viewer
CCUI_CLUSTERS__0__NAME=prod-eu
CCUI_CLUSTERS__0__GROUP=prod
CCUI_CLUSTERS__0__URL=http://cc-eu.kafka.svc:9090/kafkacruisecontrol
CCUI_CLUSTERS__0__AUTH_TYPE=basic
CCUI_CLUSTERS__0__AUTH_USER=ccui
CCUI_CLUSTERS__0__AUTH_PASSWORD=replace-me
CCUI_CLUSTERS__1__NAME=staging
CCUI_CLUSTERS__1__GROUP=dev
CCUI_CLUSTERS__1__URL=http://cc-staging.kafka.svc:9090/kafkacruisecontrol
CCUI_CLUSTERS__1__TIMEOUT=120s

Validation and error messages

The server reports every problem it finds in one run, on standard error, before anything listens. These are the messages for the mistakes people make most.

Mistake Message
Misspelled or removed variable CCUI_FOO: unknown variable
Mode other than the three CCUI_AUTH_MODE: must be none, local or oidc, got "ldap"
local or oidc without a long secret CCUI_SESSION_SECRET: required with CCUI_AUTH_MODE=local, at least 32 characters (or CCUI_SESSION_SECRET_FILE)
local without any user CCUI_LOCAL_USERS__0__NAME: CCUI_AUTH_MODE=local needs at least one local user (CCUI_LOCAL_USERS__<n>__* or CCUI_LOCAL_ADMIN_PASSWORD_HASH)
Missing hash CCUI_LOCAL_ADMIN_PASSWORD_HASH: required with CCUI_AUTH_MODE=local
Plain-text password in a hash variable CCUI_LOCAL_ADMIN_PASSWORD_HASH: must be a bcrypt or argon2id hash, not plain text
Bad role CCUI_LOCAL_USERS__0__ROLE: must be viewer or admin, got "operator"
Same user name twice CCUI_LOCAL_USERS__1__NAME: duplicate local user name "ana"
No cluster at all at least one cluster is required: set CCUI_CLUSTERS__0__NAME and CCUI_CLUSTERS__0__URL
Cluster numbering skips 0 or leaves a gap CCUI_CLUSTERS__0__: index 0 is missing, indexes must be consecutive from 0, followed by unknown variable for the variables of the cluster that was skipped
URL without a scheme CCUI_CLUSTERS__0__URL: must be an http(s) URL, got "cc:9090/x"
Duration without a unit CCUI_CLUSTERS__0__TIMEOUT: must be a positive duration such as 30s, got "60"
Count that is not a positive integer CCUI_LOGIN_MAX_ATTEMPTS: must be a positive integer, got "0"
Cluster name with a space or a slash CCUI_CLUSTERS__0__NAME: required, only letters, digits, '.', '_' and '-' are allowed
Same cluster name twice CCUI_CLUSTERS__1__NAME: duplicate cluster name "prod"
Credentials missing for the auth type CCUI_CLUSTERS__0__AUTH_USER: required with AUTH_TYPE=basic
Unknown auth type CCUI_CLUSTERS__0__AUTH_TYPE: must be none, basic, bearer, sigv4 or oidc, got "oauth"
Unknown identity forwarding CCUI_CLUSTERS__0__AUTH_FORWARD_IDENTITY: must be jwt or doas, got "x"
Only one of certificate and key CCUI_TLS_KEY: CCUI_TLS_CERT and CCUI_TLS_KEY must be set together
oidc without providers CCUI_OIDC__0__: CCUI_AUTH_MODE=oidc needs at least one provider (CCUI_OIDC__0__NAME, ISSUER, CLIENT_ID, CLIENT_SECRET)
oidc without a public URL CCUI_PUBLIC_URL: required with CCUI_AUTH_MODE=oidc
Public URL without a scheme CCUI_PUBLIC_URL: must be an http(s) URL
Provider name in capitals CCUI_OIDC__0__NAME: required, lowercase letters, digits and '-' only
Provider default role wrong CCUI_OIDC__0__DEFAULT_ROLE: must be viewer or admin

Two problems show up only after the server is running, not at start. The sigv4 credentials are resolved on the first request, so a missing AWS role shows up as a 502 on use. OIDC discovery runs on the first sign-in, so an unreachable identity provider does not stop the UI from starting. It answers 502 identity provider unavailable instead.

What the server logs

Logs are JSON lines on standard output. At start there is one config line (mode, listen address, whether TLS is on, number of clusters and providers), one cluster line per cluster (name, group, auth type, identity forwarding) and a warning when the mode is none. No secret and no URL is logged. Failed local logins log the client address but not the user name. Every POST to Cruise Control adds an audit line.