OpenClaw EnterpriseDOCSGitHub

Install OCE on a prepared EKS cluster

Complete EKS preparation first. Continue in the same protected Bash session, from the same OCE checkout, with the staged $RUN_DIR, private $KUBECONFIG, selected $CONTEXT, image/chart references and network inventory. Do not restart the session setup or replace its protected files.

This continues the development and evaluation workflow; OpenShell production qualification remains incomplete.

Confirm the database endpoint from your infrastructure outputs; EKS does not select RDS automatically. For RDS, obtain its approved CA bundle. The templates use Secret occ-database-ca, key ca.pem, mounted at /etc/openclaw/database-ca. Both database URLs must use the database hostname and sslmode=verify-full&sslrootcert=/etc/openclaw/database-ca/ca.pem. Use the provider’s verified CA for other PostgreSQL services. Keep exact database and API egress addresses current through failover; security groups do not replace NetworkPolicies.

2. Install OpenShell and bootstrap OCC

The templates use release oce, OpenShell release openshell-gateway, namespace oce-system, GatewayClass eg, and Envoy namespace envoy-gateway-system. Keep these names; they match the staged policies.

Exclude other install, upgrade and Job writers. Use a local chart and the matching CLI. Keep the new $RUN_DIR/install-receipt directory for read-only handoff. After an interrupted install, reconcile that receipt before any further writes.

For CoreDNS, read the Kubernetes DNS zone below. For other DNS servers, get CLUSTER_DOMAIN from your cluster administrator. Confirm this DNS suffix before continuing.

bash
CLUSTER_DOMAIN=$(kubectl -n kube-system get configmap coredns -o json |  jq -er '.data.Corefile | capture("(?m)^\\s*kubernetes\\s+(?<domain>[^\\s{]+)").domain')export CLUSTER_DOMAINprintf 'Cluster DNS domain: %s\n' "$CLUSTER_DOMAIN"
bash
assets="$RUN_DIR/kubernetes"jq -n --arg registry "${SANDBOX_REGISTRY:?}" \  --arg storageClass "${STORAGE_CLASS:?}" \  --arg clusterDomain "${CLUSTER_DOMAIN:?}" \  --arg repository "${SANDBOX_REPOSITORY:?}" -f "$assets/openshell-values.jq" \  > "$RUN_DIR/openshell.json"helm upgrade --install openshell-gateway "${OPENSHELL_CHART:?}" \  -n "$PLATFORM_NS" -f "$RUN_DIR/openshell.json" --waitos_ip=$(kubectl -n "$PLATFORM_NS" get svc openshell-gateway -o jsonpath='{.spec.clusterIP}')route_host="occ-gateway-362888b35fce.envoy-gateway-system.svc.${CLUSTER_DOMAIN:?}" # Bootstrap has no Agents. Replace the loopback placeholder with the exact Envoy peers before deployment.# API_CIDRS includes the API Service and endpoint /32s.jq -n --arg cluster "$CLUSTER_NAME" --arg runtime "${RUNTIME_IMAGE:?}" --arg storageClass "${STORAGE_CLASS:?}" \  --arg hostname "$route_host" --arg openshellIP "$os_ip" \  --argjson proxyCidrs '["127.0.0.1/32"]' -f "$assets/installation.jq" \  > "$RUN_DIR/installation.json"jq -n --arg cluster "$CLUSTER_NAME" --arg controller "${CONTROLLER_IMAGE:?}" --arg email "${ADMIN_EMAIL:?}" \  --arg hostname "$route_host" --argjson databaseCidrs "${DATABASE_CIDRS:?}" \  --argjson apiCidrs "${API_CIDRS:?}" -f "$assets/values.jq" > "$RUN_DIR/values.json"

Check the browser values before the first Helm install. Keep port 18443 in the Console origin and laptop forward. These browser origins are distinct from the private Envoy hostname. For an existing HTTPS ingress, use the native-admin ingress requirements for its Console and wildcard Agent hosts.

bash
jq -e --arg d "$CLUSTER_NAME.oce.localhost" '  .auth.baseUrl==("https://console."+$d+":18443") and .agentNativeAdmin.enabled  and .agentNativeAdmin.domain==("agents."+$d)' "$RUN_DIR/values.json"

