Free lesson · GenAI Security Engineering

Deploy secure vector store on GKE with network isolation

Configure GKE network policies for vector store pods. Deploy pgvector with Helm and persistent volume claims.

Course: AI Security Engineering · Chapter 9 · Embedding & Vector Store Security

Free to read — no subscription required.

Introduction

When you run a pgvector-backed vector store as a pod on Google Kubernetes Engine, the default cluster networking model treats every pod as reachable from every other pod — a compromised web frontend or a misconfigured sidecar can open a raw TCP connection to port 5432 and read embeddings directly, bypassing every application-layer control you built. Kubernetes namespaces provide organizational separation but no network boundary; without an explicit NetworkPolicy, "namespace isolation" is a naming convention, not an enforced firewall. By the end of this lesson, you'll be able to deploy pgvector on GKE with Helm and a persistent volume claim, then lock its pod behind a default-deny NetworkPolicy that admits traffic only from the specific workloads authorized to query embeddings.

Key Terminology

  • NetworkPolicy: A namespaced Kubernetes resource that defines which pods may send traffic to (ingress) or receive traffic from (egress) a selected set of pods. It is enforced by the CNI plugin, not the application.
  • Default-deny: A NetworkPolicy that selects a pod but declares an empty ingress rule set, dropping all inbound connections until a subsequent policy explicitly allows a source.
  • Pod selector: The podSelector field that matches pods by label; it determines which pods a NetworkPolicy governs and which pods are permitted as traffic sources.
  • PersistentVolumeClaim (PVC): A request for durable storage that GKE satisfies by dynamically provisioning a Compute Engine persistent disk, keeping pgvector data intact across pod restarts and rescheduling.
  • StatefulSet: A workload controller that gives each pgvector replica a stable network identity and its own PVC, so storage follows the pod identity rather than the ephemeral pod instance.
  • Helm release: A named, versioned deployment of a Helm chart; the pgvector release renders the StatefulSet, Service, and PVC from a single values.yaml.
  • Dataplane V2: GKE's eBPF-based networking layer (Cilium) that enforces NetworkPolicy resources natively; on legacy clusters, network policy enforcement must be enabled explicitly.

Concepts

Now that you understand why namespace boundaries do not stop pod-to-pod traffic, consider how the two enforcement layers — storage identity and network reachability — combine to make the vector store both durable and unreachable except from authorized callers.

Why a Deployed Pod Is Reachable Until You Deny It

GKE's flat pod network assigns every pod a routable IP, and by default any pod may dial any other pod's IP and port. A pgvector StatefulSet exposes port 5432 through a headless Service, so the moment the pgvector release is applied, every workload in the cluster — including pods you did not write — can attempt a Postgres connection. Enforcement of a NetworkPolicy requires that the cluster run a policy-aware dataplane: on GKE Autopilot and Dataplane V2 clusters this is automatic, while legacy Standard clusters need --enable-network-policy set at creation. Without that dataplane, a NetworkPolicy object is accepted by the API server but silently ignored, giving a false sense of isolation.

The correct pattern is default-deny plus a narrow allow. The vector-store-isolation policy first selects pods labeled app: pgvector and declares ingress with no permitted sources, dropping all inbound traffic. A companion rule then admits only pods carrying the label role: embedding-client on TCP 5432. Any workload lacking that label — including one running in the same vector-store namespace — is refused at the network layer before Postgres authentication even begins.

Durable Storage as an Independent Concern

Network isolation controls who may connect; the PVC controls whether data survives. A pgvector pod that loses its disk on reschedule takes every embedding with it. The values.yaml binds a PersistentVolumeClaim to the StatefulSet so GKE provisions a persistent disk that reattaches to whichever node the pod lands on. These two concerns are deliberately separate: you can tighten the NetworkPolicy without touching storage, and you can resize the PVC without reopening the network boundary.

Loading diagram...

Code Walkthrough

Having established that a deployed pod is open until a default-deny policy closes it, the following manifests deploy pgvector with durable storage and then fence it behind an ingress allowlist. The first block is the Helm values.yaml that configures the pgvector release: it enables the vector extension image, requests a 20Gi PVC named by the StatefulSet, and labels the pod app: pgvector so the network policy can select it. The second block is the vector-store-isolation NetworkPolicy, which selects that same pod, denies all ingress by default, and then re-admits only pods labeled role: embedding-client on the Postgres port.

