Configuration Reference
How configuration is resolved
Environment variables are the primary configuration surface for self-hosted TokenTimer. Defaults come from code fallbacks, then from Compose or Helm example values where applicable.
This page documents environment variables. On Helm, most are set through chart values rather than directly: the chart has first-class keys for the common ones and api.env / worker.env passthroughs for the rest. See deploy/helm/README.md for the value reference and deploy/helm/examples/ for ready-made values files.
Some settings can also be stored in the database through the System Settings admin UI. When both exist, the precedence differs per setting:
| Setting | Precedence (highest wins) |
|---|---|
| SMTP config | System Settings DB > env vars > code defaults |
| Twilio credentials and template SIDs | System Settings DB > env vars > code defaults |
| Alert thresholds, delivery window | Workspace preferences DB > env vars > code defaults |
CERTOPS_ENABLED | Env var > System Settings DB > code default (false); note both Compose and Helm supply the env var as true, so CertOps is enabled out of the box |
| Admin bootstrap | Env vars only |
In the tables below, unset means optional and disabled unless you set it. required means you must provide a value for production.
Core runtime and bootstrap
| Variable | Description | Default value | Scope |
|---|---|---|---|
TT_MODE | Core variant mode | oss | App mode |
NODE_ENV | Runtime environment | development | API, worker, dashboard |
SESSION_SECRET | Session signing secret | required | API, worker |
ADMIN_EMAIL | First admin bootstrap email | admin@your-company.com | API bootstrap |
ADMIN_PASSWORD | First admin bootstrap password | ChangeThisSecurePassword123! | API bootstrap |
ADMIN_NAME | First admin bootstrap display name | Administrator | API bootstrap |
DISABLE_ADMIN_BOOTSTRAP | Skip first admin auto-create when true | false | API bootstrap |
PORT | API listen port | 4000 | API |
HOST | API bind host | 0.0.0.0 | API |
API_PORT | Compose API port mapping | 4000 | Local compose |
DASHBOARD_PORT | Compose dashboard port mapping. Also read by pnpm dev for its port-availability preflight, though the Vite dev server itself is pinned to 5173 (strictPort), so remapping this makes that preflight check the wrong port. | 5173 | Local compose |
See Authentication for the full admin bootstrap flow.
Canonical URLs and API docs
| Variable | Description | Default value | Scope |
|---|---|---|---|
APP_URL | Frontend public URL (canonical) | http://localhost:5173 | API, worker, dashboard |
API_URL | Backend public URL (canonical) | http://localhost:4000 | API, worker, dashboard |
ENABLE_API_DOCS | Enable Swagger UI (/api-docs) in production | false | API docs serving |
VITE_API_URL | Frontend build-time API override | http://localhost:4000 | Dashboard build |
OPENAPI_SPEC_PATH | Optional absolute path to OpenAPI YAML | unset (auto-discovery) | API docs serving |
PUBLIC_BASE_URL | Public URL used for webhook signature validation | unset (falls back to API_URL) | API webhook auth |
Helm vs Compose vs runtime
| Layer | APP_URL | API_URL when unset in your config |
|---|---|---|
Helm (config.baseUrl / apiUrl) | baseUrl | apiUrl if set, else baseUrl (same host) |
Helm stock values.yaml | http://localhost:8080 | http://localhost:4000 (both set explicitly) |
Docker Compose (.env) | ${APP_URL:-http://localhost:5173} | ${API_URL:-http://localhost:4000} (no cross-fallback in compose files) |
| API process (env missing) | http://localhost:5173 | API_URL, else APP_URL, else http://localhost:4000 |
In local http://localhost with NODE_ENV=production, the browser does not persist or send the secure session cookie for the auth flow. Use HTTPS in front of the API and dashboard (reverse proxy with TLS), otherwise secure cookies are expected to fail in local HTTP.
Split-host deployments (dashboard and API on different origins)
When APP_URL and API_URL differ on HTTPS (for example dashboard at https://app.example.com and API at https://api.example.com), the API:
- Allows both origins in CORS (configured
APP_URLandAPI_URLonly in production; localhost dev origins are omitted unlessALLOW_LOCAL_DEV_CORS=true). - Sets session and CSRF cookies to
SameSite=None; Secureon the API host. Host-only cookies (noSESSION_COOKIE_DOMAIN) are enough for credentialedwithCredentialscalls from the dashboard toapi.example.com. SetSESSION_COOKIE_DOMAINonly when you intentionally need a parent-domain cookie shared across multiple subdomains (broader scope; weaker isolation).
HTTP split-host (including LAN IPs or internal DNS without TLS) does not use SameSite=None; cookies stay Lax because None requires Secure and is often blocked. Use HTTPS for split-host production, or put UI and API behind one origin (ingress).
Docker Compose (http://localhost:5173 + http://localhost:4000) and Helm port-forward (http://localhost:8080 + http://localhost:4000) keep SameSite=Lax (same-site across ports on local HTTP). For plain HTTP + NODE_ENV=production, set SESSION_COOKIE_SECURE_LOCALHOST_OVERRIDE=true only when both APP_URL and API_URL are local HTTP (localhost / 127.0.0.1); the flag is ignored on internet-facing HTTPS URLs.
Same-host installs (single ingress hostname for UI and API) can leave SESSION_COOKIE_DOMAIN unset. Helm defaults often use baseUrl: http://localhost:8080 and apiUrl: http://localhost:4000; Compose maps the dashboard to host port 5173 by default (APP_URL=http://localhost:5173). Always set APP_URL and API_URL to the origins users and integrations actually use in the browser.
Database and pooling
| Variable | Description | Default value | Scope |
|---|---|---|---|
DB_HOST | PostgreSQL host | localhost (code), postgres (compose) | API, worker |
DB_PORT | PostgreSQL port | 5432 | API, worker |
DB_NAME | PostgreSQL database name | tokentimer | API, worker |
DB_USER | PostgreSQL user | tokentimer | API, worker |
DB_PASSWORD | PostgreSQL password | password (dev fallback), required (prod) | API, worker |
DATABASE_URL | Full PostgreSQL connection URL override | unset | API, worker |
DB_SSL | DB SSL mode (require or verify) | unset (disabled) | API, worker |
PGSSLROOTCERT | Path to CA cert for DB SSL verify mode | unset | API, worker |
DB_POOL_MAX | Max DB pool connections | 10 | API, worker |
DB_POOL_MIN | Min DB pool connections | 2 | API, worker |
DB_POOL_IDLE_TIMEOUT | Idle connection timeout (ms) | 30000 | API, worker |
DB_CONNECTION_TIMEOUT | DB connection timeout (ms) | 5000 | API, worker |
PostgreSQL is the only stateful component. See Backup and restore for backup guidance.
Authentication and security
LOCAL_AUTH_ENABLED, REQUIRE_EMAIL_VERIFICATION, TWO_FACTOR_ENABLED,
SESSION_MAX_AGE, CSRF_ENABLED, MIN_PASSWORD_LENGTH, REQUIRE_UPPERCASE
and REQUIRE_NUMBERS were previously listed here as supported variables.
They are parsed but never read, so setting them has no effect. The real
behavior is fixed in code: a 2-hour rolling session cookie, CSRF always on,
local auth and TOTP always available, and a fixed 12-character 5-class
password policy. See Authentication.
The cookie, CORS, and proxy variables below are live.
| Variable | Description | Default value | Scope |
|---|---|---|---|
SESSION_COOKIE_SECURE_LOCALHOST_OVERRIDE | Allow insecure session cookies in production only when APP_URL and API_URL are both local HTTP (localhost / 127.0.0.1); ignored otherwise | false | API auth |
SESSION_COOKIE_DOMAIN | Optional parent domain for session/CSRF cookies (e.g. .example.com) when you need cookies shared across subdomains; not required for typical split-host API calls | unset | API auth |
ALLOW_LOCAL_DEV_CORS | In production, also allow http://localhost:* and http://127.0.0.1:* in CORS (local troubleshooting only) | false | API security |
PHONE_HASH_SALT | Optional salt for phone hashing | unset | API privacy |
TRUST_PROXY_HOPS | Number of trusted reverse-proxy hops in front of the API (affects req.ip and req.protocol resolution). 0 = no proxy, 1 = single ingress/reverse proxy, 2 = LB -> ingress. | 2 | API security |
WORKER_API_KEY | Worker-to-API auth key | unset (falls back to SESSION_SECRET) | Worker, API |
Email and delivery (SMTP)
| Variable | Description | Default value | Scope |
|---|---|---|---|
SMTP_HOST | SMTP host(s) (comma-separated) | localhost in config helper, unset in compose examples | API, worker email |
SMTP_PORT | SMTP port(s) (comma-separated) | 587 in config helper | API, worker email |
SMTP_USER | SMTP username(s) (comma-separated) | unset | API, worker email |
SMTP_PASS | SMTP password(s) (comma-separated) | unset | API, worker email |
FROM_EMAIL | Sender email override | unset | API, worker email |
FROM_EMAIL_NAME | Sender display name | TokenTimer | API, worker email |
SMTP_SECURE | Force SMTPS/SSL | false | API, worker email |
SMTP_REQUIRE_TLS | Require STARTTLS upgrade | true | API, worker email |
SMTP_REJECT_UNAUTHORIZED | Reject invalid TLS certs | true | API, worker email |
SMTP can be configured via env vars or from the System Settings admin UI. Env vars serve as bootstrap values; once an admin configures SMTP in the UI, the database values take precedence.
Alerts, limits, and webhooks
| Variable | Description | Default value | Scope |
|---|---|---|---|
ALERT_THRESHOLDS | Default days-before-expiry thresholds (overridden by workspace preferences) | 30,14,7,1,0 | Alerts |
ALERT_MAX_ATTEMPTS | Max delivery retries per alert | 20 | Alerts |
ALERT_RETRY_DELAY_MS | Retry delay in ms | 300000 | Alerts |
ALERT_TEST_UTC_DAY | Test-only scheduler day override | unset | Alerts testing |
GLOBAL_RATE_LIMIT_WINDOW_MS | Global limiter window in ms | 60000 | API rate limiting |
GLOBAL_RATE_LIMIT_MAX | Global limiter max requests per window | 300 (prod) / 1000 (dev,test) | API rate limiting |
GLOBAL_SLOWDOWN_WINDOW_MS | Global slowdown window in ms | 900000 | API rate limiting |
GLOBAL_SLOWDOWN_DELAY_AFTER | Requests before delay is applied | 50 | API rate limiting |
GLOBAL_SLOWDOWN_DELAY_MS | Added delay per request after threshold | 500 | API rate limiting |
LOGIN_RATE_LIMIT_WINDOW_MS | Login limiter window in ms | 900000 | Auth rate limiting |
LOGIN_RATE_LIMIT_MAX | Login attempts per window | 10 (prod) / 500 (dev,test) | Auth rate limiting |
LOGIN_EMAIL_RATE_LIMIT_MAX | Login attempts per email per window | 5 (prod) / 500 (dev,test) | Auth rate limiting |
PASSWORD_RESET_RATE_LIMIT_WINDOW_MS | Password reset limiter window in ms | 900000 | Auth rate limiting |
PASSWORD_RESET_RATE_LIMIT_MAX | Password reset attempts per window | 5 (prod) / 100 (dev,test) | Auth rate limiting |
PASSWORD_RESET_EMAIL_RATE_LIMIT_MAX | Password reset attempts per email per window | 3 (prod) / 200 (dev,test) | Auth rate limiting |
EMAIL_VERIFICATION_RATE_LIMIT_WINDOW_MS | Email verification limiter window in ms | 3600000 | Auth rate limiting |
EMAIL_VERIFICATION_RATE_LIMIT_MAX | Email verification attempts per window | 20 (prod) / 500 (dev,test) | Auth rate limiting |
AUTH_SLOWDOWN_WINDOW_MS | Auth slowdown window in ms | 900000 | Auth rate limiting |
AUTH_SLOWDOWN_DELAY_AFTER | Auth requests before slowdown | 5 (prod) / 1000 (dev,test) | Auth rate limiting |
AUTH_SLOWDOWN_DELAY_MS | Auth slowdown delay per request in ms | 500 | Auth rate limiting |
AUTH_SLOWDOWN_MAX_DELAY_MS | Auth slowdown max delay in ms | 10000 | Auth rate limiting |
API_RATE_LIMIT_WINDOW_MS | Plan-aware API limiter window in ms | 900000 | API rate limiting |
TEST_API_RATE_LIMIT_WINDOW_MS | Test/dev API limiter window in ms | 900000 | API rate limiting |
TEST_API_RATE_LIMIT_MAX | Test/dev API limiter max requests | 1000 (prod) / 10000 (dev,test) | API rate limiting |
DELIVERY_WINDOW_DEFAULT_START | Default delivery window start (UTC, overridden by workspace preferences) | 00:00 | Alerts |
DELIVERY_WINDOW_DEFAULT_END | Default delivery window end (UTC, overridden by workspace preferences) | 23:59 | Alerts |
DELIVERY_WINDOW_DEFAULT_TZ | Default delivery timezone (overridden by workspace preferences) | UTC | Alerts |
DELIVERY_WINDOW_DEFERRAL_MS | Defer delay when outside window | unset | Alerts |
MAX_WEBHOOKS | Max webhooks per workspace | 5 | Alerts/webhooks |
TEST_WEBHOOK_RATE_LIMIT_1M_WINDOW_MS | Test webhook short limiter window in ms | 60000 | Alerts/webhooks |
TEST_WEBHOOK_RATE_LIMIT_1M_MAX | Test webhook max requests in short window | 5 | Alerts/webhooks |
TEST_WEBHOOK_RATE_LIMIT_5M_WINDOW_MS | Test webhook long limiter window in ms | 300000 | Alerts/webhooks |
TEST_WEBHOOK_RATE_LIMIT_5M_MAX | Test webhook max requests in long window | 10 | Alerts/webhooks |
TEST_WEBHOOK_COOLDOWN_MS | Per-user test webhook cooldown after each attempt | 5000 | Alerts/webhooks |
WEBHOOK_ALLOW_ALL_HOSTS | Allow all webhook destinations when true | false | Webhook security |
WEBHOOK_PROVIDER_HOSTS | Extra allowed provider hosts | empty (built-in list still allowed) | Webhook security |
WEBHOOK_EXTRA_PROVIDER_HOSTS | Additional allowed hosts | empty | Webhook security |
WEBHOOK_ALLOW_PRIVATE_IPS | Allow webhook delivery to private/reserved (e.g. RFC1918) IPs when true. Self-hosted escape hatch for on-network alert targets such as an internal Rocket.Chat or Mattermost; it disables the SSRF guard entirely, so scope it to trusted networks. | false | Webhook security |
VAULT_ADDRESS_ALLOWLIST | Vault integration host allowlist | empty (no host restriction) | Integration security |
AZURE_VAULT_ADDRESS_ALLOWLIST | Azure Key Vault host allowlist | empty (no host restriction) | Integration security |
GITHUB_ADDRESS_ALLOWLIST | GitHub host allowlist | empty (no host restriction) | Integration security |
GITLAB_ADDRESS_ALLOWLIST | GitLab host allowlist | empty (no host restriction) | Integration security |
INTEGRATION_SCAN_LIMITS | JSON plan-to-limit map (core defaults unlimited) | {"oss":Infinity} | Integration quotas |
DOMAIN_CHECKER_DISCOVERY_LIMITS | Domain checker discovery result cap map (plan:value) | oss:10000000 | Domain checker |
DOMAIN_CHECKER_IMPORT_LIMITS | Domain checker import request cap map (plan:value) | oss:50000 | Domain checker |
DOMAIN_CHECKER_MAX_RESULTS | Direct override for discovery results, capped internally at 25,000,000 | unset | Domain checker |
DOMAIN_CHECKER_IMPORT_MAX_CERTIFICATES | Direct override for import certificates per request, capped at 200,000 | unset | Domain checker |
CONTACT_GROUP_LIMITS | JSON plan-to-limit map (core defaults unlimited) | {"oss":Infinity} | Contact groups |
CONTACT_GROUP_MEMBER_LIMITS | JSON plan-to-limit map (core defaults unlimited) | {"oss":Infinity} | Contact groups |
WORKSPACE_PLAN_LIMITS | JSON plan-to-limit map (core defaults unlimited) | {"oss":Infinity} | Workspaces |
MEMBER_PLAN_LIMITS | JSON plan-to-limit map (core defaults unlimited) | {"oss":Infinity} | Workspace members |
CertOps (certificate operations)
Enabling and required keys
| Variable | Description | Default value | Scope |
|---|---|---|---|
CERTOPS_ENABLED | Enable the certificate operations layer. When enabled, TokenTimer maintains a managed-certificate inventory linked to cert-category tokens, accepts public certificate (PEM) import, and bridges observations from HTTPS endpoint/domain monitors into the inventory (when the monitor has a linked token). When disabled, CertOps API endpoints return 404. | false in code, but true as deployed: Compose interpolates ${CERTOPS_ENABLED:-true} and Helm ships config.certopsEnabled: true | CertOps |
CERTOPS_SIGNING_ENCRYPTION_KEY | 64 hex characters (32 bytes). Wraps the control-plane Ed25519 job-signing private key at rest. Required before any agent can be dispatched signed work: operations that need the key fail closed with CERTOPS_SIGNING_ENCRYPTION_KEY_MISSING when it is unset or malformed. | none | Agents |
CERTOPS_REGISTRATION_ENCRYPTION_KEY | 64 hex characters (32 bytes). Wraps agent registration credentials at rest. Required for agent registration to succeed. | none | Agents |
SESSION_SECRETGenerate each with openssl rand -hex 32, set them before enabling agents, and back them up. Losing CERTOPS_SIGNING_ENCRYPTION_KEY makes the stored signing key unreadable, and you must rotate to a new signing key and have every agent re-pin it. Rotating either value without a controlled migration has the same effect. Both are read from the environment only; they are never stored in the database.
If you only use CertOps for inventory and monitoring (no agents), CERTOPS_ENABLED alone is enough. The two encryption keys are needed for the agent execution plane.
Only three CertOps settings have dedicated chart values: config.certopsEnabled, config.certopsSigningEncryptionKey and config.certopsRegistrationEncryptionKey. Everything in the sections below (renewal scheduling, agent lifecycle, sweep tuning) has no dedicated key, so pass it through instead: api.env for API-side variables, worker.env for the worker sweep variables.
api:
env:
CERTOPS_RENEWAL_THRESHOLD_DAYS: "45"
worker:
env:
CERTOPS_SWEEP_RENEWAL_SCHEDULER_TIMEOUT_MS: "60000"
Renewal scheduling
| Variable | Description | Default value | Scope |
|---|---|---|---|
CERTOPS_RENEWAL_THRESHOLD_DAYS | Schedule a renewal when a managed certificate expires within this many days. An individual certificate profile can override this with its own renew_before_days, which takes precedence and is editable from the Renewal automation page. A missing, non-numeric, or non-positive value falls back to 30 rather than disabling renewals. | 30 | Renewals |
CERTOPS_RENEWAL_PER_CA_CAP | Maximum in-flight renewals per CA endpoint per workspace, so one CA cannot be flooded. Enforced on every renew creation path (scheduler sweep, manual job, bulk renew): the sweep skips over-cap certificates and retries them next tick, while manual and bulk creation fail with 409 CERTOPS_RENEWAL_PER_CA_CAP_EXCEEDED. | 5 | Renewals |
CERTOPS_JOB_LEASE_SECONDS | How long an agent's claim on a job stays valid before it must be renewed. Raise this if legitimate renewals routinely take longer. | 900 | Jobs |
CERTOPS_LEASE_HARD_GRACE_MS | Extra time a still-heartbeating agent gets before its expired-lease job is judged. | 3600000 | Jobs |
Lease expiry never silently retries work that may already have changed a host. See Reconciling interrupted jobs.
Agent fleet and compatibility
| Variable | Description | Default value | Scope |
|---|---|---|---|
CERTOPS_AGENT_OFFLINE_AFTER_MS | Heartbeat silence after which an agent counts as offline. Drives both the persisted status (via the stale-agent sweep) and the real-time livenessState computed on every read. Observational: it never retires an agent, revokes its credential, or reassigns its jobs. | 600000 | Agents |
CERTOPS_AGENT_CLOCK_DRIFT_WARN_MS | Reported agent clock offset above which the fleet view warns. | 5000 | Agents |
CERTOPS_AGENT_CLOCK_DRIFT_ALERT_MS | Reported agent clock offset above which the fleet view alerts. Signed jobs enforce a validity window, so a badly skewed host cannot execute work. | 30000 | Agents |
CERTOPS_AGENT_MIN_PROTOCOL_VERSION | Oldest accepted agent protocol version. | 1.0.0 | Agents |
CERTOPS_AGENT_MAX_PROTOCOL_VERSION | Newest accepted agent protocol version. | 1.999.999 | Agents |
CERTOPS_AGENT_MIN_AGENT_VERSION | Oldest accepted agent build. Raise this to force a fleet upgrade. | 0.1.0 | Agents |
CERTOPS_AGENT_MAX_AGENT_VERSION | Newest accepted agent build. | 99.999.999 | Agents |
CERTOPS_REGISTRATION_REPLAY_TTL_MS | How long a registration attempt is remembered for replay rejection. | 900000 | Agents |
Narrowing the version windows blocks incompatible agents at registration and heartbeat with CERTOPS_AGENT_COMPATIBILITY_BLOCKED, which is the supported way to stop an old build from claiming work during a staged upgrade.
Maintenance sweeps
The CertOps maintenance worker runs six sweeps. Each has an enable flag (default true) and a timeout in milliseconds:
| Sweep | Enable variable | Timeout variable | What it does |
|---|---|---|---|
| Lease reaper | CERTOPS_SWEEP_LEASE_REAPER_ENABLED | CERTOPS_SWEEP_LEASE_REAPER_TIMEOUT_MS | Requeues or flags jobs whose lease expired |
| Stale agents | CERTOPS_SWEEP_STALE_AGENTS_ENABLED | CERTOPS_SWEEP_STALE_AGENTS_TIMEOUT_MS | Marks agents offline after heartbeat silence |
| Nonce sweep | CERTOPS_SWEEP_NONCE_ENABLED | CERTOPS_SWEEP_NONCE_TIMEOUT_MS | Deletes consumed/expired dispatch nonces past their grace window |
| Registration replay | CERTOPS_SWEEP_REGISTRATION_REPLAY_ENABLED | CERTOPS_SWEEP_REGISTRATION_REPLAY_TIMEOUT_MS | Expires registration replay records |
| Renewal scheduler | CERTOPS_SWEEP_RENEWAL_SCHEDULER_ENABLED | CERTOPS_SWEEP_RENEWAL_SCHEDULER_TIMEOUT_MS | Creates renewal jobs for certificates near expiry |
| Outbox drain | CERTOPS_SWEEP_OUTBOX_DRAIN_ENABLED | CERTOPS_SWEEP_OUTBOX_DRAIN_TIMEOUT_MS | Delivers the deferred side effects CertOps recorded transactionally, currently renewal-failure alerts |
Disabling the renewal-scheduler sweep stops new automatic renewal jobs from being created while leaving existing jobs, dispatch, and reporting untouched. Disabling the lease reaper leaves interrupted jobs in place indefinitely, so keep it on outside of debugging.
Disabling the outbox drain silences renewal-failure alerts. The intents are not lost: they accumulate as pending rows and are delivered once the sweep runs again. But while it is off, renewals can fail without anyone being told. If renewals are visibly failing on the dashboard and no alert arrives, check this sweep before you start debugging contact groups. See Certificate renewal failures.
Each sweep runs isolated from the others: a sweep that throws or exceeds its timeout is recorded and does not prevent the remaining sweeps in the same tick.
CertOps worker metrics
The maintenance worker exports these alongside the rest of the worker series:
| Metric | Type | Labels | Notes |
|---|---|---|---|
certops_maintenance_sweeps_total | counter | sweep, status | Sweep executions, including timeouts and failures |
certops_renewal_scheduler_certificates | gauge | outcome | Certificates handled by the last renewal-scheduler run. See Seeing what the scheduler decided for the outcome values and what to do about each |
certops_renewal_jobs_created | gauge | none | Renew jobs created by the last renewal-scheduler run |
certops_lease_reaper_jobs | gauge | outcome | Jobs the last lease-reaper run requeued or failed |
certops_stale_agents | gauge | none | Active agents demoted to offline in the last sweep |
certops_nonces_swept | gauge | none | Expired dispatch nonces deleted in the last sweep |
certops_registration_replays_swept | gauge | none | Expired registration replay rows deleted in the last sweep |
The one worth alerting on is certops_renewal_scheduler_certificates{outcome="skipped_incomplete_profile"}. A non-zero value there with a zero created series means certificates are approaching expiry and automation is refusing to renew them, which otherwise looks identical to an idle fleet.
cert-manager controller
These apply to the Kubernetes controller, not the API or worker:
| Variable | Description | Default value | Scope |
|---|---|---|---|
CERTOPS_CONTROLLER_MODE | observe (report only) or provision (create authorized Certificate resources). | observe | Controller |
CERTOPS_WATCH_NAMESPACES | Comma-separated namespaces to watch. | the Helm release namespace (certops.controller.watchNamespaces is [], which resolves to the release namespace; cluster-wide requires certops.controller.clusterWide: true) | Controller |
CERTOPS_CLUSTER_WIDE | Watch cluster-wide instead of a namespace list. | false | Controller |
CERTOPS_RECONCILE_INTERVAL | Reconcile loop interval. | 30s | Controller |
CERTOPS_HEALTH_PORT | Port for the controller's health endpoint. | 8080 | Controller |
CERTOPS_SHUTDOWN_TIMEOUT | Graceful shutdown budget. | 10s | Controller |
CERTOPS_SECRET_FALLBACK_ENABLED | When a ready Certificate carries no usable public fingerprint, read the public tls.crt from its Secret to complete the observation. Grants the controller narrow Secret read access; private key material is rejected outright. | false | Controller |
The controller is disabled by default in the Helm chart and defaults to observe mode. For the Helm values that enable it (required api.* fields, token scopes per mode, namespace scoping, and the NetworkPolicy CIDR requirement) see Path C - CertOps Kubernetes controller.
Operator commands
Some CertOps operations are deployment-level rather than per-workspace, and are exposed as commands rather than API routes:
# Inspect / rotate the control-plane job-signing key
pnpm certops:rotate-signing-key status
pnpm certops:rotate-signing-key begin
pnpm certops:rotate-signing-key complete
See Rotating the job-signing key.
TokenTimer stores only public certificate material (fingerprints, serials, issuers, subjects, SANs, validity, chains) and external key references. Requests containing private key material are rejected with HTTP 422.
For the flag resolution order and rollout guidance, see Enable CertOps.
Worker scheduling
TokenTimer runs six background workers:
- Alert discovery scans tokens and queues expiry alerts.
- Alert delivery processes the alert queue and sends notifications (email, webhooks, WhatsApp).
- Auto-sync runs scheduled integration scans (for example Vault or GitHub imports).
- Endpoint check monitors HTTPS endpoints and their SSL certificates.
- Weekly digest sends the weekly token-expiry summary.
- CertOps maintenance runs the six CertOps sweeps described in Maintenance sweeps. Only relevant when CertOps is enabled, but skipping it when CertOps is enabled is a silent failure mode, so treat it as required (see the warning below).
How they are scheduled depends on the deployment method:
-
Docker Compose: each worker runs as its own long-running container (
worker-discovery,worker-delivery,worker-auto-sync,worker-endpoint-check,worker-weekly-digest,worker-certops) using the built-in runner. The runner schedules jobs in-process from a cron expression; the containerrestart: unless-stoppedpolicy is crash recovery only, not the scheduler. -
Helm (Kubernetes): each worker is a Kubernetes CronJob that starts a one-shot process per scheduled run. Schedules come from
worker.cronjobs.<name>.scheduleinvalues.yaml, not from theWORKER_*_CRONvariables below. The chart defaults match the runner defaults.Each entry under
worker.cronjobs.<name>also acceptsenabled,concurrencyPolicy,startingDeadlineSeconds,backoffLimit,activeDeadlineSecondsand per-jobresources. The CronJob names areautoSync,discovery,delivery,endpointCheck,weeklyDigestandcertops(camelCase, unlike the Compose service names).Because Kubernetes starts a fresh one-shot process per run, the
WORKER_*_RUN_ON_STARTandWORKER_*_INTERVAL_MSvariables below have no meaningful effect there; they exist for the long-running Compose runner.
worker-certops if CertOps is enabledNothing errors when this worker is missing. The API keeps accepting agent registrations, heartbeats, claims, and results, so the deployment looks healthy while six sweeps quietly never run. The symptoms show up later and look like unrelated bugs:
- Agents that crashed, were shut down, or lost the network stay
activein the Agent fleet panel indefinitely, because nothing demotes them tooffline. - No certificate ever renews automatically, because the renewal scheduler sweep is what creates renewal jobs.
- No renewal-failure alert is ever delivered, because the outbox drain sweep is what resolves contacts and queues them. Renewals fail visibly on the dashboard and your alert channel stays silent.
- Jobs whose agent died sit in
claimedorrunningforever instead of being requeued or flagged for reconciliation. - Consumed dispatch nonces and registration replay records accumulate without bound.
It also needs CERTOPS_ENABLED set to the same value as the API. If the API sees CertOps enabled but this worker does not, the renewal scheduler counts every certificate as "skipped, workspace paused" and logs nothing at error level.
A cron expression is a five-field schedule string (minute hour day-of-month month day-of-week), for example */5 * * * * for every 5 minutes.
| Variable | Description | Default value | Scope |
|---|---|---|---|
WORKER_DISCOVERY_CRON | Alert discovery schedule (scan tokens, queue expiry alerts) | */5 * * * * | Worker (Compose) |
WORKER_DELIVERY_CRON | Alert delivery schedule (send queued notifications), offset 1 min after discovery | 1/5 * * * * | Worker (Compose) |
WORKER_AUTO_SYNC_CRON | Auto-sync schedule (scheduled integration scans) | */1 * * * * | Worker (Compose) |
WORKER_ENDPOINT_CHECK_CRON | Endpoint/SSL check schedule | */1 * * * * | Worker (Compose) |
WORKER_WEEKLY_DIGEST_CRON | Weekly digest schedule (Monday 09:00) | 0 9 * * 1 | Worker (Compose) |
WORKER_CERTOPS_CRON | CertOps maintenance sweep schedule | */1 * * * * | Worker (Compose) |
Cron expressions are evaluated in the container timezone. Compose worker services set TZ: ${TZ:-UTC}, so set TZ in deploy/compose/.env (for example TZ=Europe/Zurich) to shift schedules to local time.
Run on start
In cron mode, workers wait for their next scheduled run and do not run immediately at process start. Set a run-on-start variable to true to also run the job once at startup.
| Variable | Description | Default value | Scope |
|---|---|---|---|
WORKER_RUN_ON_START | Global run-on-start for discovery, delivery, auto-sync, and endpoint check. Not inherited by weekly digest. | false | Worker |
WORKER_DISCOVERY_RUN_ON_START | Per-worker override for alert discovery | false | Worker |
WORKER_DELIVERY_RUN_ON_START | Per-worker override for alert delivery | false | Worker |
WORKER_AUTO_SYNC_RUN_ON_START | Per-worker override for auto-sync | false (code), true (compose) | Worker |
WORKER_ENDPOINT_CHECK_RUN_ON_START | Per-worker override for endpoint check | false | Worker |
WORKER_WEEKLY_DIGEST_RUN_ON_START | Run the weekly digest immediately at startup. Required for weekly digest; it ignores the global variable. | false | Worker |
WORKER_CERTOPS_RUN_ON_START | Per-worker override for CertOps maintenance. Does not inherit the global variable. | false | Worker |
Each per-worker variable overrides the global one for that worker only. Leaving a per-worker variable unset (or empty) falls back to WORKER_RUN_ON_START, so you can set the global once and override just the workers you care about.
The Compose worker-auto-sync service sets WORKER_AUTO_SYNC_RUN_ON_START=true by default, so an integration scan runs when that container starts.
In interval mode, discovery, delivery, auto-sync, and endpoint check run once at startup by default (the weekly digest still does not).
Supported cron syntax
The runner accepts standard five-field cron expressions with:
*for any value- step values:
*/nandn/m(start atn, repeat everym) - exact integers, ranges (
n-m), and comma-separated lists - day-of-week
0-7, where both0and7mean Sunday
Not supported: named months or weekdays (JAN, MON), and the L, W, and # modifiers. Invalid expressions fail at worker startup with an error naming the variable.
Interval mode
Cron mode is the default. To use fixed millisecond intervals instead, set the worker's cron variable to interval (or 0, false, off) and set the matching interval variable:
WORKER_DISCOVERY_CRON=interval
WORKER_DISCOVERY_INTERVAL_MS=60000
WORKER_*_INTERVAL_MS variables are ignored while the worker is in cron mode.
| Variable | Description | Default value | Scope |
|---|---|---|---|
WORKER_DISCOVERY_INTERVAL_MS | Discovery interval (ms) | 60000 | Worker |
WORKER_DELIVERY_INTERVAL_MS | Delivery interval (ms) | 30000 | Worker |
WORKER_AUTO_SYNC_INTERVAL_MS | Auto-sync interval (ms) | 300000 | Worker |
WORKER_ENDPOINT_CHECK_INTERVAL_MS | Endpoint check interval (ms) | 60000 | Worker |
WORKER_WEEKLY_DIGEST_INTERVAL_MS | Weekly digest interval (ms) | 86400000 | Worker |
WORKER_CERTOPS_INTERVAL_MS | CertOps maintenance interval (ms) | 60000 | Worker |
Failure behavior
| Variable | Description | Default value | Scope |
|---|---|---|---|
WORKER_EXIT_ON_ERROR | When false, job failures are logged (worker-runner-job-failure) and the runner keeps running. When true, the process exits on the first unhandled job error and the container restart policy recovers it. | false | Worker |
For worker health metrics (runner_up, delivery and digest gauges), see Monitoring and metrics.
Metrics and observability
| Variable | Description | Default value | Scope |
|---|---|---|---|
ENABLE_METRICS | Enable /metrics and metric push attempts when true | false | API, worker |
PUSHGATEWAY_URL | Prometheus Pushgateway endpoint | unset | Worker metrics |
ENVIRONMENT_SUFFIX | Metrics environment label (if you have multiple instances) | unset | Worker metrics |
WhatsApp (Twilio)
| Variable | Description | Default value | Scope |
|---|---|---|---|
TWILIO_ACCOUNT_SID | Twilio account SID | unset | |
TWILIO_AUTH_TOKEN | Twilio auth token | unset | |
TWILIO_WHATSAPP_FROM | WhatsApp sender (E.164) | unset | |
TWILIO_WHATSAPP_ALERT_CONTENT_SID_EXPIRES | Content template SID for expiring alerts | unset | |
TWILIO_WHATSAPP_ALERT_CONTENT_SID_EXPIRED | Content template SID for expired alerts | unset | |
TWILIO_WHATSAPP_ALERT_CONTENT_SID_ENDPOINT_DOWN | Content template SID for endpoint down alerts | unset | |
TWILIO_WHATSAPP_ALERT_CONTENT_SID_ENDPOINT_RECOVERED | Content template SID for endpoint recovered alerts | unset | |
TWILIO_WHATSAPP_WEEKLY_DIGEST_CONTENT_SID | Content template SID for weekly digest | unset | |
TWILIO_WHATSAPP_TEST_CONTENT_SID | Content template SID for test messages | unset | |
WHATSAPP_RATE_PER_MIN | Outbound WhatsApp message rate cap | 60 | WhatsApp throttling |
WHATSAPP_DRY_RUN | Log messages without sending when true | false | WhatsApp testing |
Admin setup: Twilio WhatsApp
Use this flow when you want to enable WhatsApp notifications, expiry templates, weekly digest, and endpoint alert templates.
1. Configure Twilio credentials in System Settings. As an admin, open System Settings > WhatsApp (Twilio) and fill in the Account SID, Auth Token, and WhatsApp From Number. Optional template SIDs configurable in the UI: Alert (Expires), Alert (Expired), Test Message, and Weekly Digest.
2. Configure endpoint alert template SIDs at deployment level. Endpoint down and endpoint recovered WhatsApp alerts use dedicated deployment variables:
TWILIO_WHATSAPP_ALERT_CONTENT_SID_ENDPOINT_DOWNTWILIO_WHATSAPP_ALERT_CONTENT_SID_ENDPOINT_RECOVERED
Set them in your deployment method: deploy/compose/.env for Compose, deploy/helm/values.yaml for Helm, or your own secret/config injection. After changing these values, restart the delivery worker so it picks up the new template SIDs.
3. Create Twilio content templates. Create two approved Twilio WhatsApp templates for endpoint down and endpoint recovered. The exact template bodies, placeholder contracts, and copy-ready examples for all TokenTimer templates are documented in Contact groups & channels → WhatsApp templates setup.
Variables sent by TokenTimer for endpoint WhatsApp alerts: recipient_name, endpoint_name, endpoint_url, token_name, detected_at.
4. Verify.
- Use Send test WhatsApp in System Settings to verify base Twilio credentials and sender configuration.
- Trigger an endpoint outage and confirm the delivery log shows channel
whatsapp. - Confirm the delivery metadata records
template_kind = endpoint_downfor outage alerts andtemplate_kind = endpoint_recoveredfor recovery alerts.
Legacy and compatibility
| Variable | Description | Default value | Scope |
|---|---|---|---|
PLAN_API_LIMITS | API limiter plan map override (parsed as plan:value, fallback oss:6000) | unset | API middleware |
RATE_LIMIT_WINDOW | Config package generic rate-limit window (ms) | 60000 | Shared config consumers |
RATE_LIMIT_MAX | Config package generic rate-limit max requests | 100 | Shared config consumers |