Your production cluster uses Cilium as its CNI (and as its Gateway API implementation). Cilium enforces both standard Kubernetes NetworkPolicy and its own richer CiliumNetworkPolicy, which adds Layer-7 (HTTP), DNS-based egress, and named “entities”.

# How isolation works (important)

Kubernetes networking is allow-by-default until a pod is selected. A pod is unselected (all traffic allowed) until at least one policy’s podSelector matches it. The moment a policy selects it, that pod becomes isolated for that direction (ingress and/or egress), and from then on only what your policies explicitly allow is permitted.

Two consequences worth internalising:

  • Adding your first ingress policy to a pod doesn’t just “add a rule”; it flips that pod to deny-all-ingress-except-what-you-list.
  • ingress and egress are independent. Selecting a pod for ingress does nothing to its egress, and vice versa.

If you want a hard “deny everything unless allowed” baseline for a namespace, apply the default-deny policy below, then layer allow-rules on top.

# Recipe 1: allow traffic from another namespace

Let pods in namespace-b receive traffic from pods in namespace-a:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-namespace-a
  namespace: namespace-b
spec:
  podSelector: {}                       # all pods in namespace-b
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: namespace-a

kubernetes.io/metadata.name is a label Kubernetes sets automatically on every namespace, so you can select namespaces by name without labelling them yourself.

# Recipe 2: allow only a specific app to talk to another

Allow only pods labelled app: web (in frontend) to reach pods labelled app: api on port 8080:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-web
  namespace: api
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: frontend
          podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080

# Establishing a default-deny baseline

Apply this to a namespace to deny all ingress and egress for every pod, then add explicit allows. (You almost always want to also allow DNS; see the next recipe.)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: my-namespace
spec:
  podSelector: {}                       # selects every pod
  policyTypes: [Ingress, Egress]        # ...for both directions, with no allow rules => deny

# Recipe 3: allow DNS egress (almost always needed)

A default-deny-egress namespace can’t resolve DNS, which breaks nearly everything. Allow it:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: my-namespace
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

# Exposing a service to the internet

External traffic does not arrive via NetworkPolicy; it comes through the Gateway API (implemented by Cilium). On your production cluster you create your own Gateway, then a service is only reachable from the internet if it has an HTTPRoute attached to that Gateway:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  namespace: my-namespace
spec:
  parentRefs:
    - name: app-gateway          # the Gateway you created
      namespace: my-namespace
  hostnames:
    - "my-app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: my-app
          port: 80

See Exposing Apps (Gateway API) for the full setup — creating the Gateway, TLS, listener ports, and DNS.

Without an HTTPRoute, a Service is reachable only inside the cluster. If you’ve enabled default-deny-ingress, also add an ingress allow for traffic coming from the gateway so the proxy can reach your pods.

# Cilium-specific power features

When plain NetworkPolicy isn’t enough, use CiliumNetworkPolicy.

# Restrict egress to specific external hostnames (DNS-aware)

Lock a workload down so it can only reach, say, the Stripe API:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-egress-allowlist
  namespace: api
spec:
  endpointSelector:
    matchLabels:
      app: api
  egress:
    # DNS must be allowed, and Cilium watches these lookups to enforce toFQDNs
    - toEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY
          rules:
            dns:
              - matchPattern: "*"
    - toFQDNs:
        - matchName: "api.stripe.com"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP

# Allow egress to the internet but nothing internal

Cilium “entities” let you reference logical groups like world (everything outside the cluster) or cluster:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: web-egress-world-only
  namespace: frontend
spec:
  endpointSelector:
    matchLabels:
      app: web
  egress:
    - toEntities:
        - world

# Layer-7 (HTTP) rules

Cilium can allow only specific HTTP methods/paths, e.g. let a client GET from the API but not POST:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-readonly-from-frontend
  namespace: api
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/v1/.*"

# Verifying your policies

# List policies in a namespace
kubectl get networkpolicy,ciliumnetworkpolicy -n <namespace>

# Inspect what a policy matches
kubectl describe networkpolicy <name> -n <namespace>

To observe live allow/deny decisions, ask your TrueFullstaq engineer about Hubble, the Cilium observability layer, which can show flow-by-flow whether traffic was forwarded or dropped, which is invaluable when a policy is stricter than you intended.

Tip: roll out policies incrementally. Start permissive, confirm your app works, then add default-deny and re-test, so a dropped flow is easy to attribute to the policy you just added.