Skip to main content

Segment with network policies

Inspect 1.36

On a fresh cluster any pod can reach any other pod, in any namespace. The host firewall protects the nodes from outside traffic, but it does not isolate your pods from each other. You close that gap with NetworkPolicy objects, and you own that layer.

Start with a namespace-isolation baseline: one policy per namespace blocks cross-namespace traffic and rarely breaks anything. Then move to full default-deny for the namespaces that hold sensitive workloads, allowing each connection those workloads need. Do the isolation baseline first, everywhere; use full default-deny where it matters.

The two network layers#

A Syself Autopilot cluster runs two separate network layers, with different owners.

The host firewall protects each node's own services. It runs default-deny, keyed to cluster identity rather than address, and the platform manages it as a set of CiliumClusterwideNetworkPolicy objects. Do not edit these: the platform restores them. The open paths, the identity types, and how it works are covered in .

Workload policy is the set of NetworkPolicy and CiliumNetworkPolicy objects you create. They select pod endpoints and control pod-to-pod traffic. This layer is entirely yours, and it starts empty.

Warning

The host firewall is default-deny for node traffic, but pod-to-pod traffic is default-allow until a policy selects a pod. The platform does not put a default-deny between your services, because only you know which of them should talk. On a fresh cluster, any pod reaches any other pod across any namespace.

How Cilium enforces policy#

Cilium enforces network policy on identities derived from pod labels, not on IP addresses. A policy that says "frontend may reach backend" keeps working as pods restart and get new IPs. There is no IP list to maintain.

The identity model also means no per-pod cloud infrastructure. Policy is not attached to a network interface, an IP address, or any object the cloud provider has to create and count. It runs in eBPF (a Linux kernel feature for fast, programmable network control) on each node, so the pods you can put under policy are bounded by the node, not by a provider quota.

Policy types#

Kind Scope Use it for
NetworkPolicy Namespaced, L3/L4 (IP and port) Standard segmentation, portable across clusters
CiliumNetworkPolicy Namespaced, adds deny rules and (once the gate lifts) domain-name egress Deny rules, and egress to named hosts
CiliumClusterwideNetworkPolicy Cluster-wide A baseline that applies across all namespaces

Use NetworkPolicy for ordinary segmentation. Use CiliumNetworkPolicy when you need a deny rule. For outbound control, including the FQDN limitation and the egress rules that work today, see .

What is restricted on this platform#

Application-layer (L7) and DNS rules are rejected the moment you apply them. kubectl apply fails with a readable error instead of the policy shipping and silently breaking the traffic it is meant to permit. Which rules are rejected, in which kind, and whether the rejection is permanent or temporary is in ; the syself-restrict-l7-proxy-policies admission policy that does the rejecting is covered in .

Everything else works today, and that is most of what you write. Rules on workload identity, IP ranges, ports, and protocols (Layer 3 and Layer 4) are not affected. That includes a rule that opens port 53 to the cluster's DNS service, which is what the default-deny baseline below needs: opening a port is a Layer 4 rule and carries no rules.dns block.

For request-level HTTP control (allow only GET /api), use a service mesh; Istio is supported, see .

How deny and allow rules interact#

Cilium has no priority or ordering field on policies. Allow rules are additive: adding one only allows more traffic. The only way to narrow traffic a previous rule allows is a deny rule, and deny always beats allow.

To narrow access, add an ingressDeny or egressDeny rule in your own policy object. Do not edit platform policies to remove an allow. Add your own deny on top.

Start here: isolate a namespace#

One policy per namespace blocks all cross-namespace traffic while everything inside the namespace keeps working. This is the right first move on a shared cluster. It removes the biggest exposure, a compromised pod reaching every other namespace, without a long allow-list.

yaml
		apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: isolate-namespace
  namespace: team-a
spec:
  podSelector: {} # selects every pod in team-a
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: {} # allows pods in team-a only
	

Two rules make this work. Once any policy selects a pod for ingress, all ingress no policy allows is denied. And a bare podSelector inside from matches pods in the policy's own namespace only. So pods in team-a still reach each other, and pods in every other namespace are dropped.

The policy controls ingress only. Pods in team-a can still open any outbound connection, and DNS keeps working. Egress control comes with the full baseline below and in .

