Kubernetes NetworkPolicy, Ingress, and Gateway API for CKA
Introduction
Kubernetes NetworkPolicy controls allowed Pod connections, while Ingress and the Gateway API publish application traffic. Together they cover the remaining Services & Networking competencies (20% of the CKA). This lesson uses original exercises to isolate database access, route two Services behind one hostname and configure a weighted HTTPRoute. These examples practise published skills rather than reproduce exam questions.
The official exam page currently lists Kubernetes v1.37. Confirm the version for your sitting and the installed Gateway API bundle and controller capabilities before adapting manifests. Review Pod connectivity, Services, and CoreDNS first if Service ports or namespace-local DNS are unfamiliar.
After this lesson you will be able to write NetworkPolicies from a security requirement, predict which traffic they allow, publish HTTP services with Ingress, and do the same with the Gateway API resources (GatewayClass, Gateway, HTTPRoute).
Learning objectives
- Explain the default "allow all" behaviour of Pods and how a NetworkPolicy changes it.
- Write ingress and egress policies using
podSelector,namespaceSelector,ipBlockandports, and avoid the AND/OR selector mistake. - Explain the roles of an Ingress controller, an IngressClass and an Ingress resource.
- Create Ingress rules with host and path routing,
pathTypeand TLS. - Describe the Gateway API role model and create a Gateway and an HTTPRoute with weighted backends.
Network Policies
Without applicable NetworkPolicies, Pods are non-isolated by this API; application listeners, routing, firewalls and other controls can still block connections. A NetworkPolicy is a namespaced object that selects Pods and states which traffic is allowed to or from them. Three facts drive every exercise:
- Policies are additive and allow-only. There is no "deny" rule. For ordinary Pod connections, a Pod selected for ingress or egress is isolated in that direction; traffic must match an allow rule from at least one applicable policy. The API has exceptions, including traffic involving the Pod's own node, and hostNetwork behaviour depends on the network implementation.
- A Pod not selected by a policy is non-isolated by NetworkPolicy in that direction. Both the source's egress and destination's ingress must permit a connection; allowing only one side is insufficient.
- The CNI plugin must enforce policies. The API server accepts a NetworkPolicy even if the CNI ignores it (for example, plain Flannel). Calico and Cilium are examples of implementations with policy support; inspect the installed implementation and test enforcement rather than infer it from a resource existing. On a cluster without enforcement, the object exists but nothing changes.
The main fields of spec:
| Field | Meaning |
|---|---|
podSelector |
Pods the policy applies to. {} selects all Pods in the namespace. |
policyTypes |
Ingress, Egress or both. If omitted, Ingress is always assumed, and Egress only if the policy has egress rules. |
ingress[].from |
Allowed sources (peers). |
egress[].to |
Allowed destinations. |
ports |
Allowed destination ports and protocols. |
A peer is one of podSelector, namespaceSelector (alone or combined with a podSelector) or ipBlock (a CIDR with optional except).
Default deny
The foundation of most designs is a policy that selects all Pods and allows nothing:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: shop
spec:
podSelector: {}
policyTypes: ["Ingress"]Because ingress is absent, this policy adds no ingress allowances. Other applicable policies can still allow traffic, and node-related exceptions remain. Adding Egress with no egress rules also isolates outbound connections. Add narrowly scoped allowances and test new connections after policy propagation; this is not a complete host firewall or a TLS security policy.
Worked example: protect a database
Scenario. In namespace shop, Pods app=db listen on TCP 5432. Only Pods app=api in the same namespace may connect. All Pods in namespaces labelled team=sre (including monitoring) may reach port 9187 (the metrics exporter).
Reasoning.
- Select the protected Pods with
podSelector: matchLabels: app=db. - List two ingress rules, because there are two independent sources, each with its own port.
- For the monitoring rule, select the namespace by label. Use the automatic
kubernetes.io/metadata.namelabel to target a namespace by name, or confirm that the customteam=srelabel exists withkubectl get namespaces --show-labels. This example intentionally allows all Pods in matching namespaces.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow
namespace: shop
spec:
podSelector:
matchLabels:
app: db
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432
- from:
- namespaceSelector:
matchLabels:
team: sre
ports:
- protocol: TCP
port: 9187Verify. Assume an enforcing network implementation, a db Service with ready backends, working DNS, no broader allow policies and source egress permission. From an API Pod containing netcat, kubectl exec -n shop deploy/api -- nc -zvw 3 db 5432 should connect. A newly initiated connection from an ordinary app=other Pod should fail; a dropped packet may time out, but the exact error is implementation-dependent. Also test the metrics port from an allowed namespace. kubectl describe networkpolicy db-allow -n shop helps confirm the selectors, but is not proof of enforcement.
Why not the alternatives? A single from list with both peers and both ports would allow the monitoring namespace on 5432 and the API on 9187, which is broader than requested. Using only a namespaceSelector for the API would open the database to every Pod in shop.
The AND versus OR trap
Compare these two fragments:
# OR: Pods labelled app=api in THIS namespace, or any Pod in namespaces labelled team=sre
- from:
- podSelector: {matchLabels: {app: api}}
- namespaceSelector: {matchLabels: {team: sre}}# AND: Pods labelled app=api that are in namespaces labelled team=sre
- from:
- namespaceSelector: {matchLabels: {team: sre}}
podSelector: {matchLabels: {app: api}}The difference is one dash. Separate list items are ORed; selectors inside the same item are ANDed. A podSelector alone only matches Pods in the policy's own namespace.
Egress, DNS and ipBlock
Isolating egress can block DNS unless the actual DNS destination is allowed. The following spec fragment assumes ordinary CoreDNS Pods in kube-system labelled k8s-app=kube-dns. Place it under a NetworkPolicy spec with an appropriate podSelector and policyTypes: [Egress]:
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}
- to:
- ipBlock:
cidr: 10.20.0.0/16
except: ["10.20.5.0/24"]
ports:
- {protocol: TCP, port: 443}ipBlock is intended for external address ranges; use selectors for ordinary in-cluster Pod peers. NAT can happen before or after policy processing depending on the dataplane and Service type, so verify which source/destination IP the implementation evaluates. NodeLocal DNSCache and hostNetwork DNS need rules appropriate to the actual resolver path rather than blindly copying CoreDNS selectors. Return traffic for allowed connections is implicitly allowed; treatment of established connections when policy changes is implementation-dependent.
Ingress
A NodePort or LoadBalancer Service can expose one or more transport ports. Ingress adds HTTP/HTTPS routing: one entry point, many hosts and paths. Two pieces are needed:
- An Ingress controller, a Deployment or DaemonSet that watches Ingress objects and configures a proxy (for example a supported HAProxy, Traefik or cloud controller). Kubernetes does not ship one.
- Ingress resources that declare the rules.
An IngressClass names a controller; an Ingress selects it with spec.ingressClassName. If exactly one IngressClass carries the annotation ingressclass.kubernetes.io/is-default-class: "true", Ingresses without a class use it.
Recent change. The community ingress-nginx project announced that maintenance ended in March 2026. Existing installations continue to run, but no new fixes are published, and projects are migrating to the Gateway API or to other controllers. The Ingress API itself is stable and still part of the CKA curriculum, which is why you must know both models.
Writing Kubernetes Ingress rules
Assume the exercise provides a working supported controller and an IngressClass named lab-ingress. Class names are installation-specific; the name does not install a controller. Backend Services, TLS certificate/key files and the shop namespace must already exist.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
spec:
ingressClassName: lab-ingress
tls:
- hosts: ["shop.example.com"]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
name: httpKey points:
pathTypeis mandatory:Exact(the path must match exactly),Prefix(matches on path elements:/apimatches/apiand/api/v1but not/apiary) orImplementationSpecific(controller-defined).- The longest matching path wins; if matches are equally long, Exact takes precedence over Prefix. This does not automatically rewrite the path sent upstream.
- A backend refers to a Service in the same namespace as the Ingress, by port number or port name.
- The TLS Secret (
kubernetes.io/tls) must be in the Ingress namespace and containtls.crtandtls.key. - Imperative creation:
kubectl create ingress shop -n shop --class=lab-ingress --rule="shop.example.com/api*=api:80". The trailing*means aPrefixtype. Combine with--dry-run=client -o yamlfor more options. - A
defaultBackendhandles requests that match no rule.
Worked example: two Services behind one host
Scenario. Namespace shop has Services web (port 80) and api (port 8080). A supported controller and IngressClass lab-ingress are already configured. Publish https://shop.example.com/ to web and /api to api.
Steps.
- Confirm the class:
kubectl get ingressclass. - Create the TLS Secret:
kubectl create secret tls shop-tls --cert=tls.crt --key=tls.key -n shop. - Apply the Ingress above (set the
apiport to 8080). - Inspect
kubectl describe ingress shop -n shop, controller events and the configured entry point. An empty ADDRESS alone does not prove the controller is absent; some installations do not publish status addresses. - For an IPv4 entry point, test host routing and TLS with
curl --cacert lab-ca.crt --resolve shop.example.com:443:<address> https://shop.example.com/api. Supply the exercise's trusted CA file (or use normal system trust for a publicly trusted certificate), and ensure the application serves/api.
Why the trailing / rule last? It is shorter, so it matches only what /api did not. Even if you listed it first, the controller would still prefer the longest match, but ordering by specificity helps readers.
The Gateway API
The Gateway API is the successor to Ingress. It is a set of CRDs maintained by SIG Network, installed separately from the core API (gateway.networking.k8s.io). Its main benefits are a role-oriented design, support for more protocols (HTTP, gRPC, TLS, TCP), and expressive routing (header matches, weights, redirects) in API fields. Availability of each route type or feature depends on the installed bundle version, release channel and implementation; CRDs alone do not create a working gateway. The lesson on Helm, Kustomize, CRDs, and operators explains the difference between installing API definitions and their controllers.
| Resource | Owned by | Purpose |
|---|---|---|
| GatewayClass | Infrastructure provider | Names a controller implementation, similar to a StorageClass or IngressClass. |
| Gateway | Cluster operator | An instance of a class: listeners (port, protocol, hostname, TLS) and which namespaces may attach routes. |
| HTTPRoute (also GRPCRoute, TLSRoute) | Application developer | Rules that match requests and send them to backend Services. |
A route attaches to a Gateway through parentRefs. The Gateway controls which routes it accepts through allowedRoutes on each listener (Same, All or a selector of namespaces). This separation lets operators own the load balancer while teams own their own routes.
Gateway API: Gateway and HTTPRoute
Assume a controller accepts GatewayClass example-class, the infra and shop namespaces exist, backend Services expose the listed Service ports, and a valid TLS Secret shop-tls exists in infra (the Gateway namespace). Label the route namespace with kubectl label namespace shop gateway-access=true --overwrite. This is a different Secret from the Ingress example's shop/shop-tls.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
spec:
gatewayClassName: example-class
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: shop.example.com
tls:
mode: Terminate
certificateRefs:
- name: shop-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
namespace: shop
spec:
parentRefs:
- name: public
namespace: infra
sectionName: https
hostnames: ["shop.example.com"]
rules:
- matches:
- path: {type: PathPrefix, value: /api}
backendRefs:
- {name: api, port: 8080, weight: 90}
- {name: api-canary, port: 8080, weight: 10}
- matches:
- path: {type: PathPrefix, value: /}
backendRefs:
- {name: web, port: 80}Inspect the resources separately: kubectl get gatewayclass, kubectl get gateway -n infra, and kubectl describe httproute shop -n shop. Check Gateway/listener conditions and the route's parent conditions, including Accepted, ResolvedRefs and the reported observedGeneration. A route can be Accepted while a backend reference is unresolved. Check provisioning/programming status and test real requests; status acceptance alone does not prove application health.
Cross-namespace route-to-Gateway attachment uses parentRefs and the listener's allowedRoutes, not a ReferenceGrant. A route-to-Service reference in another namespace instead needs a ReferenceGrant in that Service's namespace; cross-namespace certificate references similarly require permission in the Secret's namespace.
Worked example: canary 20%
Scenario. The checkout Service (checkout-v1, port 80) serves /checkout. Send 20% of requests to checkout-v2.
Reasoning.
- Weights are relative: 80 and 20 configure an 80:20 split (4 and 1 express the same ratio). A small test sample need not contain exactly 20% canary requests. Confirm backend validity and health, and measure enough requests to assess the distribution.
- Both backends must be in the route's namespace unless a
ReferenceGrantpermits cross-namespace references. - Update the intended rule on the existing HTTPRoute. Creating another route with overlapping matches can invoke route precedence instead of combining the two rules into a weighted split.
rules:
- matches:
- path: {type: PathPrefix, value: /checkout}
backendRefs:
- {name: checkout-v1, port: 80, weight: 80}
- {name: checkout-v2, port: 80, weight: 20}Why not two separate Ingresses or Services? Standard Ingress has no portable traffic-splitting field; it relies on annotations that depend on the controller. The Gateway API makes weights part of the standard API.
Comparison: Ingress versus Gateway API
| Aspect | Ingress | Gateway API |
|---|---|---|
| Resources | Ingress, IngressClass | GatewayClass, Gateway, HTTPRoute and others |
| Protocols | HTTP and HTTPS | HTTP, HTTPS, gRPC, TLS, TCP, UDP (by route type) |
| Traffic splitting, header match | Controller annotations | Standard fields |
| Role separation | Single object | Operator (Gateway) and developer (route) |
| Installed with | Core Kubernetes API | CRDs plus a controller |
Comparison: ways to expose an application
| Need | Best choice |
|---|---|
| Internal clients only | ClusterIP |
| A single TCP port for a lab | NodePort |
| External entry point for a Service using protocols supported by the provider | LoadBalancer |
| Many HTTP hosts and paths on one IP | Ingress or Gateway API |
| Weighted routing, header-based routing | Gateway API |
Common pitfalls and exam tips
- A NetworkPolicy that does nothing. Check that the CNI enforces policies and that the policy is in the Pod's namespace. Run
kubectl get pods --show-labelsand compare with the selector. - Locked-out DNS. Allow the resolver path actually used by the Pods (including UDP and TCP DNS where required); account for NodeLocal DNSCache.
- Default deny also blocks the Ingress controller. Allow traffic from the controller's namespace to the Pods it serves.
- Dash errors. One misplaced
-converts AND into OR. Read the YAML twice. - Namespace labels.
namespaceSelectorneeds a label on the namespace. Usekubernetes.io/metadata.name: <ns>when no custom label exists. - Ingress requires a matching controller. Check IngressClass, the actual controller Deployment/DaemonSet, events and connectivity; names containing ingress and an ADDRESS field are clues, not conclusive tests.
pathTypemissing gives a validation error.- Cross-namespace backends are not allowed in Ingress, and in the Gateway API need a ReferenceGrant.
- Gateway API CRDs absent.
no matches for kind "Gateway"means the CRDs are not installed. - Exam tip: copy examples from
kubernetes.io/docs(search "network policy", "ingress", "gateway api") rather than typing from memory; then change only names, labels and ports. Test with a throwawaybusyboxorcurlimages/curlPod, and rememberkubectl explain networkpolicy.spec.ingress.
Mini case study: Contoso's three-tier lockdown
Contoso runs namespace shop with Pods app=web, app=api and app=db. Requirements from security:
- All ingress to
shopis denied unless explicitly allowed. webaccepts traffic from the ingress controller namespaceingress-systemon port 8080.apiaccepts traffic only fromwebon port 8080.dbaccepts traffic only fromapion port 5432.- Publish
webathttps://shop.example.com/.
Solution.
- Apply the
default-deny-ingresspolicy shown above inshop. - Policy
web-allow:podSelector: app=web; onefromwithnamespaceSelector: kubernetes.io/metadata.name=ingress-system; port 8080. - Policy
api-allow:podSelector: app=api;frompodSelector: app=web; port 8080. - Policy
db-allow:podSelector: app=db;frompodSelector: app=api; port 5432. - Create an Ingress with
ingressClassName, hostshop.example.com, path/and TLS, backendweb:8080.
Assume no broader ingress allowances and an enforcing dataplane. Test new connections at each hop from approved debug containers: web-to-db should fail while api-to-db should connect. Allow the actual ingress-controller source identity, including its Pod labels where appropriate; hostNetwork controllers can appear as node traffic and need implementation-specific analysis.
Commentary. The default-deny policy alone would break the application; each allow policy re-opens only one hop. The most common mistake would be forgetting item 2, which can produce upstream timeouts or controller-specific errors despite valid Ingress objects. Follow Service and network troubleshooting to separate policy, backend readiness and proxy failures.
Key terms
- NetworkPolicy: namespaced allow-list for Pod traffic, enforced by the CNI.
- Default deny: policy selecting all Pods with no allow rules.
- Ingress / IngressClass: HTTP routing rules and the controller they target.
- Ingress controller: the proxy that implements Ingress rules.
- Gateway API: successor API with GatewayClass, Gateway and HTTPRoute.
- parentRefs / allowedRoutes: how a route attaches to a Gateway and which routes a Gateway accepts.
- ReferenceGrant: permission for cross-namespace references.
Summary checklist
- NetworkPolicy isolation is per direction, additive and subject to node/hostNetwork exceptions; check both ends of a connection.
- The CNI must enforce NetworkPolicies.
- Separate list items are OR; selectors in one item are AND.
- Allow DNS (port 53) when restricting egress.
- An Ingress needs a controller, a class,
pathTypeand same-namespace backends. - Gateway API: GatewayClass (provider), Gateway (operator), HTTPRoute (developer).
- Weighted
backendRefsimplement canary releases. -
kubectl describeon the route showsAcceptedandResolvedRefs.
Official sources
- Network Policies: https://kubernetes.io/docs/concepts/services-networking/network-policies/ (opens in a new tab)
- Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/ (opens in a new tab)
- Ingress controllers: https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/ (opens in a new tab)
- Gateway API: https://kubernetes.io/docs/concepts/services-networking/gateway/ (opens in a new tab)
- Gateway API project: https://gateway-api.sigs.k8s.io/ (opens in a new tab)
- Ingress NGINX retirement: https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/ (opens in a new tab)
- HTTP traffic splitting: https://gateway-api.sigs.k8s.io/guides/user-guides/traffic-splitting/ (opens in a new tab)
- Gateway reference security: https://gateway-api.sigs.k8s.io/docs/concepts/security/ (opens in a new tab)
- Gateway status conditions: https://gateway-api.sigs.k8s.io/guides/implementers-guide/ (opens in a new tab)
- CKA domains and current version: https://training.linuxfoundation.org/certification/certified-kubernetes-administrator-cka/ (opens in a new tab)
- CKA curriculum: https://github.com/cncf/curriculum (opens in a new tab)
