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:
- It is faster and scales flatter. iptables makes the kernel walk a list of rules that grows with every Service and policy; eBPF does the same work as a constant-time lookup in a kernel table, so latency stays flat as the cluster grows.
- Network policy is built in and always on. You write standard Kubernetes
NetworkPolicy; there is no Calico to install. Dataplane V2 also adds GKE-only powers like allowing egress by hostname (*.googleapis.com) instead of by IP. - Every allow/deny decision can be logged. Because Cilium tags each pod with an identity, GKE can write a line to Cloud Logging saying “pod A tried to reach pod B on port 5432 and it was denied” — the exact evidence iptables could never produce.
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.
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:
- Enforcement is always on. Kubernetes
NetworkPolicyis enforced natively; you do not install or manage Calico. - Service routing scales by map lookup, not by walking iptables chains, so policy and service count stop being a latency tax.
- You get identity-aware logging. Because Cilium assigns a security identity to every endpoint, allow/deny decisions can be logged with pod and namespace context — the thing iptables never gave you.
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:
FQDNNetworkPolicyneeds 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
NetworkPolicyowned 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:
- NetworkPolicy object scaling. Google publishes a practical ceiling — historically on the order of a few thousand policies and a low-hundreds count of distinct pod-selector label combinations per cluster. Past that, control-plane programming latency grows. Consolidate policies; do not generate one per pod.
- No L7 policy in the managed surface. Use
NetworkPolicy,FQDNNetworkPolicy, and CCNP for L3/L4. HTTP-method/path enforcement belongs in a service mesh, not here. - FQDN policy is DNS-bound. Raw-IP egress bypasses it; TTL churn means a brief window after a record changes.
- No SSH / no custom CNI. You cannot swap the dataplane after creation by hand, and on Autopilot you cannot run privileged host-network DaemonSets that some third-party CNIs expect.
- Logging volume. Cluster-wide
allowlogging on a busy cluster is expensive; usedelegate: trueand annotate only the namespaces under investigation.
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.
- Inventory current policy. Export every
NetworkPolicyand 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. - Stand up a Dataplane V2 clone. Build a parallel cluster with
--enable-dataplane-v2 --enable-fqdn-network-policy, apply the sameNetworkPolicyset, and run synthetic and canary traffic. - Enable logging in audit mode first. Apply
NetworkLogging/defaultwithdeny.log: truebefore tightening. Watchpolicy-actionfor legitimate traffic being denied; every such entry is a missing allow rule. - Reconcile, then enforce. Add the allow and FQDN policies the logs revealed, re-test, and confirm zero unexpected denies over a representative window.
- 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:
- Can I allow egress by hostname? Yes.
FQDNNetworkPolicywatches DNS answers and programs the resolved IPs into the policy maps, so*.googleapis.combecomes enforceable. The catch is structural: enforcement is DNS-driven, so a pod that connects to a literal IP (skipping DNS) is not matched, and there is a brief reconciliation window after a record’s TTL rolls. Treat FQDN allow-lists as a positive control, backed by a CIDR deny for anything that must never be reachable by raw IP. - Can I allow only
GET /ordersand blockPOST? No — not on managed GKE. Upstream Cilium supports L7 HTTP/DNS/Kafka rules through an embedded Envoy proxy, but GKE’s managed Cilium does not expose L7 policy.CiliumClusterwideNetworkPolicyon GKE is L3/L4 only. HTTP-method and path enforcement belongs in a service mesh, not in the network-policy layer.
Flow visibility: managed Hubble and the policy-action log
Dataplane V2 gives you two distinct observability surfaces, and they answer different questions:
NetworkLogging→ thepolicy-actionlog (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.- 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:
NetworkPolicyendPortranges are not enforced on Dataplane V2 — a policy that allows a port range silently does not take effect as written. Enumerate the ports you need, or reconsider the design.- Scaling ceiling. Google publishes a practical limit on the order of a few thousand policies and a low-hundreds count of distinct pod-selector label combinations per cluster; past it, control-plane programming latency climbs. Consolidate — never generate one policy per pod.
- Logging volume is a real cost. Cluster-wide
allowlogging on a busy cluster is expensive; usedelegate: trueand annotate only the namespaces under investigation. - No custom CNI, no SSH into the datapath. You cannot swap the CNI after creation, and on Autopilot you cannot run the privileged host-network DaemonSets some third-party network tools expect.
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
- Assuming it behaves like iptables or a firewall. There is no rule order and no explicit deny rule. Policies are additive allow-lists; effective access is the union of every policy that selects a pod. If you are hunting for “the rule that blocks this,” you are in the wrong model — the traffic is blocked because nothing allows it, once any policy selects the pod for that direction.
- Default-deny egress without a DNS allow. The most common self-inflicted outage. The instant you apply a default-deny egress, DNS to
kube-dnsis dropped and every outbound connection fails at name resolution — the app looks broken for reasons that have nothing to do with the app. Always ship the UDP/TCP 53 allow tokube-systemin the same change. - Keying a policy on a pod IP. Enforcement is by label-derived identity. A pod IP is ephemeral; a policy written against one works until the pod reschedules, then silently fails. Select pods by label; use
ipBlockonly for genuinely stable addresses. - Expecting to install your own Cilium features. This is managed Cilium. Hubble UI, arbitrary Cilium versions, custom ConfigMap tweaks, and L7 HTTP policy are not yours to enable. If a how-to says
helm upgrade cilium, it does not apply to GKE — find the GKE-managed equivalent instead. - Turning on cluster-wide
allowlogging and forgetting the bill.NetworkLoggingwithallow.log: trueanddelegate: falselogs every accepted connection cluster-wide. On a busy cluster that is a large, continuous Cloud Logging spend. Usedelegate: trueand annotate only the namespaces you are investigating. - Trying to flip an existing cluster to Dataplane V2 with a flag. It is a create-time property, not a hot toggle. You migrate by standing up a parallel Dataplane V2 cluster and shifting workloads — the staged process in section 8 — not by mutating the live one.
- Forgetting load-balancer health-check ranges. A default-deny ingress silently drops Google’s health probes, the LB marks backends unhealthy, and you get a 502 with no obvious cause. Allow
35.191.0.0/16and130.211.0.0/22for LB-fronted pods.
Glossary
- Dataplane — the part of a cluster that actually moves packets: routing, load-balancing, and policy enforcement. The control plane decides intent; the dataplane carries it out.
- GKE Dataplane V2 — GKE’s eBPF/Cilium-based dataplane, managed by Google, replacing
kube-proxy+ iptables and bundling native NetworkPolicy enforcement and flow logging. - eBPF — a Linux kernel technology that runs small sandboxed programs at kernel hooks (here, on the network device) to route and filter packets without a userspace proxy or iptables chains.
- iptables /
kube-proxy— the legacy path:kube-proxyprograms iptables (or IPVS) rules so a Service VIP is DNAT’d to a backend pod. Rule-evaluation cost grows with Service and policy count. - Cilium — the open-source eBPF networking and security project that GKE runs, in managed form, as its dataplane.
anetd— the GKE Dataplane V2 node agent (a DaemonSet inkube-system) that programs the eBPF maps from Kubernetes objects. Its presence, withkube-proxyabsent, confirms Dataplane V2.- Security identity — a numeric ID Cilium derives from a pod’s labels; policy verdicts are decided on identity, not IP, so they survive pod reschedules.
NetworkPolicy— the standard Kubernetes object for L3/L4 ingress/egress allow-lists, enforced natively on Dataplane V2.FQDNNetworkPolicy— a GKE CRD that allows egress by hostname or wildcard domain by snooping DNS answers; enabled with--enable-fqdn-network-policy.NetworkLogging— the cluster-scoped GKE CRD (exactly one, nameddefault) that streams allow/deny decisions to Cloud Logging’spolicy-actionlog.CiliumClusterwideNetworkPolicy(CCNP) — a cluster-scoped Cilium CRD for org-wide L3/L4 guardrails no namespace can escape; enabled with--enable-cilium-clusterwide-network-policy.ADVANCED_DATAPATH— the value ofnetworkConfig.datapathProviderthat indicates Dataplane V2 is active (versusLEGACY_DATAPATH).- Default-deny — the posture where a pod, once selected by any policy for a direction, drops all traffic in that direction except what a policy explicitly allows.
policy-actionlog — the Cloud Logging log name (on thek8s_noderesource) where Dataplane V2 writes connection allow/deny events.- Managed Hubble — GKE Dataplane V2 observability; enable with
--enable-dataplane-v2-flow-observabilityto stream live L3/L4 flows and metrics (the managed subset of upstream Hubble).