Alright, let’s get our hands dirty with the actual spec of a NetworkPolicy. This is where the rubber meets the road. Think of a NetworkPolicy as a very specific, very powerful bouncer for your Pod’s network traffic. It doesn’t just let anything in or out; it checks the guest list. The spec is how you write that list.

The core blueprint looks like this:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-detail-oriented-bouncer
spec:
  podSelector: {} # Which Pods does this bouncer guard?
  policyTypes: # What kind of rules are we defining?
  - Ingress
  - Egress
  ingress: [] # The fine-print rules for incoming traffic
  egress: [] # The fine-print rules for outgoing traffic

Let’s break down each part of this bouncer’s contract.

The podSelector: Who Are We Protecting?

This is the single most important field. It determines which Pods this policy applies to. It uses the same label selection mechanism you use for Services or Deployments. The key thing to remember, the thing that trips up everyone at least once, is this: If a Pod doesn’t have the labels matching the podSelector, the policy does absolutely nothing to it. It’s invisible to the bouncer.

An empty selector podSelector: {} is a wildcard; it selects every Pod in the namespace. Use this with extreme caution. It’s the equivalent of hiring a bouncer for the entire city block instead of just your nightclub.

A more realistic example selects Pods with a specific label:

podSelector:
  matchLabels:
    app: api-server
    tier: backend

This bouncer now only cares about Pods that are both app=api-server and tier=backend. All other Pods in the namespace can send traffic to these Pods freely unless another policy stops them. Isolation is not default; it’s opt-in.

policyTypes: What Are We Even Controlling?

This field tells Kubernetes which types of rules you’re about to define in the ingress and egress sections. It feels a bit redundant, but there’s a reason for it. You can have an ingress array defined but if Ingress isn’t in policyTypes, it’s ignored. Same for egress.

The default behavior is:

  • If you have ingress rules defined but omit policyTypes, Kubernetes defaults to policyTypes: [Ingress].
  • If you have egress rules defined but omit policyTypes, Kubernetes defaults to policyTypes: [Ingress, Egress]. Yes, it adds Ingress by default even if you didn’t define any rules. It’s a weird quirk. Just always set policyTypes explicitly. It saves confusion.

So, if you want to define both ingress and egress rules, you state it clearly:

policyTypes:
  - Ingress
  - Egress

If you only want to control egress (outbound traffic) from your Pods, you’d write:

policyTypes:
  - Egress
# ingress: [] is omitted entirely

ingress and egress: The Actual Rules

These arrays contain the allow rules. This is a critical shift in mindset: NetworkPolicies are whitelists. Everything is denied unless you explicitly allow it.

Each rule in ingress or egress can allow traffic based on a combination of three things:

  1. Who (source for ingress, destination for egress): Defined by from/to fields which can select Pods (via podSelector) and/or IP Blocks (via ipBlock).
  2. What Port: Defined by the ports field.

A simple ingress rule that allows traffic from all Pods in the current namespace (but not from other namespaces!) would look like this:

ingress:
  - from:
      - podSelector: {} # An empty selector, meaning "all pods in this namespace"

But you’re usually more precise. Let’s say your api-server Pods need to accept traffic only from Pods with the label app=frontend and only on port 8443.

ingress:
  - ports:
      - protocol: TCP
        port: 8443
    from:
      - podSelector:
          matchLabels:
            app: frontend

For egress, the logic flips. You’re dictating where your Pod can talk to. A classic best practice is to restrict egress to only what’s necessary. For example, your database Pods should probably only talk to other database Pods for replication, not random websites on the internet.

egress:
  - to:
      - podSelector:
          matchLabels:
            app: mysql
    ports:
      - protocol: TCP
        port: 3306
  - ports:
      - protocol: UDP
        port: 53 # You almost always need to allow DNS.

See that? We allowed DNS (UDP port 53) to anywhere because we didn’t specify a to block for that rule. The rules are additive. This Pod can now talk to: 1) Pods labeled app=mysql on port 3306, and 2) Any IP on port 53 for DNS. That’s it. Everything else is blocked.

The most common pitfall here is forgetting a crucial rule, like DNS or health checks, and then wondering why your “secure” Pod can’t resolve a hostname or why it’s getting killed by the readiness probe. Start restrictive and add rules until it works, like tightening screws on a machine, not like throwing pasta at a wall.