Allow the cross-namespace traffic a service needs#

After isolating the namespace, allow each legitimate cross-namespace path explicitly. The most common one is an ingress controller routing outside traffic to your pods (see ). With ingress-nginx installed in the ingress-nginx namespace, allow that namespace to reach the pods behind your Ingress objects:

yaml
		apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-controller
  namespace: team-a
spec:
  podSelector:
    matchLabels: {app: frontend} # the pods your Ingress routes to
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: {kubernetes.io/metadata.name: ingress-nginx}
      ports:
        - {protocol: TCP, port: 8080}
	

kubernetes.io/metadata.name is a label the API server puts on every namespace, so you can select a namespace by name without labeling it yourself. Allow rules are additive, so this combines with isolate-namespace: the frontend pods accept traffic from their own namespace and from ingress-nginx, and every other pod in team-a stays namespace-only.

Repeat the pattern for each cross-namespace consumer, for example a Prometheus in a monitoring namespace scraping your metrics port.

Move to full default-deny#

Full default-deny also controls traffic inside the namespace and all outbound traffic. Use it for namespaces that hold sensitive workloads, or wherever you want an explicit allow-list of every connection. Apply the steps to one namespace at a time and verify after each.

Step 1: deny all traffic#

Apply a policy that selects all pods in the namespace and denies both incoming and outgoing traffic:

yaml
		apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: team-a
spec:
  podSelector: {} # all pods in the namespace
  policyTypes: [Ingress, Egress]
	

After this, pods in team-a cannot reach anything, including DNS. Step 2 fixes that.

Step 2: allow DNS#

DNS translates service names like backend into IP addresses. Almost every workload needs it.

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

This policy opens a port and nothing more. It carries no rules.dns block, so it does not put the pod's lookups through Cilium's DNS proxy, and none of the platform restrictions apply. Allowing DNS this way works today. Forget it, and every outbound connection fails at the name lookup.

Step 3: allow the connections each workload needs#

Open specific pod-to-pod paths by label. For example, to let frontend pods reach backend pods on port 8080:

yaml
		apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-backend
  namespace: team-a
spec:
  podSelector:
    matchLabels: {app: backend}
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels: {app: frontend}
      ports:
        - {protocol: TCP, port: 8080}
	

Because Cilium tracks pods by label, this policy stays correct as pods restart and receive new IP addresses.

Default-deny also drops traffic from an ingress controller. If an Ingress routes to pods in this namespace, keep the allow-ingress-controller policy from the isolation section.

Step 4: control outbound traffic#

A default-deny policy already blocks all egress except what you allow (Step 2's DNS allow, and any pod-to-pod path you opened). To let a workload reach something outside the cluster, name it by address and port. Naming a destination by hostname with toFQDNs is rejected at apply time today, so you use toCIDR instead. The egress rules that work today, the default-deny egress baseline, and the FQDN limitation in full are in .

Step 5: verify#

		# The allowed path answers. Expect your backend's normal response:
$ kubectl -n team-a exec deploy/frontend -- curl -m 3 http://backend:8080
 
# The same request from a pod without the frontend label is dropped.
# Expect a timeout: curl: (28) Connection timed out after 3001 milliseconds
$ kubectl -n team-a run probe --rm -i --restart=Never --image=curlimages/curl -- curl -m 3 http://backend:8080
 
# Watch the drops as they happen:
$ hubble observe --namespace team-a --verdict DROPPED
	

Denied traffic times out instead of being refused, because Cilium drops the packets without answering. A quick refusal means you reached something that is not listening, not a policy drop.

On a 1.36 cluster the Hubble relay flow API is TLS-only, so bare hubble observe fails with a TLS handshake error until you connect the CLI to the relay first. See .

What to know#

Do not edit the platform's CiliumClusterwideNetworkPolicy host rules: the platform restores them. Write your own namespaced policies for workload isolation.

Both the host firewall and your workload policies run in Cilium, so Hubble sees every drop. Watch the hubble_drop_total metric to catch a workload hitting a policy it should not.

Pair a default-deny NetworkPolicy with a Pod Security Standard level (see ) and a ResourceQuota or LimitRange. Together they address the main risks in a shared namespace, which is also the pattern behind .