Secrets
Five secrets drive the security of a deployment. Generate each with openssl rand -hex 32.
| Secret | Protects | Rotatable |
|---|---|---|
BETTER_AUTH_SECRET | Session tokens | Yes — invalidates all sessions |
ENCRYPTION_KEY | Workspace env vars, stored provider keys, MCP OAuth credentials, deployment/chat secrets | No — see below |
API_ENCRYPTION_KEY | Reversible stored copy of user-generated API keys | No — existing keys keep authenticating, but their stored copy can no longer be displayed |
INTERNAL_API_SECRET | Service-to-service calls | Yes — roll app and realtime together |
CRON_SECRET | Background job endpoints | Yes — roll app and cron together |
ENCRYPTION_KEY cannot be rotated without re-encrypting the data it protects, and cannot be recovered if lost. Changing it renders all of that data permanently unreadable. Back it up independently of the database.
BETTER_AUTH_SECRET must be identical on the app and realtime services — they share sessions through the database, and a mismatch means realtime rejects every authenticated socket.
Storing them
In increasing order of production-readiness:
--seton the command line — dev only. Values appear inhelm get valuesoutput and shell history.- A pre-created Kubernetes Secret — set
app.secrets.existingSecret.enabled: trueand the secret name. Works with Sealed Secrets and SOPS. The secret is consumed wholesale and must use the standard key names. - External Secrets Operator — sync from Vault, AWS Secrets Manager, Azure Key Vault, or GCP Secret Manager. Recommended.
In the default and External Secrets modes, the chart writes every key under app.env and realtime.env into a chart-managed Secret mounted via envFrom, so no value is inlined into a pod spec. (In existingSecret mode the pre-created Secret is the source of truth and any app.env values you still pass are rendered inline — supply everything through the Secret in that mode.) Either way, a secret committed to values.yaml is a secret in your git history.
Network boundaries
Ingress
Expose only the app (3000) and realtime (3002). Everything else — Postgres, Redis, the PII service, the cron endpoints — should be reachable only from inside the deployment.
The background job endpoints under /api/cron/*, /api/webhooks/poll/*, and /api/schedules/execute are authenticated by CRON_SECRET, but there is no reason to expose them publicly. Point cron at the in-cluster Service.
NetworkPolicy
The chart ships an optional policy that isolates east-west traffic and blocks cloud metadata endpoints (169.254.169.254/32, 169.254.170.2/32) on egress — worth enabling, because those endpoints are the standard SSRF escalation target.
networkPolicy:
enabled: truenetworkPolicy.ingressFrom defaults to [{}] — an empty peer selector that allows ingress from any pod in the cluster. On a shared or multi-tenant cluster, scope it to your ingress controller:
networkPolicy:
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginxThe policy already allows HTTPS (443) egress to everything except the metadata CIDRs, which covers model provider APIs, integration APIs, and cloud object-storage endpoints. It also allows the bundled Postgres and Redis by pod selector.
What it does not cover is any datastore you run outside the chart — a managed Postgres or Redis on a non-443 port. Add a rule for each:
networkPolicy:
enabled: true
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/16 # your VPC / managed-service subnet
ports:
- protocol: TCP
port: 6379 # managed Redis
- protocol: TCP
port: 5432 # managed PostgresThis applies even when REDIS_URL reaches the pod through a Secret rather than values.yaml — the chart cannot see the host, so it cannot generate the rule. A deployment that accepts the URL but has no matching egress rule will fail to reach Redis with networkPolicy.enabled: true.
If maintaining CIDR lists is not worth it, drop the port restriction instead:
networkPolicy:
enabled: true
allowExternalEgress: trueCloud metadata endpoints stay blocked either way. This defaults to false because Sim's chart is deliberately stricter than the common chart default, which permits unrestricted egress.
Pod Security Standards
All workloads set runAsNonRoot, drop all Linux capabilities, disable privilege escalation, and use seccompProfile: RuntimeDefault — the four controls the restricted profile requires. Label the namespace to enforce it:
kubectl label namespace simstudio pod-security.kubernetes.io/enforce=restrictedreadOnlyRootFilesystem is not set by default: Postgres and Ollama need a writable root, and the app container writes to Next.js's .next/cache. It is viable on the genuinely stateless services (realtime, pii, copilot) — set <component>.securityContext.readOnlyRootFilesystem: true and mount an emptyDir at /tmp via extraVolumes / extraVolumeMounts.
Where user code runs
Workflows can execute user-authored JavaScript and Python. Know which sandbox you are running before you expose Sim to untrusted authors.
| Mode | Configuration | Isolation |
|---|---|---|
| isolated-vm (default) | none | In-process V8 isolate inside the app container. No network namespace or filesystem separation from the app process — isolation is at the JS-engine level. JavaScript only. |
| E2B | E2B_ENABLED=true, E2B_API_KEY | Remote sandbox per execution. Strongest isolation; requires outbound access to E2B. |
| Daytona | SANDBOX_PROVIDER=daytona, DAYTONA_API_KEY | Remote sandbox per execution. |
Python execution and the tooling-dependent blocks require a remote sandbox provider — the in-process isolate runs JavaScript only.
With the default in-process sandbox, treat everyone who can author a workflow as someone running code in your app container's security context. If your Sim instance is open to a wide or partly-trusted audience, use a remote sandbox provider and enable the NetworkPolicy egress restrictions.
Resource ceilings for the in-process path:
| Variable | Controls |
|---|---|
IVM_MAX_EXECUTIONS_PER_WORKER | Executions before a worker is recycled |
IVM_MAX_BROKERS_PER_EXECUTION | Host-call brokers per execution |
IVM_MAX_BROKER_ARGS_JSON_CHARS | Max argument payload size |
IVM_MAX_BROKER_RESULT_JSON_CHARS | Max result payload size |
The SSRF boundary
Sim blocks outbound requests from database and connector tools to private, reserved, and loopback addresses. This stops a workflow from being used to scan your internal network.
Self-hosted deployments often legitimately need to reach an internal database by service name. That is opt-in:
ALLOW_PRIVATE_DATABASE_HOSTS=trueThis loosens the SSRF boundary for every workflow author on the instance. Enable it only on a trusted private network, and prefer pairing it with a NetworkPolicy that constrains what the app can actually reach.
Client IP and forwarded headers
Behind a load balancer, X-Forwarded-For is client-controllable. Set AUTH_TRUSTED_PROXIES to your proxies' actual addresses so Better Auth resolves the real client IP, and TRUSTED_ORIGINS if users reach Sim from more than one origin. Both are covered in Authentication.
Restricting who can use the instance
Signup allowlists and blocklists, social-login toggles, SSO, and the DISABLE_AUTH escape hatch are all covered in Authentication. The security-relevant summary: restrict signup before exposing the instance, and never set DISABLE_AUTH=true behind an internet-facing ingress.
PII redaction
The optional Presidio-based service supports the Guardrails PII block and, when enabled, automatic redaction of PII from workflow logs:
pii:
enabled: true
app:
env:
PII_REDACTION: "true"
INTERNAL_API_BASE_URL: "http://sim-app.simstudio.svc.cluster.local:3000"INTERNAL_API_BASE_URL must be the in-cluster Service URL. The redaction path calls the app's own API, and a public ingress URL is usually not hairpin-reachable from inside the cluster. Without a reachable value the path fails closed — affected fields are scrubbed to [REDACTION_FAILED] rather than leaking, but redaction does not actually run.
The service bundles ~2.2 GB of spaCy models, so first start takes around three minutes and it needs at least 4 GB of memory.
Pre-launch checklist
- All five secrets generated fresh, stored in a secret manager, and
ENCRYPTION_KEYbacked up separately -
BETTER_AUTH_SECRETidentical on app and realtime - Images pinned to an explicit tag or digest on app, realtime, and migrations
- TLS terminating at the ingress; HTTP redirected or disabled
-
NEXT_PUBLIC_APP_URLandBETTER_AUTH_URLset to the real public origin -
AUTH_TRUSTED_PROXIESset if behind a load balancer - Signup restricted (
DISABLE_REGISTRATIONorALLOWED_LOGIN_DOMAINS) -
DISABLE_AUTHnot set - NetworkPolicy enabled and
ingressFromscoped to the ingress controller - Namespace labelled
pod-security.kubernetes.io/enforce=restricted - Object storage buckets private, with CORS limited to your Sim origin
- Database reachable only from the deployment; TLS enforced (
sslMode: require) - Backups configured and a restore rehearsed
- Sandbox strategy decided for user code