Configuration reference
Environment variables specific to TokenTimer Enterprise. TokenTimer Core variables (database, SMTP, alerting, rate limiting, etc.) still apply unchanged; the matching Core reference is bundled with the delivery tarball.
Conventions used in the tables below:
unset: optional and disabled unless you set it.required: the feature does not start until you provide a value.
Precedence everywhere: environment variable > System Settings DB > default,
except for the enterprise license JWT, which resolves as:
TT_LICENSE_KEY (env) > system_settings.license_jwt (admin UI) > file
at TT_LICENSE_PATH (alias env name: TT_LICENSE_FILE, legacy: LICENSE_FILE).
Setting an environment variable locks the corresponding field in the System Settings UI with a "Managed by env" indicator (where applicable).
Canonical URLs (Core + Enterprise)
Core variables APP_URL and API_URL map from Helm tokentimer.config.baseUrl
and tokentimer.config.apiUrl (Compose: APP_URL, API_URL). The full Core
table (including SESSION_COOKIE_DOMAIN and SESSION_COOKIE_SECURE_LOCALHOST_OVERRIDE)
is in the Core configuration reference bundled with the delivery.
| Deployment shape | baseUrl / APP_URL | apiUrl / API_URL | Cookies |
|---|---|---|---|
| Same host (typical ingress) | https://tokentimer.example.com | same value, or omit apiUrl in Helm so API_URL copies baseUrl | SESSION_COOKIE_DOMAIN unset |
| Split host (HTTPS) | https://app.example.com | https://api.example.com | SESSION_COOKIE_DOMAIN optional (host-only API cookies are usually enough); set .example.com only if you need parent-domain sharing |
Split-host is required when the dashboard and API are on different browser
origins. The API enables cross-origin CORS for configured APP_URL / API_URL
(production omits localhost dev origins unless ALLOW_LOCAL_DEV_CORS=true) and uses
SameSite=None session cookies only when both URLs are HTTPS. Host-only cookies
on the API host are enough for credentialed dashboard calls to api.example.com;
SESSION_COOKIE_DOMAIN is optional and widens cookie scope across subdomains.
HTTP split-host (LAN, internal DNS) stays Lax; use TLS or a single ingress origin
in production. Compose / Helm port-forward on localhost HTTP also stays Lax.
SSO callbacks and IdP redirect URIs always use API_URL
(see SSO identity provider guides).
Helm stock values.yaml sets baseUrl: http://localhost:8080 and apiUrl: http://localhost:4000
(split ports for port-forward). Compose sets APP_URL / API_URL independently
(default 5173 / 4000; dashboard container listens on 8080 internally). Compose
does not copy APP_URL into API_URL; the API runtime falls back API_URL to APP_URL
only when the API_URL env var is absent.
Set SESSION_COOKIE_DOMAIN on API pods only when you need parent-domain cookies
(Core variable; optional for typical split-host; not in the enterprise ConfigMap by default):
tokentimer:
api:
env:
SESSION_COOKIE_DOMAIN: ".example.com"
Compose: add the same variable to the api service environment in .env or
docker-compose.yml.
License
| Variable | Description | Default value | Scope |
|---|---|---|---|
TT_MODE | Must be enterprise to activate the enterprise variant. oss falls back to the Core feature set. | oss | App mode |
TT_LICENSE_KEY | Raw JWT license string in the environment. Checked before the DB row and before the file path below. | unset | License |
TT_LICENSE_PATH | Absolute path inside the container to a PEM or JWT license file (used when env and DB have no JWT). | /etc/tokentimer/license.key | License |
TT_LICENSE_FILE | Alias for TT_LICENSE_PATH (same semantics). | unset | License |
TT_LICENSE_GRACE_DAYS | Days an expired license keeps serving traffic (with X-License-Warning header) before SSO and the API hard-fail. | 30 | License |
Docker Compose and the default Helm chart do not require a license file on disk: you can use TT_LICENSE_KEY and/or import the JWT from System Settings after bootstrap. For a file-based install, set TT_LICENSE_PATH and mount the file (Helm license Secret, or a Compose bind mount).
When a license is missing, invalid, or past its grace period:
- SSO endpoints (
/auth/oidc/<slug>,/auth/saml/<slug>) redirect to/loginwitherror=license_oidc_requiredorerror=license_saml_required(orerror=license_check_failedwhen verification throws) and emit aLICENSE_DENIEDaudit row. The login UI hides buttons for providers that are not entitled. - Admin API calls that require an enterprise entitlement return HTTP
403 with
code: "ENTITLEMENT_MISSING"orcode: "LICENSE_INVALID".
Per-IdP setup guides (redirect URIs, consoles, troubleshooting): SSO identity provider guides.
Single Sign-On (multi-provider)
Each IdP is a row in sso_providers with a unique slug and type
(oidc or saml). Secrets are stored encrypted in sso_provider_secrets
(AES-256-GCM; keyring required on every API pod).
Bootstrap and admin UI
| Mechanism | Description |
|---|---|
| SSO providers (dashboard: System Settings → Manage SSO providers) | Wizard, edit, test, enable, callback URLs per provider |
| Single Sign-On (System Settings) | IdP group to TokenTimer role bindings (reconciled on each SSO login) |
Helm ssoProviders[] | GitOps bootstrap; chart renders a ConfigMap (mount via extraVolumeMounts in helm examples) at /etc/tokentimer/sso-providers.json |
Compose sso-providers.json | Same JSON shape; bind-mount via docker-compose.sso.yml |
SSO_PROVIDERS_BOOTSTRAP_FILE | Path to the bootstrap manifest inside the container (default when using the SSO overlay) |
On startup, the bootstrap process upserts providers from the manifest and
reads secret material from SSO_<SLUG>_* env vars referenced in the manifest
(secrets.client_secret, secrets.cert_env, etc.; values are env var names). Slug and type are
immutable after create.
Per-provider routes
Replace <slug> with the provider slug (e.g. entra, keycloak, okta).
| Purpose | URL |
|---|---|
| OIDC sign-in start | GET /auth/oidc/<slug> |
| OIDC callback (register in IdP) | GET /auth/oidc/<slug>/callback |
| SAML sign-in start | GET /auth/saml/<slug> |
| SAML ACS (register in IdP) | POST /auth/saml/<slug>/callback |
| SAML SP metadata | GET /auth/saml/<slug>/metadata |
Default callbacks are ${API_URL}/auth/{oidc|saml}/<slug>/callback unless
config.callback_url is set on the provider row.
When a license is missing, invalid, or past its grace period, slugged SSO
endpoints redirect to /login with error=license_oidc_required or
error=license_saml_required.
Secrets keyring (required for UI-stored secrets)
The keyring holds AES-256-GCM master keys used to encrypt SSO provider secrets (OIDC client secrets, SAML private keys, and similar) before they are written to PostgreSQL (sso_provider_secrets). Every API pod must use the same keyring so encrypt and decrypt stay consistent.
The keyring material is only supplied as environment configuration (Helm: Kubernetes Secret; Compose: file mount). It is not stored in the database. Losing the keyring Secret or changing SSO_SECRETS_KEYRING without a controlled re-encryption of sso_provider_secrets makes existing ciphertext in Postgres unreadable. Back up <release>-enterprise-secrets (or your sso.existingSecret) with the same care as SESSION_SECRET.
| Variable | Description | Default value | Scope |
|---|---|---|---|
SSO_SECRETS_KEYRING | Inline JSON keyring (base64 keys). Mutually exclusive with _FILE. | unset | SSO |
SSO_SECRETS_KEYRING_FILE | Path to keyring JSON inside the container. | unset | SSO |
SSO_PROVIDERS_BOOTSTRAP_FILE | Path to bootstrap manifest JSON. | unset | SSO |
Helm and Kubernetes (default): with sso.autoGenerateKeyring: true (default in helm/values.yaml), the chart renders Secret <release>-enterprise-secrets containing SSO_SECRETS_KEYRING. The value is generated on first install and preserved on upgrade (same lookup pattern as core SESSION_SECRET). API pods already reference this Secret via tokentimer.api.envFrom in the default values.
Optional overrides:
| Values key | Effect |
|---|---|
sso.secretsKeyring | Full keyring JSON string; chart uses it instead of auto-generation |
sso.existingSecret | Name of a Secret you created yourself; chart does not render <release>-enterprise-secrets (point tokentimer.api.envFrom at that name) |
sso.autoGenerateKeyring: false | Disable chart rendering; you must mount SSO_SECRETS_KEYRING or SSO_SECRETS_KEYRING_FILE another way |
IdP client secrets from GitOps still use per-provider Kubernetes Secrets and *SecretRef in ssoProviders[] (see helm examples); those env vars are separate from the encryption keyring.
Keyring JSON format (manual setup)
When you supply the keyring yourself (sso.secretsKeyring, a Kubernetes Secret you create, Compose secrets/sso-keyring.json, or inline SSO_SECRETS_KEYRING), use this shape:
{
"primary": "k1",
"keys": {
"k1": "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789+/ab=="
}
}
| Field | Meaning |
|---|---|
primary | Key id (kid) used for new encryption. Must match one entry in keys. |
keys | Map of kid to key material. Each value is standard base64 of exactly 32 random bytes (AES-256). |
Generate one value (the command output is the string that goes in keys; do not base64-encode it again):
openssl rand -base64 32
primary can be any short id (k1, k2025, and so on). For rotation, add a second entry (for example k2), switch primary to k2, and re-encrypt existing sso_provider_secrets before removing the old kid.
Common mistake: double base64 encoding (for example piping openssl rand -base64 32 through base64 again, or Helm randBytes | b64enc). The API then reports Key "<kid>" must decode to exactly 32 bytes.
Create a Kubernetes Secret manually
Use this when sso.autoGenerateKeyring: false and/or sso.existingSecret points at a Secret you manage.
NAMESPACE=tokentimer # your namespace
SECRET_NAME=tokentimer-enterprise-secrets # must match sso.existingSecret or <release>-enterprise-secrets
KID=k1
KEY="$(openssl rand -base64 32)"
KEYRING=$(printf '{"primary":"%s","keys":{"%s":"%s"}}' "$KID" "$KID" "$KEY")
kubectl create secret generic "$SECRET_NAME" \
--namespace "$NAMESPACE" \
--from-literal=SSO_SECRETS_KEYRING="$KEYRING" \
--dry-run=client -o yaml | kubectl apply -f -
Wire the API to that Secret (default enterprise values.yaml already uses secretRef name {{ .Release.Name }}-enterprise-secrets). If you use a different name, set in values.yaml:
sso:
autoGenerateKeyring: false
existingSecret: my-sso-keyring-secret
and ensure tokentimer.api.envFrom / tokentimer.worker.envFrom reference the same Secret name.
Restart API pods after create or replace:
kubectl rollout restart deployment/<release>-tokentimer-api -n "$NAMESPACE"
To inspect (values only; do not commit output):
kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" -o jsonpath='{.data.SSO_SECRETS_KEYRING}' | base64 -d; echo
Helm: inline keyring in values
sso:
autoGenerateKeyring: false
secretsKeyring: '{"primary":"k1","keys":{"k1":"<paste-output-of-openssl-rand-base64-32>"}}'
The chart writes this string to SSO_SECRETS_KEYRING in <release>-enterprise-secrets (unless sso.existingSecret is set).
Docker Compose: the chart does not run on Compose. From compose/, generate secrets/sso-keyring.json with the same JSON shape as documented in the Compose SSO README shipped in the delivery (compose/sso-providers.README.md). The API loads that file via SSO_SECRETS_KEYRING_FILE.
Append SSO secret env keys to .env by merging the example file (only VAR= lines, no comments):
grep -v '^#' .env.sso-basic.example >> .env
For a custom sso-providers.json, copy each secrets.* value (env var name) into .env as NAME=, or use the optional jq flow in the Compose SSO README.
Per-provider secret env vars
Secret env var names follow SSO_<SLUG>_<PROTOCOL>_<FIELD>. Slug hyphens become
underscores (slug entra gives prefix SSO_ENTRA_, then OIDC_CLIENT_SECRET
gives SSO_ENTRA_OIDC_CLIENT_SECRET).
Slug tip: use a short IdP name (entra, keycloak, okta). Do not embed
-oidc or -saml in the slug; the provider type already sets the protocol, and
a slug like entra-oidc produces SSO_ENTRA_OIDC_OIDC_CLIENT_SECRET. When one
vendor runs both OIDC and SAML, pick two distinct slugs (e.g. keycloak and
keycloak2), not protocol words in the slug.
azure-ad vs entra: Token import auto-sync uses the integration id azure-ad
(license feature integration:azure). For SSO providers, prefer slug entra;
slug azure-ad remains valid if you already deployed it.
Dashboard navigation: open System Settings → Manage SSO providers (the UI may label this area SSO Providers). Group-to-role bindings are under System Settings → Single Sign-On, not inside the provider editor.
| Pattern | Holds |
|---|---|
SSO_<SLUG>_OIDC_CLIENT_SECRET | OIDC client secret |
SSO_<SLUG>_SAML_CERT | IdP signing certificate (PEM) |
SSO_<SLUG>_SAML_DECRYPTION_KEY | SP private key for encrypted assertions |
Reference these from the bootstrap manifest (secrets.client_secret,
secrets.cert_env, etc.). Example placeholder names for the bundled basic OIDC and SAML manifests live in compose/.env.sso-basic.example in the delivery.
GitOps field locks (optional)
When set, the matching provider field is managed by env and rejected on API/UI writes (shown as locked in SSO Providers):
| Env var | Locks |
|---|---|
SSO_<SLUG>_OIDC_ISSUER | config.issuer |
SSO_<SLUG>_OIDC_CLIENT_ID | config.client_id |
SSO_<SLUG>_OIDC_CLIENT_SECRET | secrets.client_secret |
SSO_<SLUG>_SAML_ENTRY_POINT | config.entry_point |
SSO_<SLUG>_SAML_ISSUER | config.issuer (SP entity ID) |
SSO_<SLUG>_SAML_CERT | config.cert |
SSO_<SLUG>_SAML_DECRYPTION_KEY | secrets.decryption_key |
Provider config fields (bootstrap JSON / API)
OIDC (required: issuer, client_id; secret: client_secret):
| Field | Description |
|---|---|
issuer | OIDC issuer URL; must match token iss |
client_id | OAuth client ID |
scopes | Space-separated scopes (default openid profile email) |
callback_url | Override computed callback (rare) |
authorization_url, token_url, userinfo_url, jwks_uri, end_session_url | Endpoint overrides when discovery is insufficient |
SAML (required: entry_point, issuer, cert; optional secret:
decryption_key):
| Field | Description |
|---|---|
entry_point | IdP SSO URL (AuthnRequest target) |
issuer | SP entity ID |
cert | IdP signing certificate (PEM) |
callback_url | ACS override (rare) |
audience, secondary_cert, name_id_format | SAML tuning |
Per-provider claim_mapping and security blocks mirror the advanced fields
in SSO Providers (PKCE, clock skew, claim paths, InResponseTo policy, etc.).
See Claim mapping.
SAML InResponseTo uses Postgres (saml_request_cache,
multi_provider_sso_v1 migration), not ingress stickiness.
HA / multi-replica (API)
| Mechanism | Purpose |
|---|---|
system_settings.sso_config_version | Incremented when a provider is created/updated/deleted via UI or API. Every API pod reloads strategies when the version changes (poll + before slugged SSO routes). |
Postgres session table (connect-pg-simple) | OIDC state, nonce, and PKCE code_verifier shared across pods. |
Postgres saml_request_cache (multi_provider_sso_v1) | Shared InResponseTo cache for SAML across pods; ingress stickiness not required. |
SSO_CONFIG_SYNC_INTERVAL_MS | Poll interval for sso_config_version (default 45000). Also reloads lazily on /auth/oidc/<slug> and /auth/saml/<slug>. Configurable in System Settings → Single Sign-On → Advanced settings; takes effect after API pod restart. |
Bootstrap-only changes (manifest or SSO_<SLUG>_* env without a UI write)
require a rolling restart of all API pods.
SSO security and identity model
Identity resolution is subject-first: every SSO login is bound to its
IdP-stable (provider, subject_id) pair. Email is only used as a
fallback when no subject row exists yet, and the email-fallback link is
gated by two checks:
SSO_ALLOW_AUTO_LINK_LOCAL_ACCOUNTSmust betrue(admin opt-in under System Settings → Single Sign-On → Installation-wide SSO policy, or per-provider override in Manage SSO providers → Advanced), and- the IdP must assert email ownership. SAML responses carry this
implicitly via the cert-signed assertion; OIDC requires
email_verified=truein the id_token / userinfo. If the IdP does not assert verification the login is rejected withemail_not_verified_for_link.
The fallback also refuses to silently rebind an existing SSO account to
a new subject under the same provider (sso_account_exists_subject_mismatch).
An admin must merge or relink such accounts explicitly.
| Variable | Description | Default value | Scope |
|---|---|---|---|
SSO_ALLOW_AUTO_LINK_LOCAL_ACCOUNTS | Gates the email-fallback link path for both local-password accounts and existing SSO accounts. When false (default), SSO login that would have to fall back to email matching is rejected. Enable only after you have audited the affected accounts. Even when enabled, OIDC also requires email_verified=true. Configurable in System Settings → Single Sign-On (global default) or per provider under Manage SSO providers → Advanced. | false | SSO |
SSO_STRICT_MEMBERSHIP_SYNC | When true, SSO-derived workspace memberships not present in the current group mapping resolution are revoked at login. Manual memberships (source='manual') and the system admin flag are never revoked. See Group mappings. Configurable globally in System Settings → Single Sign-On or per provider. | false | SSO |
SSO_CONFIG_SYNC_INTERVAL_MS | Poll interval for sso_config_version across API pods (default 45000). System Settings → Single Sign-On → Advanced settings; requires API pod restart to apply. | 45000 | SSO |
SSO_IDP_HTTP_TIMEOUT_MS | Outbound HTTP timeout when the API calls IdP metadata, token, or userinfo endpoints (default 20000, max 120000). System Settings → Single Sign-On → Advanced settings. | 20000 | SSO |
SSO_ALLOW_PRIVATE_IDP_URLS | When true, outbound IdP fetches may target RFC1918 / localhost hosts (lab only). System Settings → Single Sign-On → Advanced settings. | false | SSO |
SSO_ALLOWED_IDP_CIDRS | Comma-separated IPv4 CIDR allowlist for private IdP hosts when SSO_ALLOW_PRIVATE_IDP_URLS is false. System Settings → Single Sign-On → Advanced settings. | empty | SSO |
API behaviour
| Variable | Description | Default value | Scope |
|---|---|---|---|
TRUST_PROXY_HOPS | Number of trusted reverse-proxy hops in front of the API (affects req.ip, req.protocol, and SSO callback URL generation). 0 = no proxy, 1 = single ingress, 2 = LB then ingress. | 2 | API |
TRUST_PROXY_HOPS is a Core variable but matters specifically for SSO:
the SAML ACS URL and the OIDC redirect URI are derived from
req.protocol and req.host, which only honour X-Forwarded-*
headers when the proxy chain is trusted.
When config.callback_url is unset on a provider row, defaults are
${API_URL}/auth/oidc/<slug>/callback and ${API_URL}/auth/saml/<slug>/callback.
API_URL is the API host origin; the dashboard and server append /api for JSON routes
and /auth for SSO routes internally. SSO browser paths are always
/auth/{oidc|saml}/<slug>/...; /api/auth/* is only used for JSON such as
GET /api/auth/features.
Audit log actions
Beyond the login and group-mapping events covered in Group mappings and Authentication model, enterprise emits these actions for license and SSO provider management (System Settings → Manage SSO providers and System Settings → License):
| Action | Emitted when |
|---|---|
LICENSE_IMPORTED | An admin uploads or pastes a new license JWT under System Settings → License. |
LICENSE_REMOVED | An admin clears the DB-stored license JWT, falling back to TT_LICENSE_KEY / TT_LICENSE_PATH if set. |
LICENSE_DENIED | A request was refused because the effective license lacks the required entitlement (SSO login, auto-sync run, or another gated route). See License. |
SSO_PROVIDER_CREATED | An admin adds a new SSO provider row. |
SSO_PROVIDER_DELETED | An admin deletes a provider row. |
SSO_PROVIDER_SECRET_SET | An admin sets or rotates a provider secret (client secret, signing cert, etc.) from the UI/API. |
SSO_PROVIDER_SECRET_CLEARED | An admin clears a stored provider secret without setting a new one. |
SSO_PROVIDER_SECRET_REENCRYPTED | The rotate-sso-secrets maintenance script re-encrypts a provider secret under a new keyring key. |
SSO_PROVIDER_TESTED | An admin runs Test on a provider (POST /api/admin/sso/providers/<slug>/test). |
SSO_IDP_METADATA_FETCHED | An admin fetches the IdP's SAML/OIDC metadata from the provider wizard. |
SSO_DIAGNOSTIC_BUNDLE_EXPORTED | An admin exports the SSO diagnostic bundle for troubleshooting. |
Image registry
The enterprise images are published to Harbor. Override these only when staging from a private mirror.
| Variable | Description | Default value |
|---|---|---|
TT_ENTERPRISE_REGISTRY | Harbor image registry hostname. | harbor.tokentimer.ch |
TT_ENTERPRISE_OWNER | Registry project / namespace. | tokentimer-enterprise |
TT_ENTERPRISE_TAG | Image tag to deploy (pin to a release X.Y.Z). | matches the bundle version in VERSION |
Helm chart values
Helm exposes bootstrap providers under ssoProviders[] and global SSO toggles
under sso.*. The Core sub-chart still owns SMTP, alerting, and rate-limit
values. See helm/README.md and helm/examples/values-sso-basic-oidc.yaml (or
-basic-saml, -multi-provider) under helm/examples/ in the delivery.
# See helm/examples/values-sso-basic-oidc.yaml.
license:
create: true
ssoProviders:
- slug: entra
type: oidc
displayName: Microsoft Entra ID
enabled: true
oidc:
issuer: https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0
clientId: tokentimer
clientSecretRef:
name: entra-oidc
key: SSO_ENTRA_OIDC_CLIENT_SECRET
tokentimer:
api:
envFrom:
- configMapRef:
name: '{{ .Release.Name }}-enterprise-env'
- secretRef:
name: '{{ .Release.Name }}-enterprise-secrets'
optional: true
- secretRef:
name: entra-oidc
config:
apiUrl: https://api.tokentimer.example.com
trustProxyHops: 2
sso:
allowAutoLinkLocalAccounts: false
strictMembershipSync: false
The full Core variable reference (database, SMTP, alerts, rate limiting, audit log retention, OpenAPI surface) is bundled with the delivery tarball.