Network Policy

A network policy is a Kubernetes object that says which connections to and from a group of pods are allowed, at the IP address and port level โ€” “OSI layer 3 or 4,” in the documentation’s terms, “within your cluster, and also between Pods and the outside world.” Pods are selected by label, and the peers a policy can name are other pods, namespaces and IP ranges. The version-specific behavior discussed below is scoped to Kubernetes v1.37.

It exists because the default is wide open: “by default, all pods in a Kubernetes cluster are allowed to communicate with each other, and all network traffic is unencrypted.” Nothing about creating a namespace changes that.

The prerequisite that decides whether any of it is real

Before the semantics, the sentence that matters most in practice: “network policies are implemented by the network plugin,” and “creating a NetworkPolicy resource without a controller that implements it will have no effect.” The multi-tenancy guidance states the same thing as a warning โ€” without a CNI plugin that supports them, “NetworkPolicy resources will be ignored.”

The API server can accept a policy without a plugin enforcing it. The standard NetworkPolicy API does not expose connection accept/drop logging; plugin-specific state and logs may still provide evidence.

Test the deployed enforcement after relevant networking changes. A refused connection alone is inconclusive: there may be no listener, DNS may fail, routing may be broken, or another firewall may intervene. Use that observation with the controls and plugin evidence below.

A test worth trusting has four parts: a control connection that should succeed and does, so you know the path works at all; a target chosen so that something is genuinely listening on the other end; the attempt matched to the policy you are testing in direction, protocol, port, selector and namespace โ€” and, where it matters, from a Pod on a different node, since traffic from a Pod’s own node is always allowed; and a look at the plugin’s own state or logs, which is a different thing from the API having no standard event logging. Absent that, you have a belief.

Isolation is per-direction and conditional

The model is narrower than the word suggests. The documentation qualifies it immediately: “‘isolation’ here is not absolute, rather it means ‘some restrictions apply’.”

A pod starts non-isolated in both directions โ€” “by default, a pod is non-isolated for egress; all outbound connections are allowed” โ€” and becomes isolated for a direction only when a policy names it โ€” “a pod is isolated for egress if there is any NetworkPolicy that both selects the pod and has ‘Egress’ in its policyTypes,” and the same sentence holds for ingress with ‘Ingress’. Once isolated in a direction, only what some applicable policy allows in that direction gets through. Ingress and egress are declared independently, and both matter for a single connection.

Three consequences follow, and together they are most of what you need to debug a blocked or unexpectedly open connection.

  • Policies add up and never conflict. “Network policies do not conflict; they are additive.” Where several apply, “the connections allowed in that direction from that pod is the union of what the applicable policies allow. Thus, order of evaluation does not affect the policy result.” There is no precedence to learn โ€” and no way to carve an exception out of something already allowed. To take access away you must find the policy that granted it.
  • Both ends must agree. “For a connection from a source pod to a destination pod to be allowed, both the egress policy on the source pod and the ingress policy on the destination pod need to allow the connection. If either side does not allow the connection, it will not happen.” When two teams own the two namespaces, debugging requires both.
  • If policyTypes is omitted, Ingress is included; Egress is included when egress rules are present. An ingress-only policy does not change egress isolation or cancel another policyโ€™s egress restrictions.

The gap occurs when operators expect an ingress-only policy to restrict outbound traffic and no other selecting policy isolates egress. Evaluate all applicable policies in each direction.

Default-deny, and the DNS trap

Because there are no deny rules, a default-deny posture is built by selecting all pods in a namespace and allowing nothing, then adding allowances back. The multi-tenancy guidance recommends exactly that: start “with a default policy that denies communication between podsโ€ฆ with another rule that allows all pods to query the DNS server for name resolution.”

That second clause is not optional, and the NetworkPolicy page says so in plain terms: “a default deny-all egress policy also blocks DNS traffic. If your workloads need DNS resolution, you must add a separate NetworkPolicy that allows egress to your cluster’s DNS.” Skipping it produces an outage that looks nothing like a network problem โ€” name resolution fails, so every client reports a connection error to a host it cannot even resolve.

One related footgun from the same guidance: do not use an empty label selector for namespaceSelector, which quietly means every namespace rather than none.

