Policy Design Starting From Default Deny
Goal
You flip a namespace's communication default to deny, and write a set of policies that selects only the necessary paths by label and opens them.
Why it matters
What people get wrong most often in NetworkPolicy is not the syntax but the direction and the way selectors combine. A policy attaches not to the sending side but to the receiving side. To allow communication from web to api, the policy's podSelector must be api, and web appears inside ingress.from. Also, items of the from array are ORed, while the namespaceSelector and podSelector within one item are ANDed. With a single hyphen's position, "a specific Pod in a specific namespace" turns into "all Pods of that namespace, or that Pod anywhere." And when you block egress, be sure to add the DNS exception as a pair. Cases are reported again and again in which, in real production, only a default deny was put in first, name resolution died, and all services stopped in a chain. The cluster in this lab has no real packets flowing between Pods, so grading is based on the contents of the policy objects and the selector semantics. You set the syntax and the mindset here, and you can do the real blocking check in an environment with a CNI.
Steps
- Create the namespace
netpol-laband apply/opt/lab/fixtures/k8s/workloads.yamlin it. As a result, the three Podsweb,api, anddbmust each have anapplabel equal to its own name, and theapiPod must additionally have the labeltier=backend. - In
netpol-lab, create the NetworkPolicydefault-deny-ingress.podSelectoris the empty object{},policyTypesis onlyIngress, and there must be noingressrules at all. - Create the NetworkPolicy
allow-web-to-api.podSelector.matchLabels.appisapi, andingress[0].from[0]has onlypodSelector.matchLabels.app: web(do not put anamespaceSelectorin the same item). The allowed port is TCP8080. - Create the NetworkPolicy
default-deny-egress.policyTypesis onlyEgress. Then createallow-dns-egress, withegress[0].to[0].namespaceSelector.matchLabelsset tokubernetes.io/metadata.name: kube-system, and inegress[0].ports, put port53as two items, UDP and TCP. - Create the namespace
monitoringand attach the labelpurpose=monitoring. Then, innetpol-lab, create the NetworkPolicyallow-from-monitoring.podSelector.matchLabels.appisapi,ingress[0].from[0].namespaceSelector.matchLabels.purposeismonitoring, and the allowed port is9090. - Create the NetworkPolicy
allow-office-cidr.podSelector.matchLabels.appisweb,ingress[0].from[0].ipBlock.cidris10.0.0.0/16, andexceptis the single entry10.0.5.0/24. - Create the NetworkPolicy
api-full.podSelector.matchLabels.appisapi, andpolicyTypesis bothIngressandEgress.ingress[0].frommust contain 2 or more items (for example, the web Pod and the monitoring namespace), andegress[0].to[0]ispodSelector.matchLabels.app: db, with allowed port5432. - Create
/root/ops/netpol/out/policy-map.json. The top-level keys areweb,api, anddb, and each value is an array of the names of the policies that select that Pod. A policy with an empty selector must go in all three Pods, andallow-web-to-apimust go only inapi(it must not go inwebordb).allow-office-cidrgoes inweb, andapi-fullgoes inapi. In the end,netpol-labmust have 6 or more NetworkPolicies.
Reference
- You bring up the fixture with
kubectl apply -f /opt/lab/fixtures/k8s/workloads.yaml -n netpol-lab. Do not edit the fixture file itself. kubernetes.io/metadata.nameis a label Kubernetes automatically attaches to every namespace, so you can select a namespace without separate labeling. Conversely, a label you define yourself, likepurpose=monitoring, must actually be attached to the namespace.- To check the policy contents,
kubectl get netpol <이름> -n netpol-lab -o yaml(where the placeholder is the policy name) is fastest. Filter out typos in advance withkubectl apply --dry-run=client. - Common mistake 1: setting the
podSelectorto web in step 3. A policy attaches to the receiving side. - Common mistake 2: opening DNS only for UDP in step 4. When a response is large, it moves to TCP 53.
- Common mistake 3: putting two selectors inside one
fromitem in step 7. That becomes AND, not OR, and is a much narrower rule. - The lab Pod comes up fresh for each lab, so the cluster state created in the earlier lab does not remain. Create the namespaces and Pods yourself within this lab. This is exactly why operational procedures must be left in runbooks and manifests rather than in memory.
Create the lab namespace and labeled Pods
Create the namespace netpol-lab and apply /opt/lab/fixtures/k8s/workloads.yaml in it. As a result, the three Pods web, api, and db must each have an app label equal to its own name, and the api Pod must additionally have the label tier=backend.
A policy selects targets only by labels. The fixture already has the three Pods ready, so bring them up as they are, but check which namespace you bring them up in.
Create the default ingress deny policy
In netpol-lab, create the NetworkPolicy default-deny-ingress. podSelector is the empty object {}, policyTypes is only Ingress, and there must be no ingress rules at all.
A selector that selects the whole namespace is an empty selector. Denying everything means writing no allow rules at all.
Open only the port from web to api
Create the NetworkPolicy allow-web-to-api. podSelector.matchLabels.app is api, and ingress[0].from[0] has only podSelector.matchLabels.app: web (do not put a namespaceSelector in the same item). The allowed port is TCP 8080.
A policy attaches not to the sending side but to the receiving side. Since it is within the same namespace, do not mix a namespace selector in when choosing the other party.
Default egress deny and a DNS exception
Create the NetworkPolicy default-deny-egress. policyTypes is only Egress. Then create allow-dns-egress, with egress[0].to[0].namespaceSelector.matchLabels set to kubernetes.io/metadata.name: kube-system, and in egress[0].ports, put port 53 as two items, UDP and TCP.
If you block egress, name resolution dies first. Check that DNS does not end with UDP alone, and what the standard label is for selecting the target namespace.
Allow traffic coming from another namespace
Create the namespace monitoring and attach the label purpose=monitoring. Then, in netpol-lab, create the NetworkPolicy allow-from-monitoring. podSelector.matchLabels.app is api, ingress[0].from[0].namespaceSelector.matchLabels.purpose is monitoring, and the allowed port is 9090.
To select a namespace, the label must actually be attached to that namespace. If there is nothing for the selector to select, the policy means nothing.
Allow an IP range and specify an exception
Create the NetworkPolicy allow-office-cidr. podSelector.matchLabels.app is web, ingress[0].from[0].ipBlock.cidr is 10.0.0.0/16, and except is the single entry 10.0.5.0/24.
You use ipBlock by opening broadly and carving narrowly out. The excluded range must be contained within the allowed range.
Put ingress and egress in one policy
Create the NetworkPolicy api-full. podSelector.matchLabels.app is api, and policyTypes is both Ingress and Egress. ingress[0].from must contain 2 or more items (for example, the web Pod and the monitoring namespace), and egress[0].to[0] is podSelector.matchLabels.app: db, with allowed port 5432.
Items of the from array are ORed. To allow two kinds of sources, split them into two items. On the outgoing side, only the database port is open.
Build a map of the policies that apply to each Pod
Create /root/ops/netpol/out/policy-map.json. The top-level keys are web, api, and db, and each value is an array of the names of the policies that select that Pod. A policy with an empty selector must go in all three Pods, and allow-web-to-api must go only in api (it must not go in web or db). allow-office-cidr goes in web, and api-full goes in api. In the end, netpol-lab must have 6 or more NetworkPolicies.
A policy with an empty selector applies to all Pods. Reflect in the map that what a policy selects is the receiving side.