Skip to main content
This guide hardens the two production shapes:
  • the ChatCLI server, installed with the chatcli Helm chart or managed by the operator as an Instance;
  • the operator, with its REST API and web dashboard.
It only uses controls that exist in the code. The last section lists what ChatCLI does not protect, and what to put in place instead.
For the full AIOps install (operator, Instance, policies, observability), follow AIOps Production Setup. This page is about the security settings on top of it.

Prerequisites

  • Helm 3.8 or later (Helm 4 included), kubectl, openssl.
  • A namespace for the server (chatcli below) and rights to create Secrets in it.
  • For the certificate steps, cert-manager or your own CA.

The server’s security model

Read this before you choose settings. It explains what each control does and does not cover.

Step 1: Choose and create the credential

The simplest option, for machine clients you control. Everyone who has it is admin.
Server chart: put CHATCLI_SERVER_TOKEN in the Secret named by secrets.existingSecret, or set server.token, which the chart stores in a Secret and never passes as a command-line argument. Instance: spec.server.token: {name: chatcli-token, key: token}.

Step 2: TLS certificates

The certificate must cover the name clients dial. An operator-managed Instance is dialed as <instance>.<namespace>.svc.cluster.local, which needs a private CA; see TLS certificate in the AIOps setup. For a server reached from outside the cluster under a public name:
With the server chart, TLS needs two values. tls.existingSecret mounts the Secret at /etc/chatcli/tls, and the certificate and key paths default to /etc/chatcli/tls/tls.crt and /etc/chatcli/tls/tls.key (set certFile / keyFile only for other file names). tls.enabled: true without a Secret or both paths fails the install rather than serving plaintext:
cert-manager renewals change the Secret, but the server reads the certificate only at startup. Restart the Deployment after a renewal, or use an operator-managed Instance, which rolls its pods when the TLS Secret changes.

Step 3: Deploy the server with hardened values

Every key below exists in the chart’s schema, which rejects unknown keys. This example uses HS256 JWTs.
The chart’s default pod settings already run as non-root (UID 1000) with a read-only root filesystem, no capabilities and the RuntimeDefault seccomp profile, and meet the restricted Pod Security Standard. Probes use /healthz on the metrics port and a TCP check on the gRPC port, which work with TLS on.
The chart renders a PodDisruptionBudget only when replicaCount is greater than 1. Several replicas on one ReadWriteOnce sessions volume fail to schedule across nodes. Use persistence.accessModes: [ReadWriteMany] with a storage class that supports it, or stay on one replica; ReadWriteOncePod with more than one replica fails the render. Without ReadWriteMany, the chart stops the old pod before starting the new one on each rollout (maxSurge: 0, maxUnavailable: 1), so an upgrade has a short gap instead of hanging on the volume; see Rollouts on the sessions volume.
The same controls on an operator-managed Instance:

Step 4: Audit trail

With CHATCLI_AUDIT_LOG_PATH (chart security.auditLogPath, Instance spec.server.security.auditLogPath), the server appends one JSON line per RPC to a hash-chained file. Each line records the time, action and method, the caller (user:<subject> or anonymous), the role, the client host, the result (success, error or denied) and the duration. The path must be absolute and on a writable, persistent volume, as in step 3. On an Instance the only writable paths are emptyDir volumes, lost when the pod restarts, unless you enable persistence. To check that nobody edited the file, read it from the volume (for example in a helper pod that mounts the same PVC) and run, in the chatcli REPL:
The operator does not write a file audit log. It records its actions as AuditEvent resources, readable with kubectl get auditevents -A and exportable through GET /api/v1/audit/export. Enable the Kubernetes API server audit log to capture who changed ChatCLI resources.

Step 5: Encryption at rest

The server encrypts what it writes under ~/.chatcli (sessions, history, memory and other local stores), and on Kubernetes this is on by default with a key in a Secret the server or the operator creates. For production, supply the key yourself so it lives in your secret manager: security.encryptionKeySecret in the chart, spec.features.encryptionKeyRef on an Instance, or a Secret as in step 3. Do not use the chart’s security.encryptionKey: it puts the key in the pod spec as a plain value. Back the key Secret up with the PVC data; see Encryption at Rest. Rotation is described in Security.

Step 6: Agent, pipeline and plugin controls