The templates use CLUSTER_DOMAIN for route and certificate names; the browser listener uses namespace-relative Service DNS. Preserve mTLS, OpenShell PKI and JWT signing material through upgrades.

Create occ_migrator and occ_app logins without superuser, role/database creation, replication or row-security bypass. As administrator, prepare the fresh application database:

sql
-- Run in psql connected to the fresh database as an administrator allowed to assign these owners.SELECT current_database() AS database_name \gsetGRANT CONNECT ON DATABASE :"database_name" TO occ_migrator, occ_app;GRANT CREATE ON DATABASE :"database_name" TO occ_migrator;CREATE SCHEMA occ AUTHORIZATION occ_migrator;CREATE SCHEMA drizzle AUTHORIZATION occ_migrator;REVOKE CREATE ON SCHEMA public FROM PUBLIC;

Prepare protected migration-url and application-url files for the same database. Use sslmode=verify-full&sslrootcert=/etc/openclaw/database-ca/ca.pem in both URLs. Bootstrap, API and worker use the application login. Verify both roles from the approved network, using protected libpq service entries with the verifier host’s CA path:

bash
for role in occ_migrator occ_app; do  psql "service=$role" -XAt -v ON_ERROR_STOP=1 \    -c 'SELECT current_database(),current_user,ssl FROM pg_stat_ssl WHERE pid=pg_backend_pid()'done

Expect the target database, login and t for each connection.

Set OCC_SECRET_DIR to a private directory outside the checkout. Put the two database URL files and the approved PostgreSQL CA bundle (database-ca.pem) there. Use one raw URL per file. For this fresh installation, generate separate random 32-byte secrets in hex for auth-secret (OCC sessions) and routing-key (private routing). Store each value without a trailing newline. Keep the files mode 0600 and retain the keys for redeployment.

Create the bootstrap claim and Secrets from the matching checkout with jq, yq v4, and Node.js satisfying engines.node in the selected release checkout's package.json. The OpenShell chart’s PKI job supplies openshell-client-tls. Check that it exists before creating the OCC inputs.

