Containerization Lesson 97 of 113

GKE Dataplane V2: Cilium-Based Network Policy and Observability

In a nutshell

Level: Intermediate → Expert · Time: ~26 min

GKE Dataplane V2 is the networking engine underneath a GKE cluster — an eBPF/Cilium datapath that Google builds, runs, and patches for you. “Dataplane” just means the part of the cluster that actually moves packets: routing a request to the right pod, load-balancing across a Service’s backends, and deciding whether one pod is even allowed to talk to another. Dataplane V2 swaps the old machinery (kube-proxy writing iptables rules, plus a separate Calico add-on for network policy) for a single set of eBPF programs running in the Linux kernel of every node.

Three things fall out of that swap, and they are the whole reason you would care:

A mental model. Picture the old setup as a building where every door has its own paper checklist taped to it, and a guard reads the whole list top-to-bottom each time someone knocks — slow, and no record of who was turned away. Dataplane V2 replaces the paper with an electronic badge system baked into the building’s wiring: the reader answers instantly, the rules live in one place, the building operator (Google) maintains the whole system, and every swipe — accepted or rejected — is written to a log you can query later. You do not run the badge system; you write the access rules and read the audit trail.

If you have used upstream Cilium or Hubble before, the one sentence to internalize is: Dataplane V2 is managed Cilium. You get most of the policy model and the flow logging, but Google owns the agent, the version, and which features are exposed — you cannot helm install your own.

How a packet flows through GKE Dataplane V2: a pod's traffic is handled by Google-managed Cilium eBPF programs in the node kernel with no kube-proxy and no iptables, NetworkPolicy and FQDN egress are enforced by pod identity, the flow is forwarded or denied, and NetworkLogging records the allow/deny verdict to Cloud Logging -- with eBPF-not-iptables, the managed model, and flow logging marked

Most GKE clusters that “have network policy” actually have an iptables-based enforcement plane bolted onto kube-proxy, and the moment you ask it a hard question — which pod talked to which external host, and was that connection allowed or dropped? — it goes silent. GKE Dataplane V2 changes the substrate underneath. It replaces kube-proxy’s iptables service routing with eBPF programs in the kernel, ships a managed Cilium as the policy engine, and exposes connection-level allow/deny logging that you can actually query. This guide is about running that plane in production: building a default-deny baseline, controlling egress by FQDN and CIDR, proving every decision with logs, reaching for CiliumClusterwideNetworkPolicy when namespaced policy is not enough, and migrating a live cluster without dropping a single packet you meant to keep.

The four objects you’ll actually apply

Everything in this guide is built from four Kubernetes objects. Two are stock Kubernetes; two are GKE CRDs that only exist because Dataplane V2 is running. Keep this map handy — most confusion about “which policy do I write?” dissolves once you know which object lives where.

Object API group Scope What it does Turned on by
NetworkPolicy networking.k8s.io/v1 Namespaced L3/L4 allow-list for pods (ingress/egress by label, port, CIDR) Built in — no flag
FQDNNetworkPolicy networking.gke.io/v1alpha1 Namespaced Egress allow-list by hostname / wildcard domain --enable-fqdn-network-policy
NetworkLogging networking.gke.io/v1alpha1 Cluster (one object, named default) Streams allow/deny decisions to Cloud Logging Applied as a CRD; logging is on by config
CiliumClusterwideNetworkPolicy cilium.io/v2 Cluster Cluster-wide L3/L4 guardrails no namespace can escape --enable-cilium-clusterwide-network-policy

The mental split that scales: platform teams own the cluster-scoped objects (CiliumClusterwideNetworkPolicy for non-negotiable denies, NetworkLogging for the audit trail), and app teams own the namespaced objects (NetworkPolicy and FQDNNetworkPolicy for their own service’s allow-list). Effective access is always the union of every policy that selects a pod — there is no rule ordering, and no explicit “deny” rule to write.

