Skip to content

Commit 7686706

Browse files
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
1 parent 8b5d746 commit 7686706

15 files changed

Lines changed: 523 additions & 199 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,8 @@ jobs:
155155
- dockerfile: ./docker/realtime.Dockerfile
156156
ghcr_image: ghcr.io/simstudioai/realtime
157157
ecr_repo_secret: ECR_REALTIME
158-
# pii is ECR-only (private ECS sidecar) — no ghcr_image, so the tag
159-
# step below skips GHCR for it.
160158
- dockerfile: ./docker/pii.Dockerfile
159+
ghcr_image: ghcr.io/simstudioai/pii
161160
ecr_repo_secret: ECR_PII
162161
steps:
163162
- name: Checkout code
@@ -257,6 +256,8 @@ jobs:
257256
image: ghcr.io/simstudioai/migrations
258257
- dockerfile: ./docker/realtime.Dockerfile
259258
image: ghcr.io/simstudioai/realtime
259+
- dockerfile: ./docker/pii.Dockerfile
260+
image: ghcr.io/simstudioai/pii
260261

261262
steps:
262263
- name: Checkout code
@@ -312,6 +313,7 @@ jobs:
312313
- image: ghcr.io/simstudioai/simstudio
313314
- image: ghcr.io/simstudioai/migrations
314315
- image: ghcr.io/simstudioai/realtime
316+
- image: ghcr.io/simstudioai/pii
315317

316318
steps:
317319
- name: Login to GHCR

.github/workflows/images.yml

Lines changed: 0 additions & 186 deletions
This file was deleted.

docker/pii.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# ========================================
2-
# Combined Presidio service (analyzer + anonymizer) on a single port (3000)
2+
# Combined Presidio service (analyzer + anonymizer) on a single port (5001)
33
# ========================================
44
FROM python:3.12-slim-bookworm AS base
55

helm/sim/README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Optional components (off by default):
4848

