Production controller settings
For startup configuration and precedence, see the settings reference.
Required production controller environment
The production API is internal-only by default. Operators must provision an
internal Kubernetes ClusterIP Service and a default-deny ingress
NetworkPolicy that allows only explicitly approved namespace and Pod
selectors. Production Helm validates peer label maps. The cluster must enforce
NetworkPolicies. Do not expose the listener through a NodePort,
LoadBalancer, hostNetwork, or public endpoint.
The Helm charts allow DNS egress on UDP/TCP ports 53 and 5353 to their
configured dns.namespace and dns.podLabels peers. Port 5353 supports
OpenShift DNS backends after Service address translation. These grants cover
API, worker, initialization, collector, Slack proxy, Envoy, and observability
demo workloads.
The trusted-operator native admin pilot is the only documented public-ingress exception: the console host and Agent wildcard hosts route to OCC through the procedure in Deploy native admin UI access. Envoy and Agent gateway Services remain private, and OCC strips the shared OCE session cookie before forwarding to the native gateway.
| Variable | Required value or format | Behavior |
|---|---|---|
NODE_ENV |
Exactly production. |
Enables durable production controller composition. |
OCC_HOST |
One explicit Pod interface IP address. | Wildcard addresses and implicit hostnames are rejected. |
OCC_PORT |
Decimal integer from 1 through 65535. |
Selects the internal listener port exposed by the operator's Service. |
OCC_DATABASE_URL |
Explicit PostgreSQL application-role URL. | Must connect to the already migrated controller database. With TLS verification, an IP host must match an IP subjectAltName. |
OCC_CONFIG_PATH |
Absolute path to trusted Installation startup YAML. | Selects Configuration, IAM, Compute, and optional account Drivers. |
OCC_AUTH_SECRET |
Mounted high-entropy Better Auth secret with at least 32 characters. | Signs and verifies session material without logging it. |
OCC_AUTH_BASE_URL |
HTTPS DNS origin without path, query, fragment or user info; must share a non-public suffix with the Agent domain. | Defines the Better Auth base URL and cookie origin. |
OCC_GATEWAY_API_KEY_PATH |
Optional absolute path to the private gateway service-key file. | API only; validates at startup and reads each operation for rotation. Requires Compute endpoint resolution. |
OCC_PREPROVISIONED_NAMESPACE |
Optional JSON {"namespaceId","uid"} binding one operator-created namespace; set by Helm. |
API and worker. See preprovisioned namespace. |
OCC_CHANNEL_DIRECTORY_PROXY_URL |
Optional HTTP(S) proxy URL with one literal IPv4 address and explicit port, or the exact Helm-managed Service URL. | API only; routes Slack lookup and credential validation through an HTTP CONNECT tunnel. Invalid values fail startup. |
OCC_CHANNEL_DIRECTORY_MANAGED_PROXY_HOST |
Optional exact Helm-managed proxy Service host. | API only; the one DNS host the Slack directory Driver accepts in the proxy URL instead of an IPv4 address. |
OCC_API_HTTPS_EGRESS_OPEN |
Optional true or false, default false; set by Helm. |
API only; true (open sign-in egress) makes an unanswered Slack check a retryable 503. |
NODE_EXTRA_CA_CERTS |
Optional PEM bundle for private Gateway or IdP CAs. | Node reads it at process startup. Normal leaf renewal under that CA does not require a restart; root-bundle changes do. |
OCC_GATEWAY_CA_PATH |
Optional absolute path to the Gateway-only CA bundle; empty means no private Gateway CA. | API only; Harness node enrollment reads it instead of NODE_EXTRA_CA_CERTS. Helm sets it with OIDC CA values. |
For Helm, prefer slackProxy.enabled over an external
api.channelDirectoryProxyUrl; the
Slack Channel Driver
owns both options' API-only proxy egress and the proxy's slack.com:443
requirement. With neither, the chart renders no proxy egress rule, and Slack
lookup and credential validation require another approved network route.
OCC_AGENT_RUNTIME_LOGS_ENABLED (true or false, default true) switches the
Agent logs routes; false makes them answer
501. The chart sets it from agentRuntimeLogs.enabled.
All browser sessions require OCC_AGENT_NATIVE_ADMIN_DOMAIN, including the
bootstrap process and installations with native access disabled. In default shared-cookie mode, OCC derives the
shared cookie domain from it and the HTTPS OCC_AUTH_BASE_URL. The removed
OCC_AUTH_COOKIE_DOMAIN and agentNativeAdmin.sharedCookieDomain inputs are rejected.
Native browser access uses these settings:
| Variable | Required value or format | Behavior |
|---|---|---|
OCC_AGENT_NATIVE_ADMIN_ENABLED |
true. |
Enables Agent chat and administration through native browser access. |
OCC_AGENT_NATIVE_ADMIN_DOMAIN |
Agent host suffix, such as agents.oce.example.com, without scheme, wildcard, port, or path. |
Required for shared cookie scope; also derives stable per-Agent browser hosts. |
OCC_AGENT_NATIVE_ADMIN_SESSION_MODE |
shared-cookie (default) or host-only-exchange. |
Host-only exchange requires guarded OIDC and same-site HTTPS; see native sessions. |
For changes to startup logging.level, follow the
log-level procedure.
The API and worker load the same trusted startup YAML; only the API initializes
the optional Backend client. Both validate Backend membership
and stored ownership before accepting work. With the bundled Kubernetes Compute
Driver, drivers.compute.configuration holds the
KubernetesComputeDriverOptions shape.
Production use of that Driver requires images.requireImmutableDigest: true,
digest-pinned gateway and Agent image references, projected ServicePrincipal
credentials unless a Sandbox Driver is selected, and exactly one in-cluster
identity or explicitly named kubeconfig/context. The processes then verify
authenticated, TLS-checked, read-only Kubernetes Namespace access before serving
requests or claiming work. Installed Drivers validate their own reviewed
configuration and implementation-specific prerequisites.
Agent workspace-file requests use the selected Compute Driver's private gateway endpoint; private Agent gateway routes define the hostname contract and each setting's role.
With chart-managed routing, Helm sets OCC_CHART_GATEWAY_ROUTING for the API,
worker, and upgrade configuration check. It contains the chart's Gateway name,
release namespace, Envoy namespace, and effective and automatic hostnames.
When the bundled Kubernetes Compute Driver selects gatewayRouting, startup
requires its resource identity and effective hostname to match; otherwise it
reports CHART_LISTENER_MISMATCH before serving or replacing the API. This is
a chart-owned consistency check, not an operator input. Correct the Installation
and Helm values together; see production installation.
OCC_GATEWAY_API_KEY_PATH mounts a dedicated, high-entropy Envoy service key;
never reuse the Better Auth signing secret or a model-provider credential.
Missing or invalid key files fail startup; a file that becomes unavailable
during rotation makes new requests unavailable. The worker needs
route configuration and namespace-bound HTTPRoute permissions, but no service
key or CA bundle for native file access.
The TLS lifecycle covers the generated private CA, explicit issuers, and the trust OCC loads.
See the deployment procedure
for Envoy, cert-manager, native trusted-proxy configuration, and key/certificate rotation.
Kubernetes gateway authentication is always trusted-proxy; private routing
still requires the Installation, Helm, and service-key settings above. Unsupported Drivers and unavailable endpoints return
503 DEPENDENCY_UNAVAILABLE.
Sessions and service keys grant no rights without IAM; see
denials for 401 and 403,
Authentication for service-key issuance,
scope, and revocation, and the deployment guide
for the procedure. Normal issuance and verification require no additional
settings; initial-key delivery uses the bootstrap settings below.
The API refuses a shorter auth secret with AUTH_SECRET_INVALID before it
constructs Drivers, opens PostgreSQL, or changes configured IAM policy.
Auth-secret rotation takes effect after
replacing the mounted Secret and restarting; it also invalidates every
known-device cookie until each browser's next sign-in.
Revoking one account's cookies needs no rotation: reset its password.
Control Plane audit
Control Plane audit is off by default. Restrict API access to approved clients and
check ledger capacity before enabling it.
Rejected requests can fill the ledger and block audited actions.
Set Helm controlPlaneAudit.enabled: true or set
OCC_CONTROL_PLANE_AUDIT_ENABLED=true on the API and worker.
External sign-in and trusted proxies
These optional variables reach only the API, never other workloads.
| Variable | Helm value | Behavior |
|---|---|---|
OCC_AUTH_RECOVERY_USER_ID |
auth.recoveryUserId |
Existing local password administrator's user ID; designates the recovery account on first activation. |
OCC_AUTH_PASSWORD_SIGN_IN |
auth.passwordSignIn |
all (default, not rendered) or recovery-only: only the recovery account may use a password. Needs Google or OIDC; see recovery-only. |
OCC_AUTH_TRUSTED_PROXY_CIDRS |
api.trustedProxy.cidrs |
Comma-separated IPv4 or IPv6 CIDRs, subject to the limits below. Requests whose socket peer is inside them may carry forwarded headers. |
OCC_AUTH_TRUSTED_PROXY_PRESET |
api.trustedProxy.preset |
ingress-nginx (default), aws or generic. Named presets read x-forwarded-for. Set with the CIDRs. |
OCC_AUTH_CLIENT_IP_HEADER |
api.trustedProxy.clientAddressHeader |
Lowercase header name, up to 64 characters, generic only. Sign-in limits key on the client address it carries from a trusted peer. |
The API Deployment uses Recreate: an upgrade stops the old API Pod first, so
two controllers never serve together. Activation still requires closed ingress and stopped identity writers.
Google and OIDC have API-only HTTPS egress rules. Empty provider egressCidrs
allows every address except link-local 169.254.0.0/16. An explicit list replaces
that default, including the exception; keep it narrow. Direct GitHub sign-in
settings are rejected. See upgrade instructions.
api.trustedProxy defaults off: Forwarded, X-Forwarded-*, and X-Real-IP
return 403 on API calls (sign-in still works), and the API logs
authentication.forwarded-refused-warning at most once a minute: code
TRUSTED_PROXY_NOT_CONFIGURED, or TRUSTED_PROXY_PEER_UNTRUSTED when the sending
peer is outside api.trustedProxy.cidrs. Without a proxy, a
scanner sending X-Forwarded-For can trigger TRUSTED_PROXY_NOT_CONFIGURED;
ignore it and leave api.trustedProxy unset. Failed password sign-ins have per-email limits only: browsers behind
a proxy share its address. With Google or OIDC, start, callback, and result
key on browser cookies; start has only an active cap and the 1,000-pending-attempt
cap. Startup logs authentication.sign-in-limit-warning; Helm notes and profile
rendering also warn without failing. Set api.trustedProxy unless the API sees
each client's address. Presets:
ingress-nginx: trust the ingress controller Pod CIDR; readx-forwarded-for. Keepuse-forwarded-headersoff.aws: trust Application Load Balancer subnets targeting API Pods; readx-forwarded-for. A source-preserving Network Load Balancer needs no preset unless it fronts ingress-nginx.generic:cidrsandclientAddressHeader, such asx-real-ip, are required.
Trust only proxies that overwrite or append the header, and admit them through
api.clients. Any IPv4-mapped IPv6 spelling counts as IPv4 (prefix 1–32).
The API and chart refuse entries covering every IPv4 or IPv6 address, including
/0. Mapped peers and header hops use dotted IPv4 sign-in limit keys.
Rendering rejects invalid provider configuration, shared credential Secrets,
refused proxy CIDRs, or a different header for a named proxy preset. Credential
and routing headers such as cookie cannot supply a client address.
Google sign-in
These optional variables also apply to the API only. Google sign-in uses the same
guarded profile and OCC_AUTH_RECOVERY_USER_ID recovery user; see
Google sign-in.
| Variable | Helm value | Behavior |
|---|---|---|
OCC_AUTH_GOOGLE_CLIENT_ID |
auth.google Secret key clientIdKey |
Google OAuth web client ID; determines the provider instance. Set it with the client secret and recovery user ID, or not at all. |
OCC_AUTH_GOOGLE_CLIENT_SECRET |
auth.google Secret key clientSecretKey |
Google OAuth client secret, read from the dedicated auth.google.secretName Secret. |
OCC_AUTH_GOOGLE_ALLOWED_DOMAINS |
auth.google.allowedDomains |
Optional comma-separated hosted domains. When set, the ID token's hd must match one and email_verified must be true. Requires the client ID. |
With auth.google.enabled, the chart adds the API-only egress policy
openclaw-enterprise-api-google-login-egress on TCP 443 for
oauth2.googleapis.com and www.googleapis.com. Empty auth.google.egressCidrs
allows any address except link-local 169.254.0.0/16; narrow it with an egress proxy.
Rendering fails on incomplete Google values, a Secret shared with any other
chart Secret, agentNativeAdmin.enabled with Google, an HTTP base URL, or an allowed
domain that is not a DNS name.
OIDC sign-in
OIDC controller settings defines the provider URLs, client credentials, API egress and optional private CA trust. These API-only settings require the same guarded sign-in profile and recovery user and Google.
Production Installation bootstrap environment
Both environments run node scripts/bootstrap-installation.mjs after migration.
NODE_ENV selects development or production; no other mode is accepted.
The initializer uses the application-role database and Better Auth settings.
Development consumes the OPENCLAW_DEV_* defaults and only the private
service-key output path; it never writes a password file. API/worker startup
requires the resulting Installation and does not create credentials.
The packaged Helm initialization Job creates the singleton Installation,
human and service administrators, and initial default Namespace
before starting the API or worker. Namespace provisioning completes asynchronously
through the worker. Its separate migration
init container receives only OCC_MIGRATION_DATABASE_URL; the bootstrap
container receives the application-role OCC_DATABASE_URL, Better Auth
settings, and the following bootstrap settings. The Job sets backoffLimit: 0;
failed initialization requires manual repair
before another attempt.
| Variable | Required value or format |
|---|---|
OCC_AUTH_SECRET |
Same mounted Better Auth secret used by the API. |
OCC_AUTH_BASE_URL |
Same HTTPS DNS origin as the API. |
OCC_AGENT_NATIVE_ADMIN_DOMAIN |
Same Agent DNS suffix as the API; required for derived shared session cookies. |
OCC_BOOTSTRAP_ADMIN_EMAIL |
Email address for the first administrator account. |
OCC_BOOTSTRAP_PASSWORD_FILE |
New file path on protected operator-owned storage for the generated password. |
OCC_BOOTSTRAP_INSTALLATION_NAME |
Installation display name; it must follow the API Name rule (INSTALLATION_NAME_INVALID). |
OCC_BOOTSTRAP_SERVICE_KEY_FILE |
New private absolute JSON path; on fresh production bootstrap, a distinct sibling of the password file. |
Repeated bootstrap preserves the existing Installation only when the exact
administrator account and IAM identity still match; a mismatch fails closed.
Existing Namespaces and their configuration remain unchanged; no initial
Namespace is backfilled or recreated.
For a read-only check of a populated candidate database, set the usual production
database, authentication and administrator settings, then run
node scripts/bootstrap-installation.mjs --verify-existing "$incumbent_installation_id".
Obtain the ID independently from the authenticated incumbent Installation before
selecting the candidate database. The command checks the singleton ID and the
ordinary bootstrap administrator account, Principal and direct role binding in
one read-only PostgreSQL snapshot. On success it prints
installation.bootstrap-verified and the matched ID. An empty database, a different
ID or a failed administrator check returns a nonzero exit. This mode requires no
bootstrap output paths and creates no credentials. Check migration history
separately with scripts/migrate-production.mjs --require-current.
On fresh bootstrap, both files are created exclusively with mode 0600; their
parent directory must be private and neither destination may already exist.
Helm sets the key path from bootstrap.password.mountPath and
bootstrap.serviceKey.fileName (default initial-admin-service-key.json). The
key filename must be a simple basename distinct from bootstrap.password.fileName.
bootstrap.password.claimName must follow Kubernetes' DNS-subdomain name rule:
253 characters total, the same rule prepare-bootstrap-volume applies to
--claim. Both use that existing PVC. Reruns do not inspect,
replace, or regenerate output; see recovery.
Production operational logging collection
logging.collector configures the bundled Helm Collector; enabled defaults to
false. For enablement, existing-Collector reuse, Secret creation, networking,
and delivery checks, use
Configure platform observability.
When enabled, the chart requires a digest-pinned image, an exact exporter
destination (IPv4 /32 or paired namespace/Pod selectors), a TCP port, and
nonempty configuration and environment Secret names that differ from each other
and from every other Secret the chart reads or cert-manager writes for an enabled
feature. The named Secrets must be in the control-plane namespace:
configSecretNamesuppliescollector.yaml,kubernetes.yaml, andexporter.yamlkeys.envSecretNamesupplies exporter variables, includingOTEL_EXPORTER_OTLP_LOGS_ENDPOINT, throughenvFrom.
Relevant Helm values:
logging: collector: enabled: true image: docker.io/otel/opentelemetry-collector-contrib:0.159.0@sha256:1f2c54a30e713fac6b3ae77a1ec84010c2007e29ced8ec666214fc2f6739c1cc configSecretName: occ-otel-collector-config envSecretName: occ-otel-collector-exporter exporter: cidr: 203.0.113.10/32 port: 443 state: sizeLimit: 128MiSee chart defaults for
resources, state.sizeLimit, and tmp.sizeLimit. The
security reference owns the
credential, runtime-export, and workload isolation boundaries.
The chart rejects obvious malformed quantities such as foo, 10MiB, and
1K in top-level resources, slackProxy.resources, Collector resources,
and Collector volume size limits, with an error naming the setting. It also
rejects a value with a leading - (negative) and resource names Kubernetes
refuses. Kubernetes remains responsible for complete quantity validation;
exponent ranges, numeric parsing edge cases and a request above its limit are
not checked during rendering. A successful render does not prove API acceptance. Setting a resource map or Collector size limit to null clears its
chart default; a null size limit leaves that volume unlimited.
Private telemetry defaults
metrics.enabled defaults to true, with API and worker listeners on their Pod
IP at port 9464. Both metrics.scraperNamespaceLabels and
metrics.scraperPodLabels default to empty: no metrics ingress is granted until
both are set. Partial selectors and invalid or API-colliding ports fail rendering.
See scraping and discovery.
For an in-cluster log receiver, set both
logging.collector.exporter.namespaceLabels and podLabels, set decimal port
(1–65535, no leading zeros),
and leave cidr empty. This alternative cannot be combined with a CIDR.
Collector metrics use the same paired selector contract under
logging.collector.metrics, on fixed port 8888; metrics ingress is opt-in.
The chart grants only the selected peer and port. Other NetworkPolicies remain
additive, so review them when assessing effective access.