1. How Dataplane V2 replaces kube-proxy with eBPF and Cilium

In a stock GKE cluster, kube-proxy watches Services and Endpoints and writes iptables (or IPVS) rules so that a packet to a ClusterIP gets DNAT’d to a backend pod. Network policy, if you enabled it, was a second system (Calico) writing its own iptables chains. Two control loops, two rule sets, and rule evaluation cost that grows with the number of services and policies.

Dataplane V2 collapses this. As a packet hits a GKE node, eBPF programs attached in the kernel decide routing, load-balancing, and policy enforcement in one pass. There is no kube-proxy DaemonSet — service load-balancing is done in eBPF. Cilium’s agent (anetd / cilium pods in kube-system) programs those maps from Kubernetes objects. The practical wins:

Mental model: Dataplane V2 is managed Cilium. You get Kubernetes NetworkPolicy, GKE-specific CRDs (FQDNNetworkPolicy, NetworkLogging), and — where supported — a subset of native Cilium CRDs. You do not get a free-for-all Cilium install; Hubble UI, Cilium-managed Ingress, and arbitrary Cilium versions are Google’s to manage, not yours.

Enable it at create time (it is the default for Autopilot, and required there):

gcloud container clusters create prod-apps \
  --project my-prod-project \
  --region us-central1 \
  --enable-dataplane-v2 \
  --enable-ip-alias \
  --release-channel regular

Confirm the dataplane and the absence of kube-proxy:

gcloud container clusters describe prod-apps \
  --region us-central1 \
  --format="value(networkConfig.datapathProvider)"
# Expect: ADVANCED_DATAPATH

kubectl get ds -n kube-system | grep -E 'kube-proxy|anetd'
# anetd present; kube-proxy absent

Here is the same cluster, component by component, before and after the switch:

Concern Stock GKE (kube-proxy + Calico) Dataplane V2
Service routing kube-proxy writes iptables/IPVS rules eBPF program in the kernel; no kube-proxy
Policy engine Calico add-on with its own iptables chains Managed Cilium (anetd), native enforcement
Rule-evaluation cost Grows with Service + policy count Constant-time map lookup, flat with scale
Allow/deny logging None you can query policy-action log in Cloud Logging
Who runs the CNI You (install and upgrade Calico) Google (managed)
How you enable it Add-on toggle --enable-dataplane-v2 at create time

2. Default-deny baselines and namespace-scoped policies

A network policy plane is only as good as its baseline. The default Kubernetes posture is allow-all; until a pod is selected by at least one policy, everything reaches it. The correct production stance is default-deny per namespace, then allow explicitly.

Apply a deny-all (ingress and egress) in every workload namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}            # selects every pod in the namespace
  policyTypes:
    - Ingress
    - Egress

Egress deny-all will break DNS immediately, so the very next policy must re-allow DNS to kube-dns. Without this, name resolution fails and every outbound connection times out before it starts:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Now allow a specific path: the api pods accept ingress from frontend pods on 8080, and may egress to the db tier on 5432.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: db
      ports:
        - protocol: TCP
          port: 5432

The kubernetes.io/metadata.name label is auto-applied by Kubernetes to every namespace, which makes it a reliable selector for cross-namespace rules — do not hand-label namespaces for this.

3. Egress control with FQDN-based and CIDR-based policies

Kubernetes NetworkPolicy egress only understands IPs and CIDRs via ipBlock. That is fine for stable infrastructure (a Cloud SQL private IP, an on-prem range) and useless for *.googleapis.com whose IPs churn. Dataplane V2 solves both.

CIDR egress with stock NetworkPolicy — allow the api tier to reach an on-prem CIDR but never the metadata server or RFC1918 by accident:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress-onprem
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 10.50.0.0/16
            except:
              - 10.50.7.0/24
      ports:
        - protocol: TCP
          port: 443