bash
: "${OCC_SECRET_DIR:?Set OCC_SECRET_DIR to the protected input directory}"for input in application-url migration-url database-ca.pem auth-secret routing-key; do  path="$OCC_SECRET_DIR/$input"  if [[ ! -f "$path" || ! -r "$path" || ! -s "$path" ]]; then    printf 'Missing, unreadable or empty input: %s\n' "$input" >&2    exit 1  fidone platform_kube=(kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" -n "$PLATFORM_NS")"${platform_kube[@]}" get secret openshell-client-tls -o namebootstrap_claim=$(jq -er '.bootstrap.password.claimName' "$RUN_DIR/values.json")bootstrap_image=$(jq -er '.images.controller' "$RUN_DIR/values.json")BOOTSTRAP_CLAIM="$bootstrap_claim" yq e '  .metadata.namespace = strenv(PLATFORM_NS) |  .metadata.name = strenv(BOOTSTRAP_CLAIM) |  .spec.storageClassName = strenv(STORAGE_CLASS)' deploy/examples/production/bootstrap-pvc.yaml > "$RUN_DIR/bootstrap-pvc.yaml""${platform_kube[@]}" create -f "$RUN_DIR/bootstrap-pvc.yaml""${platform_kube[@]}" create secret generic occ-database \  --from-file="application-url=$OCC_SECRET_DIR/application-url" \  --from-file="migration-url=$OCC_SECRET_DIR/migration-url""${platform_kube[@]}" create secret generic occ-database-ca \  --from-file="ca.pem=$OCC_SECRET_DIR/database-ca.pem""${platform_kube[@]}" create secret generic occ-auth \  --from-file="secret=$OCC_SECRET_DIR/auth-secret""${platform_kube[@]}" create secret generic occ-private-gateway-key \  --from-file="occ=$OCC_SECRET_DIR/routing-key"

Prepare the fresh claim on the oce-role=control pool, where the OCC initialization Job runs. If cluster policy forbids the preparation Pod or you cannot verify its network isolation, skip the helper. Have the storage administrator set the root of the same fresh volume to UID/GID 1000 and mode 0700.

bash
scripts/prepare-bootstrap-volume --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \  --namespace "$PLATFORM_NS" --claim "$bootstrap_claim" --image "$bootstrap_image" \  --node-selector oce-role=control

Continue after the helper reports the claim prepared or the storage administrator confirms the root is ready.

bash
# Allow the initialization hook to reach the DNS Service VIP.dns_ip=$(kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}')jq -n --arg ns "$PLATFORM_NS" --arg ip "$dns_ip" '{  apiVersion:"networking.k8s.io/v1",kind:"NetworkPolicy",  metadata:{namespace:$ns,name:"oce-initialization-dns-vip"},  spec:{podSelector:{matchLabels:{"app.kubernetes.io/name":"openclaw-enterprise","app.kubernetes.io/instance":"oce","app.kubernetes.io/component":"initialization"}},    policyTypes:["Egress"],egress:[{to:[{ipBlock:{cidr:($ip+"/32")}}],      ports:[{protocol:"UDP",port:53},{protocol:"TCP",port:53}]}]}}' |  kubectl --context "$CONTEXT" create -f -kubectl -n "$PLATFORM_NS" create secret generic occ-installation-startup \  --from-file="installation.yaml=$RUN_DIR/installation.json"occ installation install --release oce --chart "${OCE_CHART:?}" \  --kubeconfig "${KUBECONFIG:?}" --context "$CONTEXT" \  --kube-namespace "$PLATFORM_NS" --values "$RUN_DIR/values.json" \  --receipt "$RUN_DIR/install-receipt" --wait-timeout 5mkubectl -n "$PLATFORM_NS" rollout status deploy/openclaw-enterprise-api# Pin the Envoy data plane through its supported resource field.kubectl -n "$PLATFORM_NS" patch envoyproxy oce-agent-gateways --type=merge \  -p "$(jq -nc --arg image "${ENVOY_IMAGE:?}" '{spec:{provider:{type:"Kubernetes",kubernetes:{envoyDeployment:{container:{image:$image}}}}}}')"kubectl -n "$PLATFORM_NS" wait gateway/oce-agent-gateways --for=condition=Programmed --timeout=5mkubectl -n envoy-gateway-system wait --for=create service/occ-gateway-362888b35fce --timeout=3mkubectl -n envoy-gateway-system get svc occ-gateway-362888b35fce -o json |  jq -e '.spec.type=="ClusterIP" and (.spec.clusterIP|test("^[0-9]+(\\.[0-9]+){3}$")) and any(.spec.ports[];.port==443 and .protocol=="TCP") and    .metadata.labels["gateway.envoyproxy.io/owning-gateway-name"]=="oce-agent-gateways" and    .metadata.labels["gateway.envoyproxy.io/owning-gateway-namespace"]=="oce-system"'

Helm bootstraps OCC with zero workers. Keep the same session directory and kubeconfig.

Configure browser access

Continue in the same operator shell. The laptop needs OpenSSL, jq and kubectl, plus permission to read the bootstrap claim, get the listener Deployment, list its Pods and create pods/portforward in oce-system. The listener terminates TLS on Pod loopback and forwards HTTP/WebSocket traffic to OCC. OCC retains browser authentication and authorizes access to the Agent UI.

Read the protected initial owner files

After the Helm initialization Job succeeds, mount its output claim read-only. Use a fresh private output directory. Kubernetes permission to read this claim is privileged. The one-use reader must run in the claim’s volume topology on a control node with access to the chosen storage.

bash
jq -n --arg image "$CONTROLLER_IMAGE" --arg namespace "$PLATFORM_NS" \  --arg claim occ-bootstrap-admin-password --argjson nodeSelector '{"oce-role":"control"}' \  -f "$assets/bootstrap-reader.jq" |  kubectl --context "$CONTEXT" create -f -kubectl -n "$PLATFORM_NS" wait --for=condition=Ready pod/oce-owner-bootstrap-reader --timeout=3mfor file in initial-admin-password initial-admin-service-key.json; do  test ! -e "$RUN_DIR/$file"  kubectl -n "$PLATFORM_NS" exec oce-owner-bootstrap-reader -- \    node -e 'process.stdout.write(require("node:fs").readFileSync(process.argv[1]))' \    "/bootstrap/$file" > "$RUN_DIR/$file.tmp"  test -s "$RUN_DIR/$file.tmp"  chmod 600 "$RUN_DIR/$file.tmp"  mv "$RUN_DIR/$file.tmp" "$RUN_DIR/$file"donekubectl -n "$PLATFORM_NS" delete pod oce-owner-bootstrap-readermv "$RUN_DIR/initial-admin-service-key.json" "$RUN_DIR/service-key.json"export OCC_SERVICE_KEY_FILE="$RUN_DIR/service-key.json"

Use ADMIN_EMAIL and $RUN_DIR/initial-admin-password for ordinary Console password sign-in. The initial administrator has the required Agent administration access. Other owners need a browser account with administer permission on this exact Agent. The service key file authenticates operator REST calls. Retain the original bootstrap claim for recovery.

Install the loopback TLS listener

Run the following on the laptop with OpenSSL, jq and kubectl. Create a separate private test CA for this installation. Its leaf covers the Console and one-label Agent hostnames. Keep the CA key private on the laptop. Mount only the serving certificate and its key in the listener.

bash
cookie_domain="$CLUSTER_NAME.oce.localhost"console_host="console.$cookie_domain"CONSOLE_ORIGIN="https://$console_host:18443"NATIVE_ADMIN_DOMAIN="agents.$cookie_domain"tls_dir="$RUN_DIR/owner-tls"mkdir -m 700 "$tls_dir"cat > "$tls_dir/ca.cnf" <<'CONF'[req]distinguished_name=dnprompt=nox509_extensions=ca[dn]CN=OCE private owner browser CA[ca]basicConstraints=critical,CA:true,pathlen:0keyUsage=critical,keyCertSign,cRLSignCONFopenssl req -x509 -newkey rsa:3072 -nodes -days 30 -config "$tls_dir/ca.cnf" \  -keyout "$tls_dir/ca.key" -out "$tls_dir/ca.crt"openssl req -new -newkey rsa:2048 -nodes -subj "/CN=OCE private owner browser" \  -keyout "$tls_dir/tls.key" -out "$tls_dir/tls.csr"printf '%s\n' 'basicConstraints=critical,CA:false' 'keyUsage=critical,digitalSignature,keyEncipherment' \  'extendedKeyUsage=serverAuth' "subjectAltName=DNS:$console_host,DNS:*.$NATIVE_ADMIN_DOMAIN" > "$tls_dir/leaf.ext"openssl x509 -req -in "$tls_dir/tls.csr" -CA "$tls_dir/ca.crt" -CAkey "$tls_dir/ca.key" \  -CAcreateserial -days 28 -extfile "$tls_dir/leaf.ext" -out "$tls_dir/tls.crt"openssl verify -CAfile "$tls_dir/ca.crt" -verify_hostname "$console_host" "$tls_dir/tls.crt"kubectl -n "$PLATFORM_NS" create secret tls oce-owner-browser-tls --cert="$tls_dir/tls.crt" --key="$tls_dir/tls.key"kubectl -n "$PLATFORM_NS" create configmap oce-owner-listener --from-file="$assets/owner-listener.mjs"kubectl -n "$PLATFORM_NS" get svc openclaw-enterprise-api -o json > "$RUN_DIR/api-service.json"kubectl -n kube-system get svc kube-dns -o json > "$RUN_DIR/dns-service.json"jq -n --arg image "$CONTROLLER_IMAGE" --argjson api "$(cat "$RUN_DIR/api-service.json")" \  --argjson dns "$(cat "$RUN_DIR/dns-service.json")" -f "$assets/owner-listener.jq" > "$RUN_DIR/listener.json"kubectl --context "$CONTEXT" create -f "$RUN_DIR/listener.json"kubectl -n "$PLATFORM_NS" rollout status deploy/oce-owner-listener

The NetworkPolicy permits the listener (app=oce-owner-browser) to reach the exact API Service VIP and selected API Pods on TCP 8080, and the DNS VIP and CoreDNS Pods on UDP/TCP 53. The chart’s api.clients entry permits the matching API ingress. If an existing CoreDNS ingress policy restricts DNS clients, admit only app=oce-owner-browser in oce-system on UDP/TCP 53 alongside the existing clients. Re-render when these Services change. No new public address or Kubernetes Service is needed.

Print a self-contained command for a second laptop terminal. Run that printed command there and leave it running:

bash
printf 'kubectl --kubeconfig %q --context %q -n %q port-forward --address 127.0.0.1 deployment/oce-owner-listener 18443:8443\n' \  "$KUBECONFIG" "$CONTEXT" "$PLATFORM_NS"

In the original shell, verify TLS and the signed-out session. Import only $tls_dir/ca.crt into the owner browser’s trust store. The Console and Agent .localhost names must resolve to laptop loopback.

bash
curl --fail --silent --show-error --noproxy '*' --cacert "$tls_dir/ca.crt" \  --resolve "$console_host:18443:127.0.0.1" "$CONSOLE_ORIGIN/api/auth/session" |  jq -e 'has("data") and .data==null'printf 'Owner Console: %s/console/\n' "$CONSOLE_ORIGIN"

3. Authenticate and prepare the Namespace

Initialize the operator session. Its REST helper uses Kubernetes exec into the API Pod.

bash
INSTALLATION_FILE="$RUN_DIR/installation.yaml"install -m 600 "$RUN_DIR/installation.json" "$INSTALLATION_FILE"VALUES_FILE="$RUN_DIR/values.json"CONSOLE_ORIGIN=$(jq -er .auth.baseUrl "$VALUES_FILE")NATIVE_ADMIN_DOMAIN=$(jq -er .agentNativeAdmin.domain "$VALUES_FILE")source "$RUN_DIR/operator-session.sh"source "$RUN_DIR/platform.sh"namespace=$(request GET /namespaces | jq -er '[.data[]|select(.name=="default")]|select(length==1)|first.id')base="/namespaces/$namespace"export ENVOY_IMAGEbash "$assets/trusted-envoy-peers.sh" "$RUN_DIR/kubeconfig" > "$RUN_DIR/trusted-peers.json"jq --slurpfile peers "$RUN_DIR/trusted-peers.json" \  '.drivers.compute.configuration.network.gatewayTrustedProxyCidrs=$peers[0]' \  "$RUN_DIR/installation.json" > "$RUN_DIR/installation-with-peers.json"jq '.worker.replicaCount=1' \  "$RUN_DIR/values.json" > "$RUN_DIR/worker.json"# The API registers model credentials and the worker manages workspaces through OpenShell.kube -n "$PLATFORM_NS" get service openshell-gateway -o json |  jq -e --arg ns "$PLATFORM_NS" --arg release "$RELEASE" '    .spec as $svc | select($svc.type=="ClusterIP" and ($svc.selector|length)>0      and ($svc.clusterIP|test("^[0-9]+(\\.[0-9]+){3}$"))) |    [$svc.ports[] | select(.protocol=="TCP" and .port==8080)] | select(length==1) | first as $port |    {apiVersion:"networking.k8s.io/v1", kind:"NetworkPolicy",     metadata:{namespace:$ns,name:"oce-controller-openshell-management"},     spec:{podSelector:{matchLabels:{"app.kubernetes.io/name":"openclaw-enterprise",       "app.kubernetes.io/instance":$release},matchExpressions:[       {key:"app.kubernetes.io/component",operator:"In",values:["api","worker"]}]},       policyTypes:["Egress"],egress:[         {to:[{ipBlock:{cidr:($svc.clusterIP+"/32")}}],ports:[{protocol:"TCP",port:8080}]},         {to:[{podSelector:{matchLabels:$svc.selector}}],ports:[{protocol:"TCP",port:$port.targetPort}]}       ]}}' > "$RUN_DIR/management-policy.json"kube create -f "$RUN_DIR/management-policy.json"cp "$RUN_DIR/installation-with-peers.json" "$INSTALLATION_FILE"cp "$RUN_DIR/worker.json" "$VALUES_FILE"save_sessionupgrade_installation

Agent deployment pairs these egress rules with management ingress for the API, worker, selected tenant’s supervisors and Agent Gateway on TCP 8080. Keep both directions. Tenant callers must match the backing namespace name and its current openclaw.dev/namespace label. Supervisor grants apply per tenant and Gateway grants per Agent. To verify ownership-based withdrawal, inspect every grant for that namespace. For a live Agent in its verified namespace, stage the current platform helper and renderer and run deployment_network in its saved session to replace a name-only grant.

If namespace ownership has changed or the Agent’s Gateway Service is gone, withdraw the obsolete grant directly. Inspect its source namespace, Agent or supervisor Pod selector, and destination management selector with kube -n "$PLATFORM_NS" get networkpolicy "$policy" -o yaml. Match them to the recorded owner. Confirm a separate restrictive ingress policy still selects the management Pods so deleting the grant keeps them isolated. Then run kube -n "$PLATFORM_NS" delete networkpolicy "$policy" with the verified policy name. Preserve grants for other tenants and callers.

Wait for Compute to create the backing namespace, then apply these tenant bindings. Verify restricted Pod Security; grant the ordinary worker role last.

bash
TENANT_NAMESPACE=$(kubectl get ns -l "openclaw.dev/namespace=$namespace" -o json |  jq -er '.items|select(length==1)|first.metadata.name')worker_sa="$PLATFORM_NS:openclaw-enterprise-worker"api_sa="$PLATFORM_NS:openclaw-enterprise-api"kubectl -n "$TENANT_NAMESPACE" create role oce-workspace-provisioner \  --verb=get,list,watch,create,update,patch,delete --resource=roles,rolebindings \  --dry-run=client -o json |  jq '.rules += [{apiGroups:["rbac.authorization.k8s.io"],resources:["roles"],    resourceNames:["openshell-workspace-sandbox"],verbs:["bind","escalate"]}]' | kubectl create -f -kubectl -n "$TENANT_NAMESPACE" create rolebinding oce-workspace-provisioner \  --role=oce-workspace-provisioner --serviceaccount="$worker_sa"kubectl -n "$TENANT_NAMESPACE" create rolebinding oce-api-secrets \  --clusterrole=oce-openclaw-tenant-api --serviceaccount="$api_sa"kubectl -n "$TENANT_NAMESPACE" create rolebinding oce-api-configuration \  --clusterrole=oce-openclaw-tenant-configuration --serviceaccount="$api_sa"kubectl -n "$TENANT_NAMESPACE" create rolebinding openclaw-enterprise-worker \  --clusterrole=oce-openclaw-tenant-worker --serviceaccount="$worker_sa"

Wait for request GET "$base" to report status: ready before registering credentials. Keep the initial credential owner selection: OpenShell Sandbox and Credential Gateway, with no repository Credential Proxy yet.

4. Prepare the operator session

Continue in the same Bash shell to Deploy your first Agent.

Verify workspace routing

After activation, check accepted/programmed Gateway and HTTPRoute status, ready certificates and an accepted API-key SecurityPolicy. Follow the routing checks to verify invalid operator keys are rejected and dedicated Harnesses cannot bypass Envoy to reach the native Gateway.

In the Console Workspace files tab, read all four paths, save and reload a harmless change, then restore the original. Previously absent files should report NOT_FOUND with editable fields, not DEPENDENCY_UNAVAILABLE. A bound PVC or healthy OCC API alone does not prove routing. Across Pod replacement, compare PVC identities and existing session IDs to verify retained Gateway state. Record resource ownership, backups, credential renewal and recovery responsibilities using production handoff.

For later upgrades, first repeat the API and webhook policy refresh. Then use Upgrade production images, its checklist and baseline. Helm rollback does not undo committed migrations or restore Agent-volume data; use upgrade recovery after a failure.

To finish, delete the Agent, then clean up the installation with Amazon EKS cleanup.

Search documentation