Skip to content

Kubernetes NetworkPolicy, Ingress, and Gateway API

Step 15 of 24

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, ipBlock and ports, 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, pathType and 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:

  1. 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.
  2. 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.
  3. 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:

YAML
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.

  1. Select the protected Pods with podSelector: matchLabels: app=db.
  2. List two ingress rules, because there are two independent sources, each with its own port.
  3. For the monitoring rule, select the namespace by label. Use the automatic kubernetes.io/metadata.name label to target a namespace by name, or confirm that the custom team=sre label exists with kubectl get namespaces --show-labels. This example intentionally allows all Pods in matching namespaces.
YAML
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: 9187

Verify. 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:

YAML
# 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}}
YAML
# 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]:

YAML
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.

YAML
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: http

Key points:

  • pathType is mandatory: Exact (the path must match exactly), Prefix (matches on path elements: /api matches /api and /api/v1 but not /apiary) or ImplementationSpecific (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 contain tls.crt and tls.key.
  • Imperative creation: kubectl create ingress shop -n shop --class=lab-ingress --rule="shop.example.com/api*=api:80". The trailing * means a Prefix type. Combine with --dry-run=client -o yaml for more options.
  • A defaultBackend handles 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.

  1. Confirm the class: kubectl get ingressclass.
  2. Create the TLS Secret: kubectl create secret tls shop-tls --cert=tls.crt --key=tls.key -n shop.
  3. Apply the Ingress above (set the api port to 8080).
  4. 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.
  5. 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.

YAML
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.

  1. 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.
  2. Both backends must be in the route's namespace unless a ReferenceGrant permits cross-namespace references.
  3. 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.
YAML
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-labels and 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. namespaceSelector needs a label on the namespace. Use kubernetes.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.
  • pathType missing 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 throwaway busybox or curlimages/curl Pod, and remember kubectl 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:

  1. All ingress to shop is denied unless explicitly allowed.
  2. web accepts traffic from the ingress controller namespace ingress-system on port 8080.
  3. api accepts traffic only from web on port 8080.
  4. db accepts traffic only from api on port 5432.
  5. Publish web at https://shop.example.com/.

Solution.

  1. Apply the default-deny-ingress policy shown above in shop.
  2. Policy web-allow: podSelector: app=web; one from with namespaceSelector: kubernetes.io/metadata.name=ingress-system; port 8080.
  3. Policy api-allow: podSelector: app=api; from podSelector: app=web; port 8080.
  4. Policy db-allow: podSelector: app=db; from podSelector: app=api; port 5432.
  5. Create an Ingress with ingressClassName, host shop.example.com, path / and TLS, backend web: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, pathType and same-namespace backends.
  • Gateway API: GatewayClass (provider), Gateway (operator), HTTPRoute (developer).
  • Weighted backendRefs implement canary releases.
  • kubectl describe on the route shows Accepted and ResolvedRefs.

Official sources