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 -
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}'
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".