mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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`
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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)
|
||||
*/}}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -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": {
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user