Skip to main content
Version: 0.8

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 shapebaseUrl / APP_URLapiUrl / API_URLCookies
Same host (typical ingress)https://tokentimer.example.comsame value, or omit apiUrl in Helm so API_URL copies baseUrlSESSION_COOKIE_DOMAIN unset
Split host (HTTPS)https://app.example.comhttps://api.example.comSESSION_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

VariableDescriptionDefault valueScope
TT_MODEMust be enterprise to activate the enterprise variant. oss falls back to the Core feature set.ossApp mode
TT_LICENSE_KEYRaw JWT license string in the environment. Checked before the DB row and before the file path below.unsetLicense
TT_LICENSE_PATHAbsolute path inside the container to a PEM or JWT license file (used when env and DB have no JWT)./etc/tokentimer/license.keyLicense
TT_LICENSE_FILEAlias for TT_LICENSE_PATH (same semantics).unsetLicense
TT_LICENSE_GRACE_DAYSDays an expired license keeps serving traffic (with X-License-Warning header) before SSO and the API hard-fail.30License

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 /login with error=license_oidc_required or error=license_saml_required (or error=license_check_failed when verification throws) and emit a LICENSE_DENIED audit 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" or code: "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

MechanismDescription
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.jsonSame JSON shape; bind-mount via docker-compose.sso.yml
SSO_PROVIDERS_BOOTSTRAP_FILEPath 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).

PurposeURL
OIDC sign-in startGET /auth/oidc/<slug>
OIDC callback (register in IdP)GET /auth/oidc/<slug>/callback
SAML sign-in startGET /auth/saml/<slug>
SAML ACS (register in IdP)POST /auth/saml/<slug>/callback
SAML SP metadataGET /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.

warning

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.

VariableDescriptionDefault valueScope
SSO_SECRETS_KEYRINGInline JSON keyring (base64 keys). Mutually exclusive with _FILE.unsetSSO
SSO_SECRETS_KEYRING_FILEPath to keyring JSON inside the container.unsetSSO
SSO_PROVIDERS_BOOTSTRAP_FILEPath to bootstrap manifest JSON.unsetSSO

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 keyEffect
sso.secretsKeyringFull keyring JSON string; chart uses it instead of auto-generation
sso.existingSecretName of a Secret you created yourself; chart does not render <release>-enterprise-secrets (point tokentimer.api.envFrom at that name)
sso.autoGenerateKeyring: falseDisable 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=="
}
}
FieldMeaning
primaryKey id (kid) used for new encryption. Must match one entry in keys.
keysMap 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.

PatternHolds
SSO_<SLUG>_OIDC_CLIENT_SECRETOIDC client secret
SSO_<SLUG>_SAML_CERTIdP signing certificate (PEM)
SSO_<SLUG>_SAML_DECRYPTION_KEYSP 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 varLocks
SSO_<SLUG>_OIDC_ISSUERconfig.issuer
SSO_<SLUG>_OIDC_CLIENT_IDconfig.client_id
SSO_<SLUG>_OIDC_CLIENT_SECRETsecrets.client_secret
SSO_<SLUG>_SAML_ENTRY_POINTconfig.entry_point
SSO_<SLUG>_SAML_ISSUERconfig.issuer (SP entity ID)
SSO_<SLUG>_SAML_CERTconfig.cert
SSO_<SLUG>_SAML_DECRYPTION_KEYsecrets.decryption_key

Provider config fields (bootstrap JSON / API)

OIDC (required: issuer, client_id; secret: client_secret):

FieldDescription
issuerOIDC issuer URL; must match token iss
client_idOAuth client ID
scopesSpace-separated scopes (default openid profile email)
callback_urlOverride computed callback (rare)
authorization_url, token_url, userinfo_url, jwks_uri, end_session_urlEndpoint overrides when discovery is insufficient

SAML (required: entry_point, issuer, cert; optional secret: decryption_key):

FieldDescription
entry_pointIdP SSO URL (AuthnRequest target)
issuerSP entity ID
certIdP signing certificate (PEM)
callback_urlACS override (rare)
audience, secondary_cert, name_id_formatSAML 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)

MechanismPurpose
system_settings.sso_config_versionIncremented 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_MSPoll 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_ACCOUNTS must be true (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=true in the id_token / userinfo. If the IdP does not assert verification the login is rejected with email_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.

VariableDescriptionDefault valueScope
SSO_ALLOW_AUTO_LINK_LOCAL_ACCOUNTSGates 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.falseSSO
SSO_STRICT_MEMBERSHIP_SYNCWhen 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.falseSSO
SSO_CONFIG_SYNC_INTERVAL_MSPoll interval for sso_config_version across API pods (default 45000). System Settings → Single Sign-On → Advanced settings; requires API pod restart to apply.45000SSO
SSO_IDP_HTTP_TIMEOUT_MSOutbound HTTP timeout when the API calls IdP metadata, token, or userinfo endpoints (default 20000, max 120000). System Settings → Single Sign-On → Advanced settings.20000SSO
SSO_ALLOW_PRIVATE_IDP_URLSWhen true, outbound IdP fetches may target RFC1918 / localhost hosts (lab only). System Settings → Single Sign-On → Advanced settings.falseSSO
SSO_ALLOWED_IDP_CIDRSComma-separated IPv4 CIDR allowlist for private IdP hosts when SSO_ALLOW_PRIVATE_IDP_URLS is false. System Settings → Single Sign-On → Advanced settings.emptySSO

API behaviour

VariableDescriptionDefault valueScope
TRUST_PROXY_HOPSNumber 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.2API

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):

ActionEmitted when
LICENSE_IMPORTEDAn admin uploads or pastes a new license JWT under System Settings → License.
LICENSE_REMOVEDAn admin clears the DB-stored license JWT, falling back to TT_LICENSE_KEY / TT_LICENSE_PATH if set.
LICENSE_DENIEDA request was refused because the effective license lacks the required entitlement (SSO login, auto-sync run, or another gated route). See License.
SSO_PROVIDER_CREATEDAn admin adds a new SSO provider row.
SSO_PROVIDER_DELETEDAn admin deletes a provider row.
SSO_PROVIDER_SECRET_SETAn admin sets or rotates a provider secret (client secret, signing cert, etc.) from the UI/API.
SSO_PROVIDER_SECRET_CLEAREDAn admin clears a stored provider secret without setting a new one.
SSO_PROVIDER_SECRET_REENCRYPTEDThe rotate-sso-secrets maintenance script re-encrypts a provider secret under a new keyring key.
SSO_PROVIDER_TESTEDAn admin runs Test on a provider (POST /api/admin/sso/providers/<slug>/test).
SSO_IDP_METADATA_FETCHEDAn admin fetches the IdP's SAML/OIDC metadata from the provider wizard.
SSO_DIAGNOSTIC_BUNDLE_EXPORTEDAn 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.

VariableDescriptionDefault value
TT_ENTERPRISE_REGISTRYHarbor image registry hostname.harbor.tokentimer.ch
TT_ENTERPRISE_OWNERRegistry project / namespace.tokentimer-enterprise
TT_ENTERPRISE_TAGImage 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.