feat(pii): publish PII image to GHCR and add Presidio sidecar to Helm chart (#5188)

* feat(pii): publish PII image to GHCR and add Presidio sidecar to Helm chart

* fix(pii): allow app→PII NetworkPolicy egress, global tolerations, topology spread
This commit is contained in:
Theodore Li
2026-06-23 21:07:06 -04:00
committed by GitHub
parent 8b5d746fe2
commit 76867062e5
15 changed files with 523 additions and 199 deletions
+4 -2
View File
@@ -155,9 +155,8 @@ jobs:
- dockerfile: ./docker/realtime.Dockerfile
ghcr_image: ghcr.io/simstudioai/realtime
ecr_repo_secret: ECR_REALTIME
# pii is ECR-only (private ECS sidecar) — no ghcr_image, so the tag
# step below skips GHCR for it.
- dockerfile: ./docker/pii.Dockerfile
ghcr_image: ghcr.io/simstudioai/pii
ecr_repo_secret: ECR_PII
steps:
- name: Checkout code
@@ -257,6 +256,8 @@ jobs:
image: ghcr.io/simstudioai/migrations
- dockerfile: ./docker/realtime.Dockerfile
image: ghcr.io/simstudioai/realtime
- dockerfile: ./docker/pii.Dockerfile
image: ghcr.io/simstudioai/pii
steps:
- name: Checkout code
@@ -312,6 +313,7 @@ jobs:
- image: ghcr.io/simstudioai/simstudio
- image: ghcr.io/simstudioai/migrations
- image: ghcr.io/simstudioai/realtime
- image: ghcr.io/simstudioai/pii
steps:
- name: Login to GHCR
-186
View File
@@ -1,186 +0,0 @@
name: Build and Push Images
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
jobs:
build-amd64:
name: Build AMD64
runs-on: blacksmith-8vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./docker/app.Dockerfile
ghcr_image: ghcr.io/simstudioai/simstudio
ecr_repo_secret: ECR_APP
- dockerfile: ./docker/db.Dockerfile
ghcr_image: ghcr.io/simstudioai/migrations
ecr_repo_secret: ECR_MIGRATIONS
- dockerfile: ./docker/realtime.Dockerfile
ghcr_image: ghcr.io/simstudioai/realtime
ecr_repo_secret: ECR_REALTIME
# pii is ECR-only (private ECS sidecar) — no ghcr_image.
- dockerfile: ./docker/pii.Dockerfile
ecr_repo_secret: ECR_PII
outputs:
registry: ${{ steps.login-ecr.outputs.registry }}
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
if: github.ref == 'refs/heads/main'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
- name: Generate tags
id: meta
run: |
ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
ECR_REPO="${{ secrets[matrix.ecr_repo_secret] }}"
GHCR_IMAGE="${{ matrix.ghcr_image }}"
# ECR tags (always build for ECR)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
ECR_TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
ECR_TAG="dev"
else
ECR_TAG="staging"
fi
ECR_IMAGE="${ECR_REGISTRY}/${ECR_REPO}:${ECR_TAG}"
# Build tags list
TAGS="${ECR_IMAGE}"
# Add GHCR tags only for main branch (and only for images with a GHCR target)
if [ "${{ github.ref }}" = "refs/heads/main" ] && [ -n "$GHCR_IMAGE" ]; then
GHCR_AMD64="${GHCR_IMAGE}:latest-amd64"
GHCR_SHA="${GHCR_IMAGE}:${{ github.sha }}-amd64"
TAGS="${TAGS},$GHCR_AMD64,$GHCR_SHA"
fi
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
- name: Build and push images
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
provenance: false
sbom: false
build-ghcr-arm64:
name: Build ARM64 (GHCR Only)
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
if: github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./docker/app.Dockerfile
image: ghcr.io/simstudioai/simstudio
- dockerfile: ./docker/db.Dockerfile
image: ghcr.io/simstudioai/migrations
- dockerfile: ./docker/realtime.Dockerfile
image: ghcr.io/simstudioai/realtime
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
- name: Generate ARM64 tags
id: meta
run: |
IMAGE="${{ matrix.image }}"
echo "tags=${IMAGE}:latest-arm64,${IMAGE}:${{ github.sha }}-arm64" >> $GITHUB_OUTPUT
- name: Build and push ARM64 to GHCR
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
provenance: false
sbom: false
create-ghcr-manifests:
name: Create GHCR Manifests
runs-on: blacksmith-2vcpu-ubuntu-2404
needs: [build-amd64, build-ghcr-arm64]
if: github.ref == 'refs/heads/main'
strategy:
matrix:
include:
- image: ghcr.io/simstudioai/simstudio
- image: ghcr.io/simstudioai/migrations
- image: ghcr.io/simstudioai/realtime
steps:
- name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifests
run: |
IMAGE_BASE="${{ matrix.image }}"
# Create latest manifest
docker manifest create "${IMAGE_BASE}:latest" \
"${IMAGE_BASE}:latest-amd64" \
"${IMAGE_BASE}:latest-arm64"
docker manifest push "${IMAGE_BASE}:latest"
# Create SHA manifest
docker manifest create "${IMAGE_BASE}:${{ github.sha }}" \
"${IMAGE_BASE}:${{ github.sha }}-amd64" \
"${IMAGE_BASE}:${{ github.sha }}-arm64"
docker manifest push "${IMAGE_BASE}:${{ github.sha }}"
+1 -1
View File
@@ -1,5 +1,5 @@
# ========================================
# Combined Presidio service (analyzer + anonymizer) on a single port (3000)
# Combined Presidio service (analyzer + anonymizer) on a single port (5001)
# ========================================
FROM python:3.12-slim-bookworm AS base
+31 -1
View File
@@ -48,6 +48,7 @@ Optional components (off by default):
* **`copilot`** — the Sim Copilot service plus its own Postgres StatefulSet.
* **`ollama`** — local LLM inference, with optional NVIDIA GPU support.
* **`pii`** — Presidio PII redaction sidecar (analyzer + anonymizer) for the Guardrails PII block and log redaction. See [PII redaction](#pii-redaction).
* **`telemetry`** — OpenTelemetry Collector wired to Jaeger / Prometheus / OTLP backends.
* **`ingress`** — NGINX-style Ingress for the app and realtime services.
* **`networkPolicy`** — east-west and egress isolation (blocks cloud metadata endpoints by default).
@@ -321,7 +322,7 @@ User-supplied `securityContext` values are merged with the defaults — your val
Other security features:
* `automountServiceAccountToken: false` on the ServiceAccount **and** every pod.
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A value placed in `app.env` always wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists).
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A value placed in `app.env` always wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists).
* Optional `networkPolicy.enabled=true` enforces east-west isolation and blocks cloud metadata endpoints in egress.
---
@@ -354,6 +355,35 @@ Requires the Prometheus Operator CRDs. Scrapes `/metrics` on the app and realtim
---
## PII redaction
Sim can redact personally identifiable information using a [Presidio](https://microsoft.github.io/presidio/) sidecar (analyzer + anonymizer combined into one image listening on port 5001). Enable it with:
```yaml
pii:
enabled: true
```
When enabled, the chart deploys the sidecar (`<release>-pii` Deployment + Service) and **auto-wires** `PII_URL` on the app to the in-cluster service. The sidecar bundles five large spaCy models (en/es/it/pl/fi, ~2.2GB), so the first start takes ~3 minutes while models load — the `startupProbe` allows for this. Size the `pii.resources` for at least ~4Gi memory.
This alone powers the **Guardrails PII block** and on-demand masking. To additionally turn on **automatic log redaction** (the org/workspace data-retention scrub), you must:
```yaml
app:
env:
PII_REDACTION: "true"
# The log-redaction path calls the app's own /api/guardrails/mask-batch,
# which must be reachable from inside the cluster. Set this to the in-cluster
# app Service URL (NOT the public ingress, which usually isn't hairpin-reachable).
INTERNAL_API_BASE_URL: "http://<release>-app.<namespace>.svc.cluster.local:3000"
```
Without a cluster-reachable `INTERNAL_API_BASE_URL` (it falls back to `NEXT_PUBLIC_APP_URL`), the redaction path fails closed — it scrubs affected fields to `[REDACTION_FAILED]` rather than leaking, but redaction won't actually run.
> The PII image is published at `ghcr.io/simstudioai/pii` (multi-arch). If you mirror images into a private registry, retag it alongside the app/realtime/migrations images.
---
## Troubleshooting
### `Error: execution error at (sim/templates/...): app.env.BETTER_AUTH_SECRET is required for production deployment`
+3
View File
@@ -16,6 +16,9 @@ Your release is named {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.copilot.enabled }}
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-copilot
{{- end }}
{{- if .Values.pii.enabled }}
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-pii
{{- end }}
2. Reach the application:
{{- if and .Values.ingress.enabled .Values.ingress.app.host }}
+32 -3
View File
@@ -117,6 +117,22 @@ Ollama selector labels
app.kubernetes.io/component: ollama
{{- end }}
{{/*
PII (Presidio) specific labels
*/}}
{{- define "sim.pii.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: pii
{{- end }}
{{/*
PII (Presidio) selector labels
*/}}
{{- define "sim.pii.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: pii
{{- end }}
{{/*
Migrations specific labels
*/}}
@@ -261,8 +277,8 @@ externalSecrets.remoteRefs.app when ESO is enabled. When ESO is on, the
chart-managed Secret is not rendered — anything not mapped via ESO would
be silently missing at runtime.
Chart-computed keys (DATABASE_URL, SOCKET_SERVER_URL, OLLAMA_URL) are
exempt because they're inlined on the container, not sourced from the
Chart-computed keys (DATABASE_URL, SOCKET_SERVER_URL, OLLAMA_URL, PII_URL)
are exempt because they're inlined on the container, not sourced from the
Secret.
Fail-fast is only safe for ESO because we can introspect remoteRefs at
@@ -273,7 +289,7 @@ than enforced.
{{- define "sim.validateExternalSecretCoverage" -}}
{{- if and .Values.externalSecrets .Values.externalSecrets.enabled -}}
{{- $remoteRefs := default (dict) (default (dict) .Values.externalSecrets.remoteRefs).app -}}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" -}}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_URL" -}}
{{- $appEnv := default (dict) .Values.app.env -}}
{{/*
Required-key coverage: these are non-optional at runtime. With ESO enabled
@@ -430,6 +446,19 @@ Ollama URL
{{- end }}
{{- end }}
{{/*
PII (Presidio) sidecar URL
*/}}
{{- define "sim.piiUrl" -}}
{{- if .Values.pii.enabled }}
{{- $serviceName := printf "%s-pii" (include "sim.fullname" .) }}
{{- $port := .Values.pii.service.port }}
{{- printf "http://%s:%v" $serviceName $port }}
{{- else }}
{{- .Values.app.env.PII_URL | default "http://localhost:5001" }}
{{- end }}
{{- end }}
{{/*
Socket Server URL (internal)
*/}}
+3 -1
View File
@@ -84,6 +84,8 @@ spec:
value: {{ include "sim.socketServerUrl" . | quote }}
- name: OLLAMA_URL
value: {{ include "sim.ollamaUrl" . | quote }}
- name: PII_URL
value: {{ include "sim.piiUrl" . | quote }}
{{- /*
Skip envDefaults keys that the user has explicitly overridden in app.env
with a non-empty value. K8s `env` takes precedence over `envFrom`, so an
@@ -112,7 +114,7 @@ spec:
and in inline mode (values flow through the chart-managed Secret).
*/}}
{{- if and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_URL" }}
{{- range $key, $value := $appEnv }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (not (has $key $chartComputed)) }}
- name: {{ $key }}
+89
View File
@@ -0,0 +1,89 @@
{{- if .Values.pii.enabled }}
---
# Deployment for the Presidio PII redaction sidecar (analyzer + anonymizer combined)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-pii
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.pii.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.pii.replicaCount }}
selector:
matchLabels:
{{- include "sim.pii.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sim.pii.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.pii | nindent 6 }}
{{- include "sim.nodeSelector" .Values.pii | nindent 6 }}
{{- include "sim.tolerations" .Values | nindent 6 }}
{{- include "sim.affinity" .Values | nindent 6 }}
{{- include "sim.topologySpreadConstraints" .Values.pii | nindent 6 }}
containers:
- name: pii
image: {{ include "sim.image" (dict "imageRoot" .Values.pii.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.pii.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.pii.service.targetPort }}
protocol: TCP
{{- if or .Values.pii.env .Values.extraEnvVars }}
env:
{{- range $key, $value := .Values.pii.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if .Values.pii.startupProbe }}
startupProbe:
{{- toYaml .Values.pii.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.pii.livenessProbe }}
livenessProbe:
{{- toYaml .Values.pii.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.pii.readinessProbe }}
readinessProbe:
{{- toYaml .Values.pii.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.pii | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.pii | nindent 10 }}
{{- if or .Values.extraVolumeMounts .Values.pii.extraVolumeMounts }}
volumeMounts:
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.pii.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if or .Values.extraVolumes .Values.pii.extraVolumes }}
volumes:
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.pii.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+1 -1
View File
@@ -105,7 +105,7 @@ spec:
deployment.
*/}}
{{- if and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_URL" }}
{{- /*
Build the effective realtime env from app.env as the base, then
overlay non-empty realtime.env values. Sprig's `merge` keeps the
+46
View File
@@ -81,6 +81,16 @@ spec:
- protocol: TCP
port: {{ .Values.ollama.service.targetPort }}
{{- end }}
# Allow egress to the PII (Presidio) sidecar
{{- if .Values.pii.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "sim.pii.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.pii.service.targetPort }}
{{- end }}
# Allow egress to OpenTelemetry collector (OTLP gRPC + HTTP)
{{- if .Values.telemetry.enabled }}
- to:
@@ -304,6 +314,42 @@ spec:
port: 443
{{- end }}
{{- if .Values.pii.enabled }}
---
# Network Policy for the PII (Presidio) sidecar
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-pii
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.pii.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "sim.pii.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow ingress from main application
- from:
- podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.pii.service.targetPort }}
egress:
# Allow DNS resolution. Models are baked into the image, so no external egress.
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- end }}
{{- if .Values.telemetry.enabled }}
---
# Network Policy for OpenTelemetry Collector
+3 -3
View File
@@ -3,8 +3,8 @@
Secret for app + realtime env. Every key in .Values.app.env and
.Values.realtime.env is written here and mounted via envFrom on the
respective Deployments. Chart-computed values (DATABASE_URL,
SOCKET_SERVER_URL, OLLAMA_URL) are omitted — they're injected as inline
env on the container so they reflect chart-time resolution.
SOCKET_SERVER_URL, OLLAMA_URL, PII_URL) are omitted — they're injected as
inline env on the container so they reflect chart-time resolution.
Treating all env values as secret-grade avoids maintaining a sensitivity
classifier and prevents accidental leaks when new provider keys are added.
@@ -18,7 +18,7 @@ metadata:
{{- include "sim.app.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_URL" }}
{{- /*
Intent: app.env is authoritative for shared keys (both pods envFrom this
Secret, so the app container must not be silently overwritten by a
+22 -1
View File
@@ -99,4 +99,25 @@ spec:
name: http
selector:
{{- include "sim.ollama.selectorLabels" . | nindent 4 }}
{{- end }}
{{- end }}
{{- if .Values.pii.enabled }}
---
# Service for the Presidio PII redaction sidecar
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-pii
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.pii.labels" . | nindent 4 }}
spec:
type: {{ .Values.pii.service.type }}
ports:
- port: {{ .Values.pii.service.port }}
targetPort: {{ .Values.pii.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "sim.pii.selectorLabels" . | nindent 4 }}
{{- end }}
+127
View File
@@ -0,0 +1,127 @@
suite: pii — optional Presidio sidecar + PII_URL wiring
release:
name: t
namespace: sim
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
tests:
- it: does not render the pii deployment when disabled
template: deployment-pii.yaml
asserts:
- hasDocuments: { count: 0 }
- it: renders the pii deployment + service when enabled
set:
pii.enabled: true
templates:
- deployment-pii.yaml
- services.yaml
asserts:
- template: deployment-pii.yaml
isKind: { of: Deployment }
- template: deployment-pii.yaml
equal: { path: metadata.name, value: t-sim-pii }
- template: deployment-pii.yaml
equal:
path: spec.template.spec.containers[0].ports[0].containerPort
value: 5001
- it: app pod gets chart-computed PII_URL pointing at the in-cluster service
template: deployment-app.yaml
set:
pii.enabled: true
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_URL
value: "http://t-sim-pii:5001"
- it: app pod gets the localhost PII_URL fallback when sidecar disabled
template: deployment-app.yaml
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_URL
value: "http://localhost:5001"
- it: PII_URL is excluded from the chart-managed app secret
template: secrets-app.yaml
set:
pii.enabled: true
app.env.PII_URL: "http://should-not-leak:5001"
asserts:
- notExists:
path: stringData.PII_URL
- it: user-set PII_URL never overrides the chart-computed inline value
template: deployment-app.yaml
set:
pii.enabled: true
app.env.PII_URL: "http://evil-pii:5001"
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: PII_URL
value: "http://evil-pii:5001"
- it: app NetworkPolicy allows egress to the PII sidecar
template: networkpolicy.yaml
set:
networkPolicy.enabled: true
pii.enabled: true
documentSelector:
path: metadata.name
value: t-sim-app
asserts:
- contains:
path: spec.egress
content:
to:
- podSelector:
matchLabels:
app.kubernetes.io/name: sim
app.kubernetes.io/instance: t
app.kubernetes.io/component: pii
ports:
- protocol: TCP
port: 5001
- it: renders a dedicated NetworkPolicy for the PII sidecar
template: networkpolicy.yaml
set:
networkPolicy.enabled: true
pii.enabled: true
documentSelector:
path: metadata.name
value: t-sim-pii
asserts:
- isKind: { of: NetworkPolicy }
- equal:
path: spec.podSelector.matchLabels["app.kubernetes.io/component"]
value: pii
- it: pii pod inherits global tolerations (not component-scoped)
template: deployment-pii.yaml
set:
pii.enabled: true
tolerations:
- key: dedicated
operator: Equal
value: pii
effect: NoSchedule
asserts:
- contains:
path: spec.template.spec.tolerations
content:
key: dedicated
operator: Equal
value: pii
effect: NoSchedule
+76
View File
@@ -704,6 +704,82 @@
}
}
},
"pii": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable the Presidio PII redaction sidecar"
},
"replicaCount": {
"type": "integer",
"minimum": 1,
"description": "Number of PII sidecar replicas"
},
"image": {
"type": "object",
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" },
"digest": { "type": "string" },
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"resources": {
"type": "object",
"properties": {
"limits": {
"type": "object",
"properties": {
"memory": {
"type": "string",
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
"description": "Memory limit (e.g., 8Gi, 4096Mi)"
},
"cpu": {
"type": "string",
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
"description": "CPU limit"
}
}
},
"requests": {
"type": "object",
"properties": {
"memory": {
"type": "string",
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
"description": "Memory request (e.g., 4Gi, 2048Mi)"
},
"cpu": {
"type": "string",
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
"description": "CPU request (e.g., 1000m, 1.0)"
}
}
}
}
},
"service": {
"type": "object",
"properties": {
"type": { "type": "string" },
"port": { "type": "integer" },
"targetPort": { "type": "integer" }
}
},
"env": { "type": "object" },
"nodeSelector": { "type": "object" },
"topologySpreadConstraints": { "type": "array", "items": { "type": "object" } },
"podSecurityContext": { "type": "object" },
"securityContext": { "type": "object" },
"startupProbe": { "type": "object" },
"livenessProbe": { "type": "object" },
"readinessProbe": { "type": "object" },
"extraVolumes": { "type": "array", "items": { "type": "object" } },
"extraVolumeMounts": { "type": "array", "items": { "type": "object" } }
}
},
"telemetry": {
"type": "object",
"properties": {
+85
View File
@@ -157,6 +157,7 @@ app:
ANTHROPIC_API_KEY_2: "" # Additional Anthropic API key for load balancing
ANTHROPIC_API_KEY_3: "" # Additional Anthropic API key for load balancing
OLLAMA_URL: "" # Ollama local LLM server URL
PII_URL: "" # Presidio PII sidecar URL; auto-computed when pii.enabled, override here otherwise
ELEVENLABS_API_KEY: "" # ElevenLabs API key for text-to-speech in deployed chat
# UI Branding & Whitelabeling Configuration
@@ -804,6 +805,90 @@ ollama:
extraVolumes: []
extraVolumeMounts: []
# Presidio PII redaction sidecar (analyzer + anonymizer combined, port 5001).
# When enabled, the app's PII_URL is auto-wired to this in-cluster service so the
# Guardrails PII block and on-demand masking work. To additionally enable automatic
# log redaction, set app.env.PII_REDACTION="true" AND app.env.INTERNAL_API_BASE_URL
# to the in-cluster app service URL (the redaction path calls the app's own
# /api/guardrails/mask-batch, which must be reachable from inside the cluster).
pii:
# Enable/disable the PII redaction sidecar
enabled: false
# Image configuration. repository resolves to ghcr.io/simstudioai/pii — the
# sim.image helper auto-prepends global.imageRegistry to simstudioai/* repos,
# so do NOT fully-qualify it here.
image:
repository: simstudioai/pii
tag: "" # defaults to Chart.AppVersion
digest: "" # sha256: pin overrides tag
pullPolicy: IfNotPresent
# Number of replicas
replicaCount: 1
# Resource limits and requests. Five large spaCy models (en/es/it/pl/fi, ~2.2GB)
# load into memory at startup, so size generously.
resources:
limits:
memory: "8Gi"
cpu: "2000m"
requests:
memory: "4Gi"
cpu: "1000m"
# The sim.podSecurityContext / sim.containerSecurityContext helpers already inject
# non-root + uid/gid/fsGroup 1001 + restricted defaults (drop ALL caps, no privilege
# escalation, RuntimeDefault seccomp), matching docker/pii.Dockerfile's USER 1001.
# Leave these empty to inherit those defaults; override only if you must.
podSecurityContext: {}
securityContext: {}
# Environment variables for the sidecar container
env: {}
# Node scheduling. nodeSelector pins the (memory-heavy, ~4Gi) sidecar to a
# node pool; tolerations are inherited from the top-level `tolerations` key
# (shared with app/realtime), and topologySpreadConstraints spreads replicas.
nodeSelector: {}
topologySpreadConstraints: []
# Service configuration
service:
type: ClusterIP
port: 5001
targetPort: 5001
# Health checks. Cold start is slow (~180s) while spaCy models load, so the
# startupProbe grace is generous (failureThreshold * periodSeconds ≈ 300s).
startupProbe:
httpGet:
path: /health
port: 5001
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /health
port: 5001
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 5001
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Additional volumes for the pii deployment
extraVolumes: []
extraVolumeMounts: []
# Ingress configuration
# When services share the same host, paths are consolidated into a single rule.
# Path order: realtime paths, copilot paths, then app paths (most specific first).