These matter only if the server runs agent work or loads plugins.
  • Pipeline. pipeline.enabled (Instance spec.pipeline.enabled) lets clients run the coder, agent and tools on the server. Those RPCs require the admin role. Leave it off unless you need it.
  • Command allowlist. strict is the default mode: only commands in the allowlist run, and CHATCLI_AGENT_ALLOWLIST adds more (separated by , or ;). The built-in list includes shells and interpreters such as bash, python, node and eval, so it is not a sandbox. Contain the pod instead: restricted NetworkPolicy egress, no chart RBAC when the watcher is off (rbac.create: false), and nothing sensitive on the pod’s volumes.
  • Plugins. At load, every tool plugin and provider plugin needs a detached signature (<plugin>.sig) that verifies against a trusted public key — in ~/.chatcli/trusted-keys/, or for a fleet in the managed /etc/chatcli/trusted-keys/ that every user trusts. Unsigned plugins, and plugins on a machine with no trusted key, are skipped unless CHATCLI_ALLOW_UNSIGNED_PLUGINS=true, which you must never set in production. A plugin whose signature does not verify is always skipped.
    In a pod, ~/.chatcli is /home/chatcli/.chatcli. Mount the trusted public keys at /home/chatcli/.chatcli/trusted-keys with extraVolumes/extraVolumeMounts. Keep the private key out of the cluster.

Step 7: Operator, REST API and dashboard

1

API keys

The REST API and dashboard authenticate only with the X-API-Key header, against the Secret chatcli-operator-secrets (key api-keys) in the operator’s namespace. Create one key per team and role (viewer, operator, admin), and keep admin keys few. See Dashboard and REST API keys for the commands.Keys are re-read every 30 seconds. To revoke one, remove it from the Secret. Deleting both the Secret and the ConfigMap chatcli-operator-config revokes every key within about 30 seconds. An edit that leaves invalid YAML does not revoke anything: the last valid key set stays in force and the operator logs the error. Give each approver their own key and set its name: approvals are recorded against the key, and a quorum counts each key once.
2

Never enable dev mode

security.devMode: true (CHATCLI_OPERATOR_DEV_MODE) makes the REST API accept every caller as admin while no keys are loaded. The variable is read with boolean semantics, case-insensitively: true, TRUE, 1 and t all turn it on, and the startup log reports the same mode the API applies. Keep it false.
3

TLS for the REST API

Terminate TLS at your ingress, or serve HTTPS (TLS 1.3) from the operator itself:
4

CORS stays closed

Cross-origin calls are denied until you name an origin in security.corsAllowedOrigins. The dashboard is served from the same origin as the API and needs none.
5

NetworkPolicy

Set networkPolicy.enabled: true on the operator chart, restrict apiIngressFrom and metricsIngressFrom, and keep egress: restricted. The values are in step 1 of the AIOps setup.
6

RBAC on ChatCLI resources

The operator runs with a cluster-wide ClusterRole that can read and write Secrets in every namespace, change workloads, create pods (chaos stress pods), evict pods (DrainNode) and update nodes. No admission webhook checks what users submit, so Kubernetes RBAC is the only gate on who can make the operator act. Grant create/update on remediationplans, runbooks, instances, chaosexperiments, approvalrequests and the policy kinds only to the people who should hold that power. The chart pre-provisions the ClusterRoles chatcli-role-viewer, chatcli-role-operator, chatcli-role-admin and chatcli-role-superadmin; bind them yourself, since nothing binds them automatically.
7

Remediation guardrails

  • security.allowedDiagnosticCommands adds commands to the read-only ExecDiagnostic allowlist; everything else is refused.
  • security.allowedResourceTypes adds kinds that the ApplyManifest action may create. It extends the built-in list and never narrows it.
  • security.logScrubPatterns adds comma-separated regular expressions to the scrubbing applied to the logs and context the operator sends to the LLM. It does not scrub the operator’s own logs.
  • Gate risky actions with an ApprovalPolicy: see Approvals.

Step 8: Verify the images

Release images are signed with cosign (keyless) and carry SBOM and provenance attestations.
Use the same command for ghcr.io/diillson/chatcli-operator:1.226.0. Pin image tags, or digests, in production instead of latest.

Step 9: Verify the hardening

1

TLS 1.3 only, with the right name

2

Health answers, without a credential

The check reads the server’s own TLS settings, flags included, and verifies the server against the certificate it serves. With mTLS, set CHATCLI_TLS_CLIENT_CERT and CHATCLI_TLS_CLIENT_KEY in the pod.
3

Calls without a credential are refused

ca.crt is the CA of the server certificate (not needed for a public CA). $JWT is a token from step 1.
4

Reflection is off

5

Pod settings

6

Network isolation

7

Audit lines are written

After a few calls, the audit file on the volume grows by one line per RPC, and /config security verify-audit reports the chain as intact.
8

Operator REST API

What ChatCLI does not protect

These gaps exist in current releases. Cover each one with the mitigation next to it.

Final checklist

Next steps

Security

Every security control in detail.

AIOps Production Setup

The complete operator and Instance runbook.

K8s Operator

Instance fields and what the operator creates.

Environment variables

Every variable the server and the operator read.