Code snippet yaml
1# values.yaml — applied via: 2# helm install pgvector bitnami/postgresql \ 3# -n vector-store -f values.yaml 4image: 5 repository: pgvector/pgvector 6 tag: pg16 7primary: 8 podLabels: 9 app: pgvector 10 persistence: 11 enabled: true 12 existingClaim: "" # chart provisions data-pgvector-0 13 size: 20Gi 14 storageClass: premium-rwo # GKE SSD-backed dynamic provisioning 15 initdb: 16 scripts: 17 enable_vector.sql: | 18 CREATE EXTENSION IF NOT EXISTS vector; 19auth: 20 existingSecret: pgvector-credentials # never inline passwords 21 database: embeddings 22service: 23 ports: 24 postgresql: 5432
  • Lines 5-6: pin the pgvector/pgvector:pg16 image so the vector type is available without a custom build.
  • Lines 8-9: stamp the pod with app: pgvector, the exact label the NetworkPolicy matches; a mismatch here silently leaves the pod ungoverned.
  • Lines 10-14: enable persistence and request a 20Gi disk from the premium-rwo StorageClass, which GKE fulfills as an SSD persistent disk bound to data-pgvector-0.
  • Lines 15-18: run enable_vector.sql at first init so the extension exists before any embedding is written.
  • Lines 19-21: source credentials from the pgvector-credentials Secret rather than plaintext values.
Code snippet yaml
1# network-policy.yaml — applied via: 2# kubectl apply -f network-policy.yaml -n vector-store 3apiVersion: networking.k8s.io/v1 4kind: NetworkPolicy 5metadata: 6 name: vector-store-isolation 7 namespace: vector-store 8spec: 9 podSelector: 10 matchLabels: 11 app: pgvector # governs the pgvector pod only 12 policyTypes: 13 - Ingress 14 ingress: 15 - from: 16 - podSelector: 17 matchLabels: 18 role: embedding-client # sole permitted source 19 ports: 20 - protocol: TCP 21 port: 5432
  • Lines 9-11: podSelector binds this policy to pods labeled app: pgvector; selecting a pod with any policyTypes: Ingress entry flips it to default-deny.
  • Lines 12-13: declaring Ingress means all inbound traffic is dropped unless an ingress rule re-admits it.
  • Lines 14-18: the single from clause admits only pods carrying role: embedding-client; unlabeled pods in the same namespace are refused.
  • Lines 19-21: the allowance is scoped to TCP 5432, so even an authorized client cannot reach other ports on the pod.

Verify by launching a throwaway pod without the role label — kubectl run probe --rm -it --image=postgres:16 -n vector-store -- psql -h pgvector -U app embeddings should hang and time out, while the same command from a pod labeled role: embedding-client connects immediately.

Do's and Don'ts

Having walked through the manifests above, the following Do's and Don'ts distill them into practice.

Do's

  1. Do confirm the dataplane enforces policy — On a legacy Standard cluster, apply the vector-store-isolation policy and then run the unlabeled-probe test; a connection that succeeds proves network policy enforcement is off. Recreate the cluster with Dataplane V2 or --enable-network-policy before trusting any NetworkPolicy.
  2. Do pin the PVC to a StatefulSet, not a Deployment — A StatefulSet gives the pgvector pod the stable identity data-pgvector-0 needs, so the premium-rwo disk reattaches after reschedule. A Deployment with a shared PVC risks two pods mounting one disk or losing the volume on rollout.
  3. Do label clients explicitly — The role: embedding-client label is the credential the NetworkPolicy checks. Add it to your query workloads' pod templates in code review, so a new service cannot reach pgvector merely by being deployed into the vector-store namespace.

Don'ts

  1. Don't rely on the namespace as a boundary — Placing pgvector in a vector-store namespace organizes resources but permits any in-cluster pod to dial port 5432. Only a NetworkPolicy with a default-deny Ingress rule actually blocks the connection.
  2. Don't inline database credentials in values.yaml — Set auth.existingSecret: pgvector-credentials and manage the password out of band. A credential committed alongside the Helm values leaks to anyone with read access to the chart, defeating the network fence entirely.
  3. Don't leave egress unrestricted when the threat model needs it — The vector-store-isolation policy in this lesson governs Ingress only. If a compromised pgvector pod must be prevented from exfiltrating embeddings, add a matching policyTypes: Egress block; ingress-only policies say nothing about what the pod may send outbound.

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 · Already a subscriber? Sign in →

More free lessons in AI Security Engineering

All free lessons in GenAI Security Engineering