FQDN egress is a GKE-specific CRD, FQDNNetworkPolicy, enabled per-cluster. It works by snooping DNS responses (so it requires kube-dns or Cloud DNS — custom CoreDNS is not supported) and programming the resolved IPs into the eBPF policy maps. Turn it on:

gcloud container clusters update prod-apps \
  --region us-central1 \
  --enable-fqdn-network-policy

Then restrict the api pods to a specific external host plus a wildcard domain on 443. name is an exact FQDN; pattern accepts wildcards:

apiVersion: networking.gke.io/v1alpha1
kind: FQDNNetworkPolicy
metadata:
  name: api-egress-fqdn
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: api
  egress:
    - matches:
        - name: "secure.payments-partner.com"
        - pattern: "*.googleapis.com"
      ports:
        - protocol: TCP
          port: 443

Requirements worth pinning: FQDNNetworkPolicy needs GKE 1.26.4-gke.500 / 1.27.1-gke.400 or later and a supported DNS provider, and it does not cover Windows node pools or Cloud Service Mesh sidecars. Because enforcement is DNS-driven, a pod that connects by raw IP (skipping DNS) is not matched by an FQDN rule — pair FQDN policies with a CIDR deny if that bypass matters to you.

4. Network policy logging and verifying allow/deny decisions

This is the capability that justifies Dataplane V2 on its own. Logging is configured by a single cluster-scoped CRD named NetworkLogging. There is exactly one object per cluster and its name must be default — it cannot be renamed.

apiVersion: networking.gke.io/v1alpha1
kind: NetworkLogging
metadata:
  name: default
spec:
  cluster:
    allow:
      log: true
      delegate: false
    deny:
      log: true
      delegate: false

delegate: false logs cluster-wide; set delegate: true to only log connections for namespaces explicitly annotated (policy.network.gke.io/enable-logging: "true"), which is how you keep log volume sane at scale. Apply it:

kubectl apply -f networklogging.yaml
kubectl get networklogging default -o yaml

Decisions land in Cloud Logging under the policy-action log on the k8s_node resource. Query allowed and denied connections for a workload:

gcloud logging read \
  --project my-prod-project \
  'resource.type="k8s_node"
   resource.labels.cluster_name="prod-apps"
   logName="projects/my-prod-project/logs/policy-action"
   jsonPayload.connection.dest_port=5432' \
  --limit 20 --freshness 1h

A denied entry carries jsonPayload.disposition="deny" plus source/destination pod, namespace, and the policy_ref that made (or failed to make) the decision. To isolate drops in Logs Explorer:

resource.type="k8s_node"
resource.labels.cluster_name="prod-apps"
logName="projects/my-prod-project/logs/policy-action"
jsonPayload.disposition="deny"

This is your loop for safe rollout: enable logging first, watch deny entries for traffic you actually need, add allow rules, and only then tighten. You never have to guess what a policy will break.

5. Cluster-wide policies and tiering with Cilium CRDs

Namespaced NetworkPolicy cannot express “no pod in this cluster may ever reach the GCE metadata server” — you would have to copy it into every namespace and trust nobody forgets. Dataplane V2 supports CiliumClusterwideNetworkPolicy (CCNP), a cluster-scoped CRD, for exactly these org-wide guardrails. Enable it:

gcloud container clusters update prod-apps \
  --region us-central1 \
  --enable-cilium-clusterwide-network-policy

A canonical guardrail — block the link-local metadata endpoint cluster-wide while still allowing it through Workload Identity’s expected path is a common pattern, but the simplest universal rule is a clusterwide ingress contract. This example lets every role=backend endpoint accept ingress on 80 only from role=frontend, regardless of namespace:

apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: l4-ingress-backend
spec:
  endpointSelector:
    matchLabels:
      role: backend
  ingress:
    - fromEndpoints:
        - matchLabels:
            role: frontend
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP

