Certified Kubernetes Application Developer (CKAD) #19 Ingress and NetworkPolicy
In #18 we used a Service to give a group of Pods a stable entry point. But a Service alone falls short in two ways. One is L7 routing that splits a single external entry point across hosts and paths to several services, and the other is controlling Pod-to-Pod communication inside the cluster at the level of who is allowed to talk to whom. The former is Ingress, the latter NetworkPolicy.
In this post we will cover Ingress, which routes incoming external traffic by host and path, and NetworkPolicy, which locks Pod-to-Pod communication down to a whitelist, from a hands-on exam perspective. Both belong to the CKAD Services and Networking domain, and the key is getting the manifest fields into your fingers. If the networking fundamentals feel fuzzy, K8s Intermediate #3 is a good warm-up first.
Ingress: L7 routing from a single entry point #
The LoadBalancer Service type grabs one external IP per service. As services multiply, external IPs multiply with them, and so does the cost. Ingress takes HTTP/HTTPS requests at a single entry point and splits them by host name and URL path before forwarding them to internal Services. Unlike a Service, which operates at L4, Ingress routes at L7 by reading the Host and path from the HTTP header.
Host- and path-based routing #
The core field of an Ingress is rules. Each rule has a host (optional) and an http.paths array, and each path specifies a path string, a pathType, and a backend to receive the traffic. The backend points to a target Service’s name and port.
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80A request arriving at shop.example.com/api goes to api-svc, and every other path goes to web-svc. Within a single host, traffic is split across different services by path.
pathType: Prefix and Exact #
pathType decides how the path is matched. It is a required field that is easy to leave blank on the CKAD exam.
| pathType | Matching behavior |
|---|---|
Prefix | Prefix matching by URL path element. /api matches /api and /api/v1 |
Exact | Matches only when the path is exactly equal. Case-sensitive |
ImplementationSpecific | Delegated to the controller implementation |
In practice you almost always use Prefix. Omitting pathType makes the manifest get rejected or behave unexpectedly, so you must fill it in for every path.
IngressClass and defaultBackend #
A cluster may have several kinds of Ingress Controller, so you specify which controller handles this Ingress with spec.ingressClassName. The kubernetes.io/ingress.class annotation was used in the past, but the ingressClassName field is the standard now.
spec:
ingressClassName: nginxYou can set a default backend for requests that match no rule with spec.defaultBackend. If you don’t specify one, unmatched requests fall through to the controller’s default 404.
TLS: HTTPS termination via a secret reference #
Ingress terminates HTTPS with spec.tls. The certificate and key go in a Secret of type kubernetes.io/tls, and the Ingress references that Secret by name.
spec:
tls:
- hosts:
- shop.example.com
secretName: shop-tlsHTTPS requests arriving at the host listed in hosts get the certificate from the shop-tls Secret applied. You create the Secret itself ahead of time with kubectl create secret tls shop-tls --cert=tls.crt --key=tls.key.
An Ingress Controller is required for it to work #
An Ingress is just a spec describing rules; on its own it does nothing. What actually receives traffic and routes it is an Ingress Controller such as ingress-nginx or Traefik. The controller watches Ingress objects and configures a reverse proxy according to their rules. So if no Ingress Controller is installed in the cluster, no amount of Ingress objects will bring external traffic in. The CKAD environment usually has a controller pre-installed, so first check the available class names with kubectl get ingressclass and put that name into ingressClassName.
NetworkPolicy: lock Pod-to-Pod communication with a whitelist #
A default Kubernetes cluster lets all Pods communicate freely with each other. The default is all-allow, where any Pod can connect to any other Pod’s IP. NetworkPolicy is the resource that imposes firewall rules on this open communication.
The core behavior fits in one sentence. A Pod that no NetworkPolicy selects stays all-allow, but a Pod that any NetworkPolicy selects allows only the traffic spelled out in that policy. In other words, the moment a policy attaches, that Pod switches to whitelist mode.
podSelector and policyTypes #
The target of a NetworkPolicy is chosen with spec.podSelector. Pods in the same namespace that match this selector are the policy’s targets.
spec.policyTypes decides whether this policy controls incoming traffic (Ingress), outgoing traffic (Egress), or both.
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
- EgressIf Ingress is in policyTypes, incoming connections are allowed only from the sources spelled out in the spec.ingress rules, and any unlisted source is blocked. Egress controls outgoing connections the same way.
ingress.from and egress.to #
You express the allowed sources and destinations with three selectors.
| Selector | Meaning |
|---|---|
podSelector | Pods chosen by label within the same namespace |
namespaceSelector | All namespaces chosen by label |
ipBlock | An IP range specified by CIDR (cidr) and exceptions (except) |
Listing these selectors under ingress.from allows only the traffic coming from those sources. Here lies a trap where one space of YAML indentation changes the meaning. Putting podSelector and namespaceSelector together inside a single from item requires both to be satisfied (AND), while splitting them into separate - items requires only one of the two to be satisfied (OR).
# AND: only Pods with the given label inside the given namespace
ingress:
- from:
- namespaceSelector:
matchLabels:
team: backend
podSelector:
matchLabels:
app: web
# OR: the entire given namespace, or Pods with the given label in the same ns
ingress:
- from:
- namespaceSelector:
matchLabels:
team: backend
- podSelector:
matchLabels:
app: webPort restriction #
You can add ports to ingress and egress rules to narrow the allowed ports. You pick the source with from or to, then restrict the ports that source may reach with ports.
ingress:
- from:
- podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 5432This allows only TCP 5432 connections from app: web Pods and blocks the rest.
default deny pattern #
The most frequently tested pattern is default deny, which applies a baseline block to an entire namespace. Setting podSelector to an empty value ({}) makes every Pod in the namespace a target, and emptying the ingress rules blocks all incoming traffic.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: prod
spec:
podSelector: {}
policyTypes:
- IngressSince policyTypes has only Ingress and there is no spec.ingress, all incoming connections to every Pod in the prod namespace are blocked. The standard operational approach is to attach individual allow policies on top of this to widen the whitelist. To block outgoing traffic as well, add Egress to policyTypes and leave egress empty.
The CNI must support NetworkPolicy #
Like Ingress, NetworkPolicy is just a spec; what actually enforces it is the cluster’s CNI plugin. CNIs such as Calico and Cilium support NetworkPolicy, but some simpler network plugins ignore the policy. That is, even after you create a NetworkPolicy, communication may stay wide open if the CNI doesn’t support it, so in the exam environment you must verify that the policy actually blocks after applying it.
Hands-on YAML examples #
Ingress: host + path + TLS #
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop-ingress
namespace: shop
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.example.com
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80It terminates HTTPS requests arriving at shop.example.com with the shop-tls certificate, routes /api to api-svc:8080, and routes the rest to web-svc:80.
NetworkPolicy: allow ingress only from a specific label + default deny #
First lay down a default deny that blocks the whole namespace, then put a policy on top that opens only DB access coming from app: web Pods.
# 1) baseline block
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: shop
spec:
podSelector: {}
policyTypes:
- Ingress
---
# 2) allow only 5432 from web to db
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-db
namespace: shop
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 5432The app: db Pods are selected by both policies, but since NetworkPolicy operates as a union, the net result is that only TCP 5432 from app: web Pods is allowed and every other ingress is blocked.
Debugging: verify the policy actually blocks #
A NetworkPolicy produces no immediately visible result after you apply it, so you have to attempt a connection yourself to check whether it blocks. Spinning up a temporary Pod and connecting to the target Service or Pod is the fastest way.
# before applying the policy: a successful connection is correct
k run probe --image=busybox --rm -it --restart=Never -- \
wget -qO- --timeout=3 db-svc:5432
# after applying the policy: a disallowed source is blocked with a timeout
k run probe --image=busybox --rm -it --restart=Never -- \
wget -qO- --timeout=3 db-svc:5432Sending from a Pod that has the allowed label gets through, and sending from one that doesn’t times out. When a policy doesn’t behave as intended, here is what to check.
- Confirm the policy is in the right namespace with
kubectl get networkpolicy -n <ns> - Confirm
podSelectorpicks the intended Pod withkubectl describe networkpolicy <name> - Confirm the labels on the target Pod and the source Pod match the selectors exactly
- Confirm the indentation of the
fromitems didn’t flip AND/OR - If communication isn’t blocked even after all that, confirm the CNI supports NetworkPolicy
When the Ingress side doesn’t work, look at whether the rules and backend Services are wired correctly with kubectl describe ingress <name>, and confirm the specified class actually exists with kubectl get ingressclass. A typo in the backend Service name or port is the most common culprit.
Exam points #
- Ingress is L7 routing. With
rules.hostandhttp.pathsit splits traffic across backend Services by host and path. pathTypeis required. It’sPrefixmost of the time; useExactonly when you need an exact match.- Specify the controller with
ingressClassName. Check the name first withkubectl get ingressclass. - TLS references a
kubernetes.io/tlsSecret viaspec.tls.secretName. Create the Secret withkubectl create secret tls. - Ingress works only with an Ingress Controller. The spec alone makes no traffic flow.
- A Pod selected by a NetworkPolicy switches to whitelist mode. Without a policy, all-allow is the default.
- Build default deny with
podSelector: {}+policyTypes. Emptyingress/egressto block everything. - Selectors inside one
fromitem are AND, items split by-are OR. One space of indentation changes the meaning. - NetworkPolicy is enforced only if the CNI supports it. Verify it actually blocks after applying.
Wrap-up #
What this post locked in:
- Ingress. host/path routing,
pathType(Prefix/Exact),rules/backend(service+port),ingressClassName,defaultBackend, the TLS Secret reference, and the Ingress Controller dependency - NetworkPolicy. the all-allow default, the whitelist switch for a Pod that a policy attaches to,
podSelector/policyTypes, the three selectors ofingress.from/egress.to, port restriction - The default deny pattern and the operational approach of stacking allow policies on top of it, the CNI dependency, and the debugging procedure
Starting from Service, we finished external entry with Ingress and internal communication control with NetworkPolicy. If you want to firm up the selector syntax of these two resources once more, reviewing how labels and selectors behave in K8s Intermediate #7 is a good idea.
Next: exam tips and time management #
The per-domain knowledge wrapped up with #18 and #19. What’s left now is the operational skill of turning that knowledge into a score within two hours.
#20 Exam Tips and Time Management, the Patterns People Miss will gather per-task time allocation, the solving order that maximizes partial credit, the speed strategy that leans on kubectl explain and dry-run, and the patterns candidates miss over and over, like forgetting to switch context or omitting pathType.