Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion helm/sim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,15 @@ Before installing in production, confirm each of the following:
* **Secrets management** — provide secrets via External Secrets Operator (ESO) or pre-created Kubernetes Secrets. Never commit secrets to `values.yaml`.
* **TLS / Ingress** — set the `cert-manager.io/cluster-issuer` annotation on the ingress and tune `proxy-body-size` / `proxy-read-timeout` for your workload. See commented examples in `values.yaml`.
* **Network policy egress** — review `networkPolicy.egressExceptCidrs`. Defaults block cloud metadata endpoints (`169.254.169.254/32`, `169.254.170.2/32`); add your cluster's API server CIDR for stronger isolation. Custom egress rules go in `networkPolicy.egress` (a list).
* **Namespace hardening** — label the install namespace with Pod Security Standards `restricted` enforcement (`pod-security.kubernetes.io/enforce=restricted`).
* **Network policy ingress** — `networkPolicy.ingressFrom` defaults to `[{}]` (an empty peer selector), which allows ingress traffic from **any pod in the cluster**, not just your ingress controller. This is a deliberate simple default, not a locked-down one. On a shared or multi-tenant cluster, scope it down, e.g. to the ingress-nginx namespace:
```yaml
networkPolicy:
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
```
* **Namespace hardening** — label the install namespace with Pod Security Standards `restricted` enforcement (`pod-security.kubernetes.io/enforce=restricted`). All workloads set `runAsNonRoot`, drop all Linux capabilities, disable privilege escalation, and set `seccompProfile: RuntimeDefault` — the four controls the Restricted profile requires. `readOnlyRootFilesystem` is intentionally **not** defaulted anywhere (Postgres/Ollama genuinely need a writable root; the stateless services — `realtime`, `pii`, `copilot` — could tolerate it but aren't pre-wired with a `/tmp` `emptyDir`). If your policy requires it, set `<component>.securityContext.readOnlyRootFilesystem: true` and mount an `emptyDir` at `/tmp` yourself via `extraVolumes`/`extraVolumeMounts`.
* **Env validation** — keys under `app.env`, `realtime.env`, and `copilot.env` are passed through to the application and validated at startup. The JSON Schema intentionally does not enforce `additionalProperties: false` (would break custom user envs), so typos like `OPENA_API_KEY` (instead of `OPENAI_API_KEY`) surface as missing-key errors at runtime, not at `helm install` time. Review your env block carefully.
* **Set public URLs** — `app.env.NEXT_PUBLIC_APP_URL` and `app.env.BETTER_AUTH_URL` must match your public origin (e.g. `https://sim.example.com`). Leaving them as `localhost` breaks sign-in.

Expand Down Expand Up @@ -287,6 +295,17 @@ externalSecrets:
INTERNAL_API_SECRET: sim/app/internal-api-secret
postgresql:
password: sim/postgresql/password
# Only needed when copilot.enabled=true and copilot.server.secret.create=true.
# Every non-empty copilot.server.env key must have a matching entry here —
# template rendering fails with a clear message naming the missing key otherwise.
copilot:
AGENT_API_DB_ENCRYPTION_KEY: sim/copilot/agent-api-db-encryption-key
INTERNAL_API_SECRET: sim/copilot/internal-api-secret
LICENSE_KEY: sim/copilot/license-key
SIM_BASE_URL: sim/copilot/sim-base-url
SIM_AGENT_API_KEY: sim/copilot/sim-agent-api-key
REDIS_URL: sim/copilot/redis-url
OPENAI_API_KEY_1: sim/copilot/openai-api-key
```

See `examples/values-external-secrets.yaml`.
Expand Down
12 changes: 8 additions & 4 deletions helm/sim/examples/values-copilot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ copilot:

# Required secrets (set via values or provide your own secret)
env:
PORT: "8080"
SERVICE_NAME: "copilot"
ENVIRONMENT: "production"
AGENT_API_DB_ENCRYPTION_KEY: "" # openssl rand -hex 32
INTERNAL_API_SECRET: "" # reuse Sim INTERNAL_API_SECRET
LICENSE_KEY: "" # Provided by Sim team
Expand All @@ -72,9 +69,16 @@ copilot:
SIM_AGENT_API_KEY: "" # Must match SIM-side COPILOT_API_KEY
REDIS_URL: "redis://default:password@redis:6379"
# Optional configuration
LOG_LEVEL: "info"
CORS_ALLOWED_ORIGINS: "https://sim.example.com"
OTEL_EXPORTER_OTLP_ENDPOINT: ""

# Non-secret operational tunables — inlined as plain container env, never go through
# the Secret/ExternalSecret, so they don't need externalSecrets.remoteRefs.copilot entries
envDefaults:
PORT: "8080"
SERVICE_NAME: "copilot"
ENVIRONMENT: "production"
LOG_LEVEL: "info"

# Create a Secret from the values above. Set create=false to reference an existing secret instead.
secret:
Expand Down
10 changes: 10 additions & 0 deletions helm/sim/examples/values-external-secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ externalSecrets:
API_ENCRYPTION_KEY: "sim/app/api-encryption-key"
postgresql:
password: "sim/postgresql/password"
# Only needed when copilot.enabled=true and copilot.server.secret.create=true
# (uncomment and pre-populate the referenced paths in your secret store):
# copilot:
# AGENT_API_DB_ENCRYPTION_KEY: "sim/copilot/agent-api-db-encryption-key"
# INTERNAL_API_SECRET: "sim/copilot/internal-api-secret"
# LICENSE_KEY: "sim/copilot/license-key"
# SIM_BASE_URL: "sim/copilot/sim-base-url"
# SIM_AGENT_API_KEY: "sim/copilot/sim-agent-api-key"
# REDIS_URL: "sim/copilot/redis-url"
# OPENAI_API_KEY_1: "sim/copilot/openai-api-key"

app:
enabled: true
Expand Down
59 changes: 54 additions & 5 deletions helm/sim/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,38 @@ Migrations specific labels
app.kubernetes.io/component: migrations
{{- end }}

{{/*
Copilot specific labels
*/}}
{{- define "sim.copilot.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: copilot
{{- end }}

{{/*
Copilot selector labels
*/}}
{{- define "sim.copilot.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: copilot
{{- end }}

{{/*
Copilot PostgreSQL specific labels
*/}}
{{- define "sim.copilotPostgresql.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: copilot-postgresql
{{- end }}

{{/*
Copilot PostgreSQL selector labels
*/}}
{{- define "sim.copilotPostgresql.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: copilot-postgresql
{{- end }}

{{/*
Create the name of the service account to use
*/}}
Expand Down Expand Up @@ -609,16 +641,33 @@ Validate Copilot configuration
{{- end -}}
{{- if .Values.copilot.server.secret.create -}}
{{- $env := .Values.copilot.server.env -}}
{{- $useExternalSecrets := and .Values.externalSecrets .Values.externalSecrets.enabled -}}
{{- $remoteRefs := default (dict) (default (dict) .Values.externalSecrets.remoteRefs).copilot -}}
{{- $required := list "AGENT_API_DB_ENCRYPTION_KEY" "INTERNAL_API_SECRET" "LICENSE_KEY" "SIM_BASE_URL" "SIM_AGENT_API_KEY" "REDIS_URL" -}}
{{- range $key := $required -}}
{{- if not (and $env (index $env $key) (ne (index $env $key) "")) -}}
{{- fail (printf "copilot.server.env.%s is required when copilot is enabled" $key) -}}
{{- $inEnv := and $env (index $env $key) (ne (index $env $key) "") -}}
{{- $mapped := index $remoteRefs $key -}}
{{- if not (or $inEnv (and $useExternalSecrets $mapped)) -}}
{{- if $useExternalSecrets -}}
{{- fail (printf "Required key '%s' is missing: externalSecrets.enabled=true but the key is neither set in copilot.server.env nor mapped in externalSecrets.remoteRefs.copilot. Map it via externalSecrets.remoteRefs.copilot.%s='path/in/store' so it is synced into the copilot Secret." $key $key) -}}
{{- else -}}
{{- fail (printf "copilot.server.env.%s is required when copilot is enabled" $key) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $useExternalSecrets -}}
{{- range $key, $value := $env -}}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") -}}
{{- if not (index $remoteRefs $key) -}}
{{- fail (printf "Key '%s' is set in copilot.server.env but externalSecrets.enabled=true and externalSecrets.remoteRefs.copilot.%s is not configured. When ESO is enabled the chart-managed copilot Secret is not rendered, so the container would start with no value. Either map it via externalSecrets.remoteRefs.copilot.%s='path/in/store' or remove it from copilot.server.env." $key $key $key) -}}
Comment thread
waleedlatif1 marked this conversation as resolved.
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $hasOpenAI := and $env (ne (default "" (index $env "OPENAI_API_KEY_1")) "") -}}
{{- $hasAnthropic := and $env (ne (default "" (index $env "ANTHROPIC_API_KEY_1")) "") -}}
{{- $hasOpenAI := or (and $env (ne (default "" (index $env "OPENAI_API_KEY_1")) "")) (and $useExternalSecrets (index $remoteRefs "OPENAI_API_KEY_1")) -}}
{{- $hasAnthropic := or (and $env (ne (default "" (index $env "ANTHROPIC_API_KEY_1")) "")) (and $useExternalSecrets (index $remoteRefs "ANTHROPIC_API_KEY_1")) -}}
{{- if not (or $hasOpenAI $hasAnthropic) -}}
{{- fail "Set at least one of copilot.server.env.OPENAI_API_KEY_1 or copilot.server.env.ANTHROPIC_API_KEY_1" -}}
{{- fail "Set at least one of copilot.server.env.OPENAI_API_KEY_1 or copilot.server.env.ANTHROPIC_API_KEY_1 (or map one via externalSecrets.remoteRefs.copilot when externalSecrets.enabled=true)" -}}
{{- end -}}
{{- end -}}
{{- if .Values.copilot.postgresql.enabled -}}
Expand Down
75 changes: 60 additions & 15 deletions helm/sim/templates/deployment-copilot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ metadata:
name: {{ include "sim.fullname" . }}-copilot
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.labels" . | nindent 4 }}
spec:
type: {{ .Values.copilot.server.service.type }}
ports:
Expand All @@ -16,36 +15,38 @@ spec:
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.selectorLabels" . | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-copilot
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.copilot.server.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "sim.name" . }}
Comment thread
waleedlatif1 marked this conversation as resolved.
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secrets-copilot.yaml") . | sha256sum }}
{{- /*
Hash both the inline Secret and the ExternalSecret template — whichever mode is
active, only one renders non-empty content, but concatenating both means a mode
switch or a remoteRefs.copilot mapping change still changes the checksum and
forces a rollout. This can't see the live value ESO syncs from the external
store (Helm only has the ExternalSecret manifest at render time, not the
store's payload) — that's an inherent ESO limitation, not something a Helm
checksum can close; ESO's own reconciliation handles picking up rotated values.
*/}}
checksum/secret: {{ printf "%s%s" (include (print $.Template.BasePath "/secrets-copilot.yaml") .) (include (print $.Template.BasePath "/external-secret-copilot.yaml") .) | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
Expand Down Expand Up @@ -85,9 +86,35 @@ spec:
{{- with .Values.copilot.server.extraEnvFrom }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.server.extraEnv }}
{{- /*
copilot.server.env flows through envFrom (the Secret/ExternalSecret) above.
copilot.server.envDefaults are inlined here so they don't leak into the
chart-managed Secret and don't need to be mapped when
externalSecrets.enabled=true — same rationale as app.envDefaults. Skipped
entirely in existingSecret mode: the pre-created Secret is the source of
truth, and inlining defaults would silently shadow keys (PORT, LOG_LEVEL,
etc.) the user stored there via envFrom, since explicit env wins over
envFrom. Keyed purely on copilot.server.secret.create, NOT the global
externalSecrets.enabled flag — envFrom always points at the user-provided
Secret name whenever secret.create=false, regardless of whether ESO is used
for other components, so the global flag is irrelevant here.
*/}}
{{- $useExistingSecret := not .Values.copilot.server.secret.create }}
{{- if or (not $useExistingSecret) .Values.copilot.server.extraEnv }}
env:
{{- $copilotEnv := .Values.copilot.server.env | default dict }}
{{- if not $useExistingSecret }}
{{- range $key, $value := .Values.copilot.server.envDefaults | default dict }}
{{- $override := index $copilotEnv $key }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (or (not $override) (eq (toString $override) "") (eq (toString $override) "<nil>")) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
Comment thread
waleedlatif1 marked this conversation as resolved.
{{- end }}
{{- with .Values.copilot.server.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
Comment thread
waleedlatif1 marked this conversation as resolved.
{{- end }}
{{- if .Values.copilot.server.startupProbe }}
startupProbe:
Expand All @@ -106,5 +133,23 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.server | nindent 10 }}
{{- if or .Values.extraVolumeMounts .Values.copilot.server.extraVolumeMounts }}
volumeMounts:
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.server.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if or .Values.extraVolumes .Values.copilot.server.extraVolumes }}
volumes:
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.copilot.server.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}

38 changes: 38 additions & 0 deletions helm/sim/templates/external-secret-copilot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{{- if and .Values.externalSecrets.enabled .Values.copilot.enabled .Values.copilot.server.secret.create }}
# ExternalSecret for copilot server credentials (syncs from external secret managers)
#
# Mirrors external-secret-app.yaml: the data list is generated by iterating every
# non-empty entry in externalSecrets.remoteRefs.copilot. sim.copilot.validate fails
# template rendering if a required key (or any non-empty copilot.server.env key) is
# missing a matching remoteRefs.copilot entry, so a misconfiguration surfaces at
# `helm template` time instead of a container starting with an empty value.
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.copilot.envSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilot.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ required "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" .Values.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" }}
target:
name: {{ include "sim.copilot.envSecretName" . }}
creationPolicy: Owner
data:
{{- range $secretKey, $ref := .Values.externalSecrets.remoteRefs.copilot }}
{{- if $ref }}
{{- if kindIs "string" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
key: {{ $ref }}
{{- else if kindIs "map" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
{{- toYaml $ref | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
7 changes: 2 additions & 5 deletions helm/sim/templates/poddisruptionbudget.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ metadata:
name: {{ include "sim.fullname" $ }}-copilot-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "sim.labels" $ | nindent 4 }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.labels" $ | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
Expand All @@ -90,8 +89,6 @@ spec:
{{- end }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "sim.name" $ }}
app.kubernetes.io/instance: {{ $.Release.Name }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
8 changes: 3 additions & 5 deletions helm/sim/templates/secrets-copilot.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{{- if and .Values.copilot.enabled .Values.copilot.server.secret.create }}
{{- if and .Values.copilot.enabled .Values.copilot.server.secret.create (not (and .Values.externalSecrets .Values.externalSecrets.enabled)) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sim.copilot.envSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.labels" . | nindent 4 }}
{{- with .Values.copilot.server.secret.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
Expand All @@ -25,8 +24,7 @@ metadata:
name: {{ include "sim.copilot.databaseSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot
{{- include "sim.copilot.labels" . | nindent 4 }}
{{- with .Values.copilot.server.secret.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
Expand Down
Loading
Loading