Version floor: CCNP needs gcloud 465.0.0+ and GKE 1.28.6-gke.1095000 / 1.29.1-gke.1016000 or later. Treat CCNP as the platform team’s layer — broad baselines and non-negotiable denies — and leave per-app allows to namespaced NetworkPolicy owned by app teams. GKE’s managed Cilium does not expose every upstream Cilium feature, so validate any CRD field against the GKE docs before depending on it; L7 (HTTP) policy in particular is not part of the managed surface.

6. Interactions with Gateway API, Services, and internal load balancers

Two failure modes bite teams here. First, load balancer health checks. A default-deny ingress policy will silently drop Google’s health-check probes, the LB marks backends unhealthy, and you get a 502 with no obvious cause. You must allow the GKE health-check ranges. With container-native load balancing (NEGs), probes originate from Google’s infrastructure ranges; allow them explicitly:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gclb-health-checks
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - ipBlock:
            cidr: 35.191.0.0/16
        - ipBlock:
            cidr: 130.211.0.0/22
      ports:
        - protocol: TCP
          port: 8080

Second, client identity through the data path. With the Gateway API and container-native LBs, traffic from an external or internal Application Load Balancer arrives at the pod from those Google ranges, not from a node IP — so ingress policies must select on the LB CIDRs above, not on pod selectors, for north-south traffic. East-west pod-to-pod and pod-to-ClusterIP traffic keeps Cilium identity, so podSelector works there. An internal passthrough LB (Service type: LoadBalancer, internal) preserves the original client IP, which means your ipBlock rules should reflect the actual on-prem or VPC client ranges, not the LB.

7. Performance characteristics and known limitations

The eBPF datapath removes the iptables-chain tax: service and policy lookups are O(1) map operations, so throughput and tail latency stay flat as you scale services and policies — the opposite of kube-proxy + Calico, where the rule count is a linear cost. Direct Server Return and eBPF-based load balancing also cut per-connection overhead.

What to plan around:

Enterprise scenario

A fintech platform team ran a regional GKE Standard cluster hosting a PCI-scoped payments namespace alongside a dozen non-regulated services. Their auditor’s finding was blunt: they could not prove that payment pods only egressed to the card-network partner and Google APIs, and nothing else. They had Calico NetworkPolicy, but Calico gave them no per-connection allow/deny evidence and no FQDN control — the partner published a hostname, not a stable CIDR, so the team had been allow-listing a /16 that was far wider than the partner actually used.

The constraint: they could not take a maintenance window long enough to recreate the cluster, and they could not risk dropping live settlement traffic. The fix was a staged migration to Dataplane V2 (covered in the next section), but the audit-closing piece was the policy design. They enabled FQDNNetworkPolicy, replaced the /16 allow with an exact-host rule, and turned on NetworkLogging scoped to payments via delegate. Two CRDs closed the finding:

apiVersion: networking.gke.io/v1alpha1
kind: FQDNNetworkPolicy
metadata:
  name: payments-partner-egress
  namespace: payments
spec:
  podSelector:
    matchLabels:
      pci-scope: "true"
  egress:
    - matches:
        - name: "settle.cardnetwork-partner.com"
        - pattern: "*.googleapis.com"
      ports:
        - protocol: TCP
          port: 443
---
apiVersion: networking.gke.io/v1alpha1
kind: NetworkLogging
metadata:
  name: default
spec:
  cluster:
    allow:
      log: true
      delegate: true        # only annotated namespaces (payments)
    deny:
      log: true
      delegate: false        # log all denies cluster-wide

They annotated the payments namespace, exported the policy-action logs to a BigQuery sink, and handed the auditor a query that returned every payment-pod egress with its destination FQDN and disposition. The /16 shrank to one hostname, and “we believe it is restricted” became “here is the connection log.”

Verify

Run this sequence after applying policies — it confirms the dataplane, the deny baseline, an FQDN allow, and that logging is recording decisions.

