Network Policies¶
Pod-to-pod network isolation enforced by Kubernetes NetworkPolicy resources.
Current posture¶
The tomoda app defines three NetworkPolicies in k8s/apps/tomoda/base/network-policy.yaml:
| Policy | Target | Allowed ingress |
|---|---|---|
tomoda-api-policy |
Pods with app: tomoda-api |
TCP 8080 from namespace traefik-system only |
tomoda-async-policy |
Pods with app: tomoda-async |
None (default-deny) — async pods only initiate connections (Redis, Postgres). The kubelet's /health probe is not subject to NetworkPolicy. |
frontend-policy |
Pods with app: frontend |
TCP 8081 from namespace traefik-system only |
# Excerpt
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tomoda-api-policy
spec:
podSelector:
matchLabels:
app: tomoda-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik-system
ports:
- protocol: TCP
port: 8080
Effect: no pod outside traefik-system can reach the api or frontend
pods, and no pod can reach the async pods at all. Only the Traefik
ingress controller can talk to api/frontend. If someone breaches another
workload in the cluster, they cannot pivot directly into the backend pools.
Each CNPG cluster also has an ingress policy alongside its manifests in
k8s/envs/{dev,prod}/postgres/manifests/network-policy.yaml:
| Policy | Target | Allowed ingress |
|---|---|---|
postgres-dev-policy |
Pods cnpg.io/cluster: postgres-dev |
TCP 5432 from tomoda-dev + data; all ports from cnpg-system; TCP 9187 from monitoring |
postgres-prod-policy |
Pods cnpg.io/cluster: postgres-prod |
TCP 5432 from tomoda-prod + data; all ports from cnpg-system; TCP 9187 from monitoring |
Effect: only the app namespace, this namespace's own admin tooling (pgAdmin,
backups), the CNPG operator, and the Prometheus scrape can reach Postgres. A
breach in any other namespace cannot even open a socket to :5432.
What is not restricted¶
Egress¶
No egress NetworkPolicy is defined. Pods can:
- Reach external APIs (Stripe, Google, Apple, LINE, OpenAI) directly
- Connect to Postgres in the
datanamespace - Connect to Redis in the
datanamespace - Resolve DNS via kube-dns
- Reach the GCP metadata server (which is how Workload Identity works)
If zero-trust egress becomes a requirement, you would add policies that whitelist specific FQDNs (via FQDN NetworkPolicy controllers like Cilium) or CIDR blocks.
Frontend service port¶
The frontend NetworkPolicy specifies port 8081 — confirm this matches the Service targetPort and the container containerPort before relying on the policy. A mismatched port silently allows nothing.
Adding a NetworkPolicy¶
Add the manifest to the relevant app's directory, commit, push. Argo CD syncs.
The CNPG database policies (above) live in each cluster's manifests/
directory. A minimal ingress policy looks like:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-prod-policy
namespace: data
spec:
podSelector:
matchLabels:
cnpg.io/cluster: postgres-prod
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: tomoda-prod
ports:
- protocol: TCP
port: 5432
Do not forget the operator
A policy that selects the CNPG pods with only an app-namespace rule will
lock out the CloudNativePG operator (cnpg-system) and break
reconciliation. Always allow cnpg-system and the monitoring scrape, as
the shipped postgres-{dev,prod}-policy do.
Test additions in dev first
A misconfigured NetworkPolicy can take an app down hard — the default-deny posture kicks in as soon as any policy selects a pod for the given policyType. Apply to dev, smoke-test, then promote.
Verifying enforcement¶
# Inside a debug pod in a namespace that should be BLOCKED
kubectl run shell --rm -it --image=busybox -n default -- /bin/sh
# Inside:
wget -T 3 -O- http://backend-service.tomoda.svc.cluster.local:8080/health
# Expected: "wget: download timed out"
# From inside Traefik's namespace (should succeed)
kubectl run shell --rm -it --image=busybox -n traefik-system -- /bin/sh
wget -T 3 -O- http://backend-service.tomoda.svc.cluster.local:8080/health
# Expected: 200 OK
If both succeed, the GKE cluster might not have NetworkPolicy enforcement enabled (Calico or Cilium). Check:
gcloud container clusters describe gke-tomoda \
--zone us-central1-a --project development-485000 \
--format="value(networkPolicy.enabled)"
Should print True. If False, enforcement is not happening — fix in Terraform and reapply.