Even a complete deny-all has documented holes, and they are the right kind of boring to memorize.

  • “A pod cannot block access to itself.”
  • “Traffic to and from the node where a Pod is running is always allowed, regardless of the IP address of the Pod or the node.”
  • A deny-all is “only guaranteed to deny TCP, UDP and SCTP connections. For other protocols, such as ARP or ICMP, the behavior is undefined.”
  • Behavior for pods on the host network is “undefined,” and the most common implementation “ignores hostNetwork pods when matching podSelector and namespaceSelector.”

IP-range rules carry their own uncertainty. They “should be cluster-external IPs, since Pod IPs are ephemeral and unpredictable,” and where the cluster rewrites source or destination addresses, “it is not defined whether this happens before or after NetworkPolicy processing, and the behavior may be different for different combinations of network plugin, cloud provider, Service implementation.” An egress rule expressed as a CIDR may or may not apply to traffic sent through a Service address, depending on your stack. Test it rather than reasoning about it.

Two timing behaviors to design around

Policies are not applied instantaneously, and the documentation is unusually direct about what that requires of your application.

At startup, a Pod created before the network plugin has handled a relevant policy may initially be unprotected. After the plugin has handled the policy, it must isolate newly affected Pods before their containers start. Allow rules can arrive later, so a new Pod may initially have no connectivity. Distributed enforcement can also make peers on different nodes become reachable at different times; the Kubernetes API does not report the exact enforcement moment.

Applications must tolerate these startup differences. An init container can wait for required destinations before the application containers start; exiting on the first failed connection can instead prevent startup. Policies apply at Pod level, including init, sidecar, and regular containers.

At the other end, revocation is not specified. When the policies affecting an established connection change, “it is implementation defined as to whether the change will take effect for that existing connection or not,” and the advice is “not to modify policies/pods/namespaces in ways that might affect existing connections.” A network policy is admission control for new connections, not a kill switch.

Deleting a Pod does not prove that its process has exited. Normal termination gives hooks and cleanup a grace period, while force deletion can remove the API object without confirmation from the node. A partitioned or unresponsive node may keep running the process. Native sidecar ordering also differs from regular-container ordering; neither replaces confirmation of actual termination.

Be precise about what termination does achieve, because the opposite over-claim is just as misleading. If the process really does exit, its open sockets go with it โ€” the kernel closes the file descriptors a terminating process held, and when the last reference to a socket is gone that process is no longer using the connection. Termination is a real containment mechanism; the uncertainty is whether it happened. Two cases differ. Where the kubelet is reachable and the container exits, on SIGTERM or on the later KILL, the connections that process owned are finished with it. Where the node is partitioned, wedged or has lost contact with the control plane, nothing ran there โ€” the API object disappeared and the container may still be serving traffic, which is exactly the situation the force-deletion caution describes. A Pod owned by a controller will also be replaced, so the workload returns under a new name.

One further distinction matters when reading a connection list during an incident. A closed socket on one end is not immediately known at the other: a peer holding a connection whose process has exited may not notice until it writes and the reset arrives, or until a keepalive or timeout expires. So “the process is gone” and “the remote side has stopped waiting” are different observations, made in different places, and a dashboard showing an established connection can be describing a peer’s stale view rather than live traffic.

So containment is three separate things, and an incident response that conflates them will report a connection cut that is still open: the delete request, the process actually terminating on the node, and the network path being blocked. Only the second and third actually stop traffic, and the second is verified on the node rather than in the API. Cutting off a compromised workload means stopping the controller from recreating it, confirming on the node that the process is gone, and โ€” for anything still running, or for the case where you cannot confirm โ€” using a control your environment actually enforces at the packet level โ€” which you should have verified in advance rather than during the incident.

What it is not, and what sits next to it

As of the v1.37 documentation, the API cannot express an explicit deny, target Services by name, apply to nodes by their Kubernetes identity, define defaults across all namespaces, or log anything. Nor does it touch TLS or force traffic through a gateway. Those absences are why a real segmentation design usually combines layers.

MechanismControlsBlind to
RBACWho may read or change API objectsPackets between pods
Network policyWhich pod-to-pod and external connections may be established, at layer 3 and 4Identity beyond labels, request content, and its own enforcement status
Service meshWorkload authentication and traffic authorization on paths where the mesh is deployed and enforces policyKubernetes API object permissions, bypass or non-mesh paths, host isolation and resource contention; verify implementation-specific coverage

Standard NetworkPolicy allowances combine across selecting objects. Keeping them reviewed as policy as code helps inspect that union, while tests and plugin evidence establish what is enforced. Network segmentation is one part of tenant isolation; API access uses authorization. See There Is No Deny Rule for their combined operating context.

References


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.