# 1. Confirm Dataplane V2 is active and kube-proxy is gone
gcloud container clusters describe prod-apps --region us-central1 \
  --format="value(networkConfig.datapathProvider)"   # ADVANCED_DATAPATH
kubectl get pods -n kube-system -l k8s-app=cilium      # anetd/cilium Running

# 2. Default-deny works: this should TIME OUT (no allow rule yet)
kubectl run probe --rm -it --image=curlimages/curl -n payments -- \
  curl -m 5 http://api.payments.svc.cluster.local:8080

# 3. FQDN allow works: this should SUCCEED for an allowed host
kubectl run probe --rm -it --image=curlimages/curl -n payments \
  --labels="app=api" -- curl -sS -m 5 https://www.googleapis.com -o /dev/null -w "%{http_code}\n"

# 4. The deny was logged
gcloud logging read --project my-prod-project \
  'logName="projects/my-prod-project/logs/policy-action"
   resource.labels.cluster_name="prod-apps"
   jsonPayload.disposition="deny"' \
  --limit 5 --freshness 10m

If step 2 returns a response, your deny baseline is not selecting that pod (check policyTypes). If step 3 hangs, your FQDNNetworkPolicy either is not enabled at the cluster level or the pod is not matched by podSelector. If step 4 is empty, NetworkLogging/default is missing or deny.log is false.

8. Migrating an existing cluster and validating no regressions

You cannot flip an existing Standard cluster to Dataplane V2 in place — the migration recreates the dataplane and is disruptive — so treat it as a planned change, not a flag toggle.

  1. Inventory current policy. Export every NetworkPolicy and confirm whether Calico is the enforcer (kubectl get pods -n kube-system | grep calico). Catalog egress that relies on wide CIDRs you intend to replace with FQDN rules.
  2. Stand up a Dataplane V2 clone. Build a parallel cluster with --enable-dataplane-v2 --enable-fqdn-network-policy, apply the same NetworkPolicy set, and run synthetic and canary traffic.
  3. Enable logging in audit mode first. Apply NetworkLogging/default with deny.log: true before tightening. Watch policy-action for legitimate traffic being denied; every such entry is a missing allow rule.
  4. Reconcile, then enforce. Add the allow and FQDN policies the logs revealed, re-test, and confirm zero unexpected denies over a representative window.
  5. Cut over by shifting workloads, not by mutating the old cluster — drain and redeploy onto the new cluster behind the same LB/Gateway, watch error rates, and keep the old cluster as instant rollback until the deny log is clean.

The non-negotiable rule: log before you enforce. Dataplane V2’s allow/deny logging exists precisely so a network-policy migration is evidence-driven, not a leap of faith. If your policy-action deny stream is empty for traffic you expect to keep, you are ready to cut over.

Going deeper

If default-deny and label-based policy are new to you, the mechanics are built from first principles in Kubernetes network policies: default-deny and L7 with Cilium; this section is the GKE-specific, principal-level layer on top.

Why eBPF beats the iptables datapath at scale

The reason Dataplane V2 exists is a data-structure problem. kube-proxy in iptables mode expresses every Service as a chain of rules, and the kernel evaluates them sequentially — an O(n) walk where n grows with Services, endpoints, and policies. On a cluster with thousands of Services this shows up as measurable connection-setup latency and as iptables-restore passes that take seconds, during which rule updates lag reality. IPVS mode improves the lookup but still leans on the netfilter conntrack path.

eBPF changes the shape of the problem. Cilium compiles policy and Service state into hash maps the kernel reads in constant time, attached at the tc and XDP hooks on the node’s network device. A Service lookup or a policy verdict is a single map read regardless of how many Services or policies exist — so p99 connection latency stays flat while an equivalent iptables cluster degrades. For in-cluster traffic Cilium can even load-balance at the socket layer (at connect() time), so the packet never carries a Service VIP onto the wire and there is no per-packet DNAT at all. The upstream eBPF/Cilium/Hubble model is unpacked in Cilium eBPF network policy and Hubble observability; Dataplane V2 is that engine in managed form.

