Free lesson · GenAI Security Engineering
Deploy RAG defense system on GKE with pgvector
Deploy pgvector-backed knowledge base with access controls. Create Helm chart for RAG defense services and build end-to-end poisoning detection tests.
Course: AI Security Engineering · Chapter 6 · RAG Data Poisoning Defense
Free to read — no subscription required.
Introduction
In production, a RAG system is only as trustworthy as the controls that protect its knowledge base at runtime — but document fingerprinting, embedding-drift monitoring, canary documents, and provenance tracking deliver no value if they run as ad-hoc scripts on a developer machine instead of a hardened, monitored cluster tier. This lesson teaches you how to deploy the full RAG defense stack on Google Kubernetes Engine: a dedicated rag-defense namespace with Pod Security Admission enforcement, a pgvector StatefulSet backed by a customer-managed-key disk, defense-service Deployments wired to Workload Identity, and the operating discipline — pinned image digests and CronJob alerting — that keeps the defense plane reliable around the clock.
Key Terminology
- Pod Security Admission (PSA) — Kubernetes' built-in admission controller, activated by the
pod-security.kubernetes.io/enforce: restrictedlabel on therag-defensenamespace; atrestrictedlevel it rejects pods that run as root, mount host paths, or omit mandatory security contexts. - Audit label — The
pod-security.kubernetes.io/audit: restrictednamespace label, which records policy violations to the Kubernetes audit log regardless of enforcement state, preserving a forensic trail whenenforceis temporarily relaxed during incident response. - Image digest pinning — Referencing a container image by its immutable
@sha256:<hash>suffix (e.g.,pgvector/pgvector@sha256:<pin-digest-here>) instead of a mutable tag, guaranteeing that a registry re-push cannot silently replace what the cluster runs. - Customer-Managed Encryption Key (CMEK) disk — A GKE persistent disk whose encryption key is controlled by the operator rather than Google, provisioned via
storageClassName: pd-ssd-cmekso the pgvector data volume cannot be decrypted without the operator's key material. - Workload Identity — A GKE binding between a Kubernetes
ServiceAccount(hereprovenance-tracker-sa) and a Google Cloud IAM service account, granting pods short-lived, automatically rotated GCP credentials without embedding long-lived keys as Secrets. - Defense plane — The runtime cluster tier —
pgvectorStatefulSet,provenance-trackerDeployment, and supporting CronJobs — that enforces RAG poisoning defenses; verified healthy bycheck_defense_plane()before live ingest traffic is routed through it.
Concepts
Namespace Isolation as a Security Boundary
Kubernetes namespaces are administrative partitions, not security boundaries by default — any pod can reach any other pod across namespaces unless network policies and admission controls say otherwise. The rag-defense namespace hardens that boundary in two complementary ways: the pod-security.kubernetes.io/enforce: restricted label makes the API server itself reject non-compliant pod specs before they are scheduled, and the pod-security.kubernetes.io/audit: restricted label writes policy violations to the audit log independently of enforcement state. The audit label is the more operationally important of the two: under incident pressure, teams often loosen enforce to unblock a rollout, but without audit still set to restricted that window leaves no trace of what the cluster would have caught. Running the defense workloads — pgvector, provenance-tracker, and alerting CronJobs — inside this dedicated namespace means a misconfigured network policy or a compromised application pod in a neighboring namespace cannot silently reach the defense store (see Code Walkthrough).
Image Digest Pinning and Supply-Chain Integrity
Container tags like :latest or :1.4 are mutable pointers: a registry operator can push a new image layer under the same tag at any time, and the next rollout will silently pull the replacement. For a defense plane this is unacceptable — an attacker who can push to the upstream pgvector/pgvector repository could install a tampered image across every cluster that pulls by tag. Image digests (@sha256:<hash>) are content-addressed and immutable: the digest is a cryptographic hash of the image manifest, so the only way to change what the cluster runs is to update the manifest through a deliberate, reviewable commit. This is why both the pgvector StatefulSet and the provenance-tracker Deployment use @sha256:<pin-digest-here> references rather than tags; digest updates should go through the same change-management path as a database schema migration.
Workload Identity and Least-Privilege Access
The provenance-tracker Deployment needs access to GCP services — Cloud Storage for provenance logs, Cloud KMS for CMEK operations — but should never hold a long-lived JSON key. Workload Identity solves this by binding the Kubernetes ServiceAccount named provenance-tracker-sa to a GCP IAM service account, so pods automatically receive short-lived, rotated GCP credentials scoped to only the IAM roles explicitly granted to that account. No credential is stored in a Kubernetes Secret, and revoking access is a single IAM binding change rather than a secret-rotation cycle. Combined with namespace isolation and PSA enforcement, this creates a least-privilege posture where even a compromised defense pod cannot escalate to broader GCP resources.
Operating Discipline: Audit Trail, CronJob Alerting, and Health Verification
Deploying the defense plane is the start, not the end. Three operating practices keep it reliable: first, maintaining the pod-security.kubernetes.io/audit label even when enforce is relaxed, so any policy violation during an incident window appears in the audit log; second, wrapping CronJob-based monitors — drift detection, canary checks — with Alertmanager rules that page on missed successful runs, because a CronJob that silently stops firing provides no defense; and third, running check_defense_plane() before routing live ingest traffic to confirm every pod is in Running phase with all containerStatuses[].ready == True. The liveness and readiness probes on the provenance-tracker container feed this last check: liveness restarts a deadlocked pod, readiness gates traffic, and together they ensure the health signal the script reads is meaningful (see Code Walkthrough).
Code Walkthrough
With the operating-discipline concepts in hand, the manifest below consolidates the three foundational deployment objects — a hardened namespace, a pgvector StatefulSet pinned by image digest to a customer-managed-key disk, and a provenance-tracker Deployment wired to Workload Identity — into a single file you apply with kubectl apply -f rag-defense-stack.yaml:
Code snippetyaml
1--- 2# Hardened namespace — Pod Security Admission at restricted level 3apiVersion: v1 4kind: Namespace 5metadata: 6 name: rag-defense 7 labels: 8 pod-security.kubernetes.io/enforce: restricted 9 pod-security.kubernetes.io/audit: restricted # preserved even if enforce is loosened 10 workload: ai-security 11--- 12# pgvector StatefulSet — pinned digest, CMEK disk, dedicated node pool 13apiVersion: apps/v1 14kind: StatefulSet 15metadata: 16 name: pgvector 17 namespace: rag-defense 18spec: 19 serviceName: pgvector 20 replicas: 1 21 selector: 22 matchLabels: {app: pgvector} 23 template: 24 metadata: 25 labels: {app: pgvector} 26 spec: 27 nodeSelector: {workload: ai-security-db} 28 containers: 29 - name: pgvector 30 image: pgvector/pgvector@sha256:<pin-digest-here> # never a floating tag 31 env: 32 - name: POSTGRES_PASSWORD 33 valueFrom: 34 secretKeyRef: {name: pgvector-secret, key: password} 35 volumeMounts: 36 - {name: data, mountPath: /var/lib/postgresql/data} 37 resources: 38 requests: {cpu: "1", memory: 4Gi} 39 limits: {cpu: "2", memory: 8Gi} 40 volumeClaimTemplates: 41 - metadata: {name: data} 42 spec: 43 accessModes: [ReadWriteOnce] 44 storageClassName: pd-ssd-cmek 45 resources: {requests: {storage: 100Gi}} 46--- 47# Provenance-tracker Deployment — Workload Identity, liveness + readiness probes 48apiVersion: apps/v1 49kind: Deployment 50metadata: 51 name: provenance-tracker 52 namespace: rag-defense 53spec: 54 replicas: 2 55 selector: 56 matchLabels: {app: provenance-tracker} 57 template: 58 metadata: 59 labels: {app: provenance-tracker} 60 spec: 61 serviceAccountName: provenance-tracker-sa 62 containers: 63 - name: app 64 image: registry.example.com/rag-defense/provenance@sha256:<pin-digest-here> 65 env: 66 - {name: PGVECTOR_HOST, value: pgvector.rag-defense.svc.cluster.local} 67 - {name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://otel-collector:4317} 68 livenessProbe: 69 httpGet: {path: /healthz, port: 8080} 70 initialDelaySeconds: 10 71 readinessProbe: 72 httpGet: {path: /readyz, port: 8080} 73 initialDelaySeconds: 3 74 resources: 75 requests: {cpu: 200m, memory: 256Mi} 76 limits: {cpu: 500m, memory: 512Mi}
The pod-security.kubernetes.io/audit: restricted label is easy to skip under incident pressure, but it is what gives you the audit trail when the enforce label is temporarily relaxed — exactly the pitfall the operating-discipline section flags. Every image reference uses a digest rather than a tag so a :latest re-push on the upstream registry cannot silently change what your cluster runs.
After applying the manifest, verify the defense plane is healthy before routing live ingest traffic through it:
Code snippetpython
1import subprocess, json, sys 2 3def check_defense_plane(namespace: str = "rag-defense") -> None: 4 """Exit 1 if any pod in the defense namespace is not Running and fully Ready.""" 5 result = subprocess.run( 6 ["kubectl", "get", "pods", "-n", namespace, "-o", "json"], 7 capture_output=True, text=True, check=True, 8 ) 9 pods = json.loads(result.stdout)["items"] 10 not_ready = [ 11 p["metadata"]["name"] 12 for p in pods 13 if p["status"].get("phase") != "Running" 14 or not all( 15 c["ready"] 16 for c in p["status"].get("containerStatuses", []) 17 ) 18 ] 19 if not_ready: 20 print(f"NOT READY: {not_ready}", file=sys.stderr) 21 sys.exit(1) 22 print(f"All {len(pods)} pods in '{namespace}' are Running and Ready.") 23 24if __name__ == "__main__": 25 check_defense_plane()
You've completed this when kubectl get pods -n rag-defense shows every pod in Running state with all containers reporting Ready: True, and the script above exits with code 0.
Do's and Don'ts
Now that you have worked through the implementation, the practices below separate a durable approach from a fragile one.
Do's
- ✓Do retain the
pod-security.kubernetes.io/audit: restrictedlabel even when temporarily relaxing theenforcelabel — without the audit label, violations during an incident window are silently swallowed, leaving you no trail to reconstruct what ran inrag-defensewhile enforcement was loosened. - ✓Do pin every image reference in
rag-defense-stack.yamlto a SHA256 digest instead of a tag — a floating tag like:latestmeans a upstream re-push silently replaces what your pgvector StatefulSet or provenance-tracker Deployment runs, bypassing the entire supply-chain control the namespace hardening is designed to enforce. - ✓Do run
check_defense_plane()(orkubectl get pods -n rag-defense) before routing live ingest traffic through the defense stack — fingerprinting, drift monitoring, canary documents, and provenance tracking all fail silently if the pods that back them are crash-looping or not yet Ready, turning your hardened namespace into a false-confidence layer.
Don'ts
- ✗Don't schedule the
rag-defensenamespace's workloads onto general-purpose node pools — the pgvector StatefulSet'snodeSelector: {workload: ai-security-db}exists to isolate CMEK-disk-backed database pods from shared workloads; omitting it allows other tenant pods to share the node and potentially read memory or storage belonging to the defense plane. - ✗Don't mount pgvector's persistent volume without the
pd-ssd-cmekStorageClass — using a default StorageClass creates a disk outside the customer-managed-key envelope, meaning the vector store holding document fingerprints and provenance records is not covered by your key-rotation and revocation controls. - ✗Don't wire the
provenance-trackerDeployment to a long-lived service-account key file instead ofserviceAccountName: provenance-tracker-sa— bypassing Workload Identity means rotating or revoking GCP credentials requires a manual secret update and pod restart, and a leaked key file grants persistent access to the provenance store that no cluster-level control can revoke without redeployment.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Security Engineering subscription.
From · cancel anytime
More free lessons in AI Security Engineering
- Ch 1Monitor injection attempts with Prometheus and Grafana
- Ch 3Deploy output sanitizer as response middleware on GKE
- Ch 4Deploy multi-stage safety pipeline on GKE
- Ch 6Deploy RAG defense system on GKE with pgvectorYou are here
- Ch 8Integrate PII defense with LiteLLM gateway
- Ch 8Deploy PII defense pipeline on GKE
- Ch 11Detect tool poisoning in MCP tool descriptions