Skip to content

Network Policy

Purpose

Restrict which pods can open a connection to the frontend/backend pods, on top of whatever the Service/Ingress routing already implies (routing is a convenience, not a security boundary on its own).

Architecture/context

helm/akshayabazaar/templates/networkpolicy.yaml defines up to four NetworkPolicy resources per release:

Policy podSelector Allows ingress from
<release>-default-deny-ingress this release's own pods only (app.kubernetes.io/name + instance) nothing by default - baseline deny
<release>-frontend-allow-ingress frontend pods Traefik, via a namespaceSelector matching kube-system
<release>-backend-allow-ingress backend pods frontend pods only (same-namespace podSelector), plus Prometheus (monitoring.namespace) when monitoring.enabled: true

Egress is deliberately left unrestricted for both workloads - the backend talks to an external MySQL host, R2, and several third-party HTTPS APIs (Razorpay, SendGrid, Meta WhatsApp Cloud API, Msg91, FCM) with no stable IP/CIDR a NetworkPolicy could pin to without a DNS-aware CNI (Cilium, etc.), which this cluster doesn't run. Restricting ingress is where a static policy is reliable; egress restriction is a documented future option, not attempted here.

The one hard-won lesson: podSelector: {} is not "this release's pods"

An empty podSelector: {} on default-deny-ingress matches every pod in the namespace, not just the ones this chart created - this was a real, live-diagnosed bug: a namespace-local mysql-local/redis-local pod (used only in the local k3d topology, entirely outside this Helm release) got silently cut off from all ingress the moment the empty-selector policy was created, because Kubernetes NetworkPolicy has no concept of "pods this specific Helm release owns" - only whatever labels you actually match on. The fix, already applied in this chart, scopes default-deny-ingress's podSelector to app.kubernetes.io/name + app.kubernetes.io/instance - this release's own pods, nothing else in the namespace.

Commands

# List every NetworkPolicy in the namespace
kubectl get networkpolicy -n staging

# Inspect one policy's actual selector (the check that would have caught the bug above)
kubectl get networkpolicy -n staging akshaya-local-default-deny-ingress -o jsonpath='{.spec.podSelector}'

# Full detail
kubectl describe networkpolicy -n staging akshaya-local-backend-allow-ingress

Testing connectivity the correct way

An ad-hoc/unlabeled debug pod will be denied by backend-allow-ingress even when the backend itself is perfectly healthy - it doesn't carry the exact app.kubernetes.io/name/instance/component: frontend labels the policy requires. Always test from the real frontend pod instead (this is exactly what verify-staging.ps1 does):

kubectl exec -n staging deploy/akshaya-local-frontend -- wget -qO- http://akshaya-local-backend:8080/health/live

The Helm hook-ordering deadlock (know this before debugging a migration failure)

NetworkPolicy is a normal (non-hook) chart resource - Helm only syncs it after a pre-upgrade hook (the migration Job) succeeds. If the migration Job's own failure is caused by a NetworkPolicy problem, a plain helm upgrade retry can never fix itself, because the corrected policy never reaches the cluster until the hook it's blocking succeeds. Break the deadlock once by applying just the corrected policy directly:

helm template akshaya-local helm/akshayabazaar -f helm/akshayabazaar/values-local-staging.yaml `
  --show-only templates/networkpolicy.yaml | kubectl apply -f -
Then retry the normal helm upgrade. This only touches the NetworkPolicy objects - no Deployment, Secret, ConfigMap, or data is affected.

Single-VM topology: how staging and production stay isolated on one node

podSelector/ingress.from.podSelector (used throughout this file, with no namespaceSelector) only ever match pods in the same namespace as the NetworkPolicy object itself - this is Kubernetes' own semantics, not something this chart opts into. Every release creates its own copy of these resources scoped to its own namespace, so staging's (akshayabazaar-staging) and production's (akshayabazaar-production) policies are automatically independent even while sharing one physical K3s node.

Validation / expected result

kubectl get networkpolicy -n staging akshaya-local-default-deny-ingress -o jsonpath='{.spec.podSelector}'
Expect the release's own app.kubernetes.io/name/instance labels, never {}.

Troubleshooting

See Kubernetes → Troubleshooting and Operations → Verification "Common failure scenarios".

Rollback / recovery

Same as any other chart template - helm rollback. If you've applied a NetworkPolicy directly (the deadlock-break above) ahead of a normal release sync, the next successful helm upgrade simply re-applies the same object as a no-op.

Security considerations

  • This is the actual security boundary preventing a staging pod from ever reaching a production pod on the shared K3s node - not merely namespace separation, which alone doesn't stop cross-namespace traffic without a NetworkPolicy.
  • Verify the Traefik assumption (kube-system) before applying this chart to any cluster that isn't K3s's own default - see Ingress "Security considerations".