Identity, not IP address

This is the conceptual leap that trips up engineers coming from firewalls. In a traditional network you allow 10.1.2.3 → 10.4.5.6:5432. In Cilium the unit of policy is a security identity: a numeric ID derived from a pod’s labels. When you write “pods with app: api may reach pods with app: db on 5432,” Cilium resolves app=api and app=db to identities and programs those into the eBPF maps. The identity travels with the packet, so the verdict is decided once and cached.

The practical payoff: a pod can be killed and rescheduled onto a completely different node and IP, and the policy still holds the instant the new pod comes up with the same labels — no rule rewrite, no propagation lag keyed on IP churn. It also means you must never key a policy on a pod IP; IPs are ephemeral and a policy written against one is a latent outage. Key on labels for pods, and reserve ipBlock for things that genuinely have stable addresses (on-prem ranges, a Cloud SQL private IP, the health-check ranges). The same identity model is what lets a CiliumClusterwideNetworkPolicy fence off the GCE metadata server without breaking Workload Identity’s expected path.

FQDN egress works; L7 HTTP policy does not

Two questions come up constantly, and they have opposite answers on GKE’s managed surface:

Flow visibility: managed Hubble and the policy-action log

Dataplane V2 gives you two distinct observability surfaces, and they answer different questions:

  1. NetworkLogging → the policy-action log (section 4) is the audit surface: a durable, queryable record in Cloud Logging of every allow/deny with pod, namespace, and deciding policy. This is what you hand an auditor.
  2. GKE Dataplane V2 observability (managed Hubble) is the operational surface. Enable it with gcloud container clusters update CLUSTER --enable-dataplane-v2-flow-observability, and GKE runs Hubble Relay for you and lets you stream live L3/L4 flows (hubble observe) and scrape flow metrics. You do not get to run your own Hubble stack or the full upstream Hubble UI — as with everything here, it is the managed subset.

Reach for the policy-action log to prove a decision after the fact; reach for managed Hubble to watch traffic while you are actively debugging a policy you just changed.

The managed-Cilium model: what you can and can’t tune

The single most important mental adjustment for an experienced Cilium user is the boundary of control:

You control Google controls
NetworkPolicy, FQDNNetworkPolicy, NetworkLogging, CiliumClusterwideNetworkPolicy objects The Cilium version and its upgrade cadence
Which features you enable (FQDN, CCNP, flow observability) via cluster flags The anetd agent config and eBPF datapath internals
Your policy design, scope, and log routing Which Cilium CRDs and fields are exposed (L7 is not)
Namespace annotations for scoped logging Hubble UI, custom Cilium Helm values, arbitrary CRDs

The rule of thumb: if a capability requires editing the Cilium ConfigMap or installing a Cilium Helm chart, it is not yours to change on GKE. Validate any cilium.io CRD field against the GKE docs, not upstream Cilium docs, before you depend on it — the upstream field may simply be ignored.

Migrating from Dataplane V1 (and the limits that bite)

Dataplane V2 is a create-time property of a cluster. Autopilot clusters are Dataplane V2 by default, so there is nothing to migrate there. For a Standard cluster still on Dataplane V1 (kube-proxy plus optional Calico), the supported route is the staged, parallel-cluster migration in section 8 — you do not hot-toggle a running production cluster’s datapath. What you can change with gcloud container clusters update on a cluster that already runs Dataplane V2 is the feature set: --enable-fqdn-network-policy, --enable-cilium-clusterwide-network-policy, and --enable-dataplane-v2-flow-observability.

A few managed-datapath limitations to design around, beyond the L7 and DNS-IP caveats above:

Practice challenges

Work these in order; each has a hidden solution. If you have no cluster, treat them as “write the manifest/command and check it against the solution” — every answer here is schema-correct for current GKE.

