Cilium NetworkPolicy: default-deny and DNS-aware rules

Published: 2026-03-27

k3s with Cilium means standard Kubernetes NetworkPolicy works, but CiliumNetworkPolicy gives more: L7 HTTP rules, DNS-based egress, and cluster-wide policies via CiliumClusterwideNetworkPolicy.


Default deny

Start with a namespace-wide deny-all ingress policy. Without it, every pod can reach every other pod across namespaces.

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

Apply to every application namespace. podSelector: {} matches all pods. Only ingress is denied — egress is allowed by default (pods can still reach Kubernetes DNS, external services).

For full isolation, add egress deny too:

yamlspec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Then add explicit egress rules for DNS and other needed services.


Allow ingress from APISIX

After default-deny, services need explicit rules allowing traffic from the ingress controller:

yamlapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-apisix-ingress
  namespace: dev
spec:
  podSelector:
    matchLabels:
      expose: "true"
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-apisix

Services that should be reachable from outside get the expose: "true" label. Others remain isolated.


Allow inter-service communication

Allow a frontend to talk to the backend within the same namespace:

yamlapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: dev
spec:
  podSelector:
    matchLabels:
      app: backend
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - port: 8080

CiliumNetworkPolicy for DNS-based egress

Standard NetworkPolicy can only select by IP. CiliumNetworkPolicy supports FQDN rules:

yamlapiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-external-apis
  namespace: dev
spec:
  endpointSelector:
    matchLabels:
      app: backend
  egress:
    - toFQDNs:
        - matchName: "api.some-external-service.com"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP
    - toEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": kube-system
            "k8s:k8s-app": kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: UDP
          rules:
            dns:
              - matchPattern: "*"

The DNS rule must explicitly allow port 53 to kube-dns — otherwise FQDN matching can't resolve names.


L7 HTTP rules

Restrict which paths a service can receive:

yamlapiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-http-policy
  namespace: dev
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: GET
                path: /api/v1/.*
              - method: POST
                path: /api/v1/orders

L7 rules require Cilium's HTTP proxy (Envoy). At L7, Cilium can log matched requests to Hubble UI.


Cluster-wide policy for Prometheus

Instead of adding a rule to every namespace, use CiliumClusterwideNetworkPolicy:

yamlapiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: allow-prometheus-scrape
spec:
  endpointSelector: {}
  ingress:
    - fromEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": observability
            app.kubernetes.io/name: prometheus
      toPorts:
        - ports:
            - port: "9090"
              protocol: TCP
            - port: "9100"
              protocol: TCP
            - port: "8080"
              protocol: TCP

One policy for Prometheus scraping across all namespaces.


Verifying with Hubble

bash# Live dropped flows
hubble observe --namespace dev --verdict DROPPED -f

# Policy verdicts for a specific pod
hubble observe --pod dev/api-6d8b4c5f9-xk4qv -f

# All flows to a specific port
hubble observe --namespace dev --port 5432 -f

Dropped flows show which policy blocks traffic immediately. Much faster than guessing.


Debugging checklist

  1. hubble observe --verdict DROPPED — see what's being blocked
  2. Check namespace labels — namespaceSelector uses namespace labels
  3. Check pod labels — wrong label means the policy won't match
  4. DNS egress must include port 53 to kube-dns before any FQDN rules
  5. L7 rules require Cilium's Envoy proxy to be enabled