4949
* **`copilot`** — the Sim Copilot service plus its own Postgres StatefulSet.
5050
* **`ollama`** — local LLM inference, with optional NVIDIA GPU support.
51+
* **`pii`** — Presidio PII redaction sidecar (analyzer + anonymizer) for the Guardrails PII block and log redaction. See [PII redaction](#pii-redaction).
5152
* **`telemetry`** — OpenTelemetry Collector wired to Jaeger / Prometheus / OTLP backends.
5253
* **`ingress`** — NGINX-style Ingress for the app and realtime services.
5354
* **`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
321322
Other security features:
322323

323324
* `automountServiceAccountToken: false` on the ServiceAccount **and** every pod.
324-
* 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).
325+
* 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).
325326
* Optional `networkPolicy.enabled=true` enforces east-west isolation and blocks cloud metadata endpoints in egress.
326327

327328
---
@@ -354,6 +355,35 @@ Requires the Prometheus Operator CRDs. Scrapes `/metrics` on the app and realtim
354355

355356
---
356357

358+
## PII redaction
359+
360+
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:
361+
362+
```yaml
363+
pii:
364+
enabled: true
365+
```
366+
367+
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.
368+
369+
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:
370+
371+
```yaml
372+
app:
373+
env:
374+
PII_REDACTION: "true"
375+
# The log-redaction path calls the app's own /api/guardrails/mask-batch,
376+
# which must be reachable from inside the cluster. Set this to the in-cluster
377+
# app Service URL (NOT the public ingress, which usually isn't hairpin-reachable).
378+
INTERNAL_API_BASE_URL: "http://<release>-app.<namespace>.svc.cluster.local:3000"
379+
```
380+
381+
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.
382+
383+
> 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.
384+
385+
---
386+
357387
## Troubleshooting
358388

359389
### `Error: execution error at (sim/templates/...): app.env.BETTER_AUTH_SECRET is required for production deployment`

helm/sim/templates/NOTES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Your release is named {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
1616
{{- if .Values.copilot.enabled }}
1717
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-copilot
1818
{{- end }}
19+
{{- if .Values.pii.enabled }}
20+
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-pii
21+
{{- end }}
1922

2023
2. Reach the application:
2124
{{- if and .Values.ingress.enabled .Values.ingress.app.host }}

helm/sim/templates/_helpers.tpl

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,22 @@ Ollama selector labels
117117
app.kubernetes.io/component: ollama
118118
{{- end }}
119119

120+
{{/*
121+
PII (Presidio) specific labels
122+
*/}}
123+
{{- define "sim.pii.labels" -}}
124+
{{ include "sim.labels" . }}
125+
app.kubernetes.io/component: pii
126+
{{- end }}
127+
128+
{{/*
129+
PII (Presidio) selector labels
130+
*/}}
131+
{{- define "sim.pii.selectorLabels" -}}
132+
{{ include "sim.selectorLabels" . }}
133+
app.kubernetes.io/component: pii
134+
{{- end }}
135+
120136
{{/*
121137
Migrations specific labels
122138
*/}}
@@ -261,8 +277,8 @@ externalSecrets.remoteRefs.app when ESO is enabled. When ESO is on, the
261277
chart-managed Secret is not rendered — anything not mapped via ESO would
262278
be silently missing at runtime.
263279
264-
Chart-computed keys (DATABASE_URL, SOCKET_SERVER_URL, OLLAMA_URL) are
265-
exempt because they're inlined on the container, not sourced from the
280+
Chart-computed keys (DATABASE_URL, SOCKET_SERVER_URL, OLLAMA_URL, PII_URL)
281+
are exempt because they're inlined on the container, not sourced from the
266282
Secret.
267283
268284
Fail-fast is only safe for ESO because we can introspect remoteRefs at
@@ -273,7 +289,7 @@ than enforced.
273289
{{- define "sim.validateExternalSecretCoverage" -}}
274290
{{- if and .Values.externalSecrets .Values.externalSecrets.enabled -}}
275291
{{- $remoteRefs := default (dict) (default (dict) .Values.externalSecrets.remoteRefs).app -}}
276-
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" -}}
292+
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_URL" -}}
277293
{{- $appEnv := default (dict) .Values.app.env -}}
278294
{{/*
279295
Required-key coverage: these are non-optional at runtime. With ESO enabled
@@ -430,6 +446,19 @@ Ollama URL
430446
{{- end }}
431447
{{- end }}
432448

449+
{{/*
450+
PII (Presidio) sidecar URL
451+
*/}}
452+
{{- define "sim.piiUrl" -}}
453+
{{- if .Values.pii.enabled }}
454+
{{- $serviceName := printf "%s-pii" (include "sim.fullname" .) }}
455+
{{- $port := .Values.pii.service.port }}
456+
{{- printf "http://%s:%v" $serviceName $port }}
457+
{{- else }}
458+
{{- .Values.app.env.PII_URL | default "http://localhost:5001" }}
459+
{{- end }}
460+
{{- end }}
461+
433462
{{/*
434463
Socket Server URL (internal)
435464
*/}}

helm/sim/templates/deployment-app.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ spec:
8484
value: {{ include "sim.socketServerUrl" . | quote }}
8585
- name: OLLAMA_URL
8686
value: {{ include "sim.ollamaUrl" . | quote }}
87+
- name: PII_URL
88+
value: {{ include "sim.piiUrl" . | quote }}
8789
{{- /*
8890
Skip envDefaults keys that the user has explicitly overridden in app.env
8991
with a non-empty value. K8s `env` takes precedence over `envFrom`, so an
@@ -112,7 +114,7 @@ spec:
112114
and in inline mode (values flow through the chart-managed Secret).
113115
*/}}
114116
{{- if and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
115-
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
117+
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_URL" }}
116118
{{- range $key, $value := $appEnv }}
117119
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (not (has $key $chartComputed)) }}
118120
- name: {{ $key }}

0 commit comments

Comments
 (0)