1 (Beginner) — Confirm the datapath. You inherit a cluster and need to know in one command whether it runs Dataplane V2. Which command, and what value proves it?

<details> <summary>Solution</summary>

gcloud container clusters describe CLUSTER --region REGION \
  --format="value(networkConfig.datapathProvider)"
# ADVANCED_DATAPATH  ->  Dataplane V2 is on
# LEGACY_DATAPATH    ->  it is not

ADVANCED_DATAPATH is the tell. As a cross-check, kubectl get ds -n kube-system shows anetd present and kube-proxy absent.

</details>

2 (Beginner) — Lock a namespace, keep DNS. Write the two policies that put namespace shop into default-deny (ingress + egress) without breaking name resolution.

<details> <summary>Solution</summary>

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-all, namespace: shop }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns-egress, namespace: shop }
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }

Why: a default-deny egress drops DNS immediately, so the DNS allow must land in the same change or every lookup times out.

</details>

3 (Intermediate) — Allow egress to Google APIs by name. The api pods in shop must reach *.googleapis.com on 443 and nothing else external. What must be enabled, and what do you apply?

<details> <summary>Solution</summary>

Enable the feature once per cluster, then apply the FQDN policy:

gcloud container clusters update CLUSTER --region REGION \
  --enable-fqdn-network-policy
apiVersion: networking.gke.io/v1alpha1
kind: FQDNNetworkPolicy
metadata: { name: api-egress-google, namespace: shop }
spec:
  podSelector:
    matchLabels: { app: api }
  egress:
    - matches:
        - pattern: "*.googleapis.com"
      ports:
        - { protocol: TCP, port: 443 }

Gotcha: a pod that dials a raw IP skips DNS and is not matched — keep the default-deny egress from challenge 2 as the backstop.

</details>

4 (Intermediate) — Prove a denial. A partner says your pods are being blocked. Turn on deny logging and pull the last hour of denied connections to port 5432 on cluster prod-apps.

<details> <summary>Solution</summary>

apiVersion: networking.gke.io/v1alpha1
kind: NetworkLogging
metadata: { name: default }        # MUST be named default
spec:
  cluster:
    allow: { log: false, delegate: false }
    deny:  { log: true,  delegate: false }
gcloud logging read \
  'resource.type="k8s_node"
   resource.labels.cluster_name="prod-apps"
   logName="projects/PROJECT/logs/policy-action"
   jsonPayload.disposition="deny"
   jsonPayload.connection.dest_port=5432' \
  --limit 20 --freshness 1h

Why: the NetworkLogging object is cluster-scoped and must be named default; denies carry disposition="deny" plus source/dest pod and the deciding policy_ref.

</details>

5 (Advanced) — One guardrail, whole cluster. Platform policy: any pod labelled role: backend, in any namespace, may accept ingress on 80 only from role: frontend. Namespaced policy cannot guarantee this. What do you use, and why is this the right layer?

<details> <summary>Solution</summary>

Enable and apply a cluster-scoped Cilium policy:

gcloud container clusters update CLUSTER --region REGION \
  --enable-cilium-clusterwide-network-policy
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata: { name: backend-ingress-frontend-only }
spec:
  endpointSelector:
    matchLabels: { role: backend }
  ingress:
    - fromEndpoints:
        - matchLabels: { role: frontend }
      toPorts:
        - ports: [{ port: "80", protocol: TCP }]

Why this layer: a namespaced NetworkPolicy would have to be copied into every namespace and trusts nobody to forget. CiliumClusterwideNetworkPolicy is cluster-scoped, so it applies everywhere at once — the platform team’s guardrail, not an app team’s rule. Keep it L3/L4; L7 is not supported here.

</details>

Common beginner mistakes

Glossary

Checklist

gkedataplane-v2ciliumnetwork-policykubernetes-security
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments