Lesson 12 NetworkPolicies
Theme:

NetworkPolicies

Pod-level firewalling in Kubernetes, templated through Helm.

Why NetworkPolicies Exist

By default, every pod in a Kubernetes cluster can communicate with every other pod on any port, across any namespace. This flat network model makes development easy but is a security liability in production. A compromised pod can freely probe databases, caches, and internal APIs it should never touch.

NetworkPolicies solve this by acting as a declarative firewall attached to pods. They let you express rules like "only the frontend pods may reach the API on port 8080" or "nothing in this namespace is allowed to make outbound connections except to DNS." Unlike traditional firewalls that operate at the IP level, NetworkPolicies are label-aware: rules follow pods as they scale, restart, and get new IPs.

Key mental model

Without any NetworkPolicy objects in a namespace, all traffic is allowed. The moment you create a policy that selects a pod, that pod switches to a default-deny posture for the direction(s) the policy covers. You then add explicit allow rules.

Default-Deny Patterns

The first step toward a zero-trust network is to create a default-deny policy. These are minimal policies that select all pods (via an empty podSelector) and declare a policy type without providing any allow rules.

Deny All Ingress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress

Every pod in the production namespace now rejects all inbound traffic unless another NetworkPolicy explicitly allows it.

Deny All Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress

Deny Both Directions

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Warning

Denying all egress breaks DNS resolution. Pods will be unable to resolve service names. You almost always need a companion rule that allows UDP/TCP port 53 egress.

Ingress Rule Anatomy

An ingress rule describes who is allowed to send traffic into the selected pods and on which ports. Each rule has a from array (the source selectors) and an optional ports array.

There are three types of source selectors:

SelectorMatchesExample
podSelectorPods in the same namespace by labelapp: frontend
namespaceSelectorAll pods in namespaces matching labelsenv: production
ipBlockCIDR ranges, with optional exceptions10.0.0.0/8 except 10.0.1.0/24
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
        - protocol: TCP
          port: 8443

This policy applies to pods labeled app: api and allows inbound TCP traffic on ports 8080 and 8443, but only from pods labeled app: frontend in the same namespace.

Egress Rule Anatomy

Egress rules mirror ingress rules but control outbound traffic. The to array accepts the same three selector types: podSelector, namespaceSelector, and ipBlock.

spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    # Allow DNS resolution
    - to: []
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow outbound to the database
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    # Allow external HTTPS
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
      ports:
        - protocol: TCP
          port: 443

Note the DNS rule uses an empty to: [], which means "any destination" but is restricted to port 53 only. This is the standard pattern for allowing DNS while keeping egress locked down.

AND vs OR: The Selector Trap

This is the single most misunderstood aspect of NetworkPolicies. The difference between AND and OR comes down to whether two selectors are in the same list item or separate list items.

OR logic (two list items)

from:
  - podSelector:        # item 1
      matchLabels:
        app: frontend
  - namespaceSelector:  # item 2
      matchLabels:
        env: staging

This allows traffic from pods labeled app: frontend in any namespace, OR from any pod in namespaces labeled env: staging. These are two independent conditions.

AND logic (one list item)

from:
  - podSelector:        # same item
      matchLabels:
        app: frontend
    namespaceSelector:  # same item (no dash)
      matchLabels:
        env: staging

This allows traffic only from pods labeled app: frontend that are also in namespaces labeled env: staging. Both conditions must be true.

Tip

Always render your templates with helm template and carefully inspect the YAML dashes in the from/to arrays. A misplaced dash can silently widen your policy to allow unintended traffic.

Common Patterns

Allow only same-namespace traffic

ingress:
  - from:
      - podSelector: {}

An empty podSelector matches all pods, but only within the policy's own namespace (since there is no namespaceSelector).

Allow from a specific namespace

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: monitoring

The built-in label kubernetes.io/metadata.name matches a namespace by its actual name, which is more precise than custom labels.

Allow DNS egress (always include this)

egress:
  - to: []
    ports:
      - protocol: UDP
        port: 53
      - protocol: TCP
        port: 53

Templating NetworkPolicies in Helm

NetworkPolicies are excellent candidates for Helm templating because different environments have different security requirements. A development cluster might disable them entirely, while production enforces strict rules.

# templates/networkpolicy.yaml
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  podSelector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  policyTypes:
  {{- range .Values.networkPolicy.policyTypes }}
    - {{ . }}
  {{- end }}
  {{- with .Values.networkPolicy.ingress }}
  ingress:
    {{- toYaml . | nindent 4 }}
  {{- end }}
  {{- with .Values.networkPolicy.egress }}
  egress:
    {{- toYaml . | nindent 4 }}
  {{- end }}
{{- end }}
# values.yaml
networkPolicy:
  enabled: false
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to: []
      ports:
        - protocol: UDP
          port: 53
# values-production.yaml (override)
networkPolicy:
  enabled: true

Best practice

Use {{- with }} instead of {{- if }} for the ingress/egress blocks. This avoids rendering an empty ingress: key when no rules are defined, which would produce an invalid policy.

CNI Requirements

NetworkPolicy is a Kubernetes API object, but the actual enforcement depends on your cluster's CNI (Container Network Interface) plugin. The API server will always accept the resource; a missing CNI implementation simply means the rules are silently ignored.

CNI PluginNetworkPolicy SupportNotes
CalicoFullMost widely deployed; also supports its own CRDs for extended policies
CiliumFull + L7Supports HTTP-aware policies (CiliumNetworkPolicy CRD)
Weave NetFullStraightforward setup
FlannelNonePolicies are accepted but never enforced
AWS VPC CNIRequires addonInstall Calico as a policy-only addon alongside VPC CNI
Azure CNIFull (with Azure NPM)Enable network policy engine during cluster creation

Critical

If you deploy NetworkPolicies on a cluster running Flannel, nothing is enforced. Your manifests will exist in the API but provide zero security. Always verify your CNI before relying on NetworkPolicies.

Testing and Debugging

NetworkPolicies are notoriously hard to debug because there are no logs by default -- connections are simply dropped. Here is a systematic approach:

1. Verify policies are applied

# List all policies in the namespace
kubectl get networkpolicy -n myns

# Inspect a specific policy
kubectl describe networkpolicy allow-frontend -n myns

2. Test connectivity from inside the cluster

# Exec into an existing pod
kubectl exec -it deploy/frontend -n myns -- \
  curl -s --max-time 3 http://api:8080/health

# Spin up a temporary test pod
kubectl run nettest --rm -it \
  --image=busybox:1.36 \
  --restart=Never -n myns -- \
  wget -qO- --timeout=3 http://api:8080/health

3. Check from a different namespace

# Test cross-namespace connectivity
kubectl run nettest --rm -it \
  --image=busybox:1.36 \
  --restart=Never -n other-ns -- \
  wget -qO- --timeout=3 http://api.myns.svc:8080/health

4. Common debugging checklist

Calico tip

Enable flow logs with calicoctl to see which packets are being allowed or denied. Cilium provides similar observability via hubble.