GCP ACE: RBAC and Workload Identity Federation in GKE
Two separate access questions come up constantly on the exam, and they’re easy to confuse: who can call the Kubernetes API and do what (RBAC), versus how a Pod’s own code authenticates to Google Cloud APIs like Cloud Storage or BigQuery (Workload Identity Federation). This article covers both.
RBAC — who can do what inside the cluster
Four objects, two pairs:
| Object | Scope |
|---|---|
| Role | Permissions within one namespace |
| ClusterRole | Permissions cluster-wide — covers cluster-scoped resources (like nodes), non-resource endpoints (/healthz), or namespaced resources across every namespace |
| RoleBinding | Binds a Role (or a ClusterRole) to subjects, scoped to one namespace — binding a ClusterRole this way limits its effect to just that namespace |
| ClusterRoleBinding | Binds a ClusterRole to subjects across all namespaces |
Subjects can be Google Cloud users, Kubernetes ServiceAccounts, IAM service accounts, or Google Groups (the last one only after you configure Google Groups for RBAC, with the groups nested under a gke-security-groups group). RBAC is purely additive — there are no deny rules, so every binding only grants, never restricts.
Example: read-only Pod access in one namespace
# role-pod-reader.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: dev
name: pod-reader
rules:
- apiGroups: [""] # "" = core API group
resources: ["pods"]
verbs: ["get", "list", "watch"]
# rolebinding-pod-reader.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-reader-binding
namespace: dev
subjects:
- kind: User
name: [email protected]
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Equivalent imperative form:
kubectl create role pod-reader --namespace=dev --resource=pods --verb=get,list,watch
kubectl create rolebinding pod-reader-binding --namespace=dev --role=pod-reader --user=[email protected]
Verifying access:
kubectl auth can-i list pods --namespace=dev --as=[email protected] # yes
kubectl auth can-i delete pods --namespace=dev --as=[email protected] # no
One rule worth remembering: you can only grant permissions you already hold yourself. To create or update a Role, you need every permission it contains, or the escalate verb on roles. To create or update a binding, you need the permissions in the role being bound (at the same scope), or the bind verb on that role. You can’t create a Role that lets someone do more than you can do. On GKE, if those permissions came to you through IAM rather than RBAC, the request can still fail with attempt to grant extra privileges.
Workload Identity Federation — how Pods authenticate to Google Cloud
This solves a different problem: your app’s code inside a Pod needs to call a Google Cloud API (read from Cloud Storage, write to BigQuery). The insecure default is letting it inherit the node’s service account — meaning every Pod on that node, regardless of what it actually needs, has whatever permissions the node has. Workload Identity Federation scopes that down to the individual workload.
How it works: each workload is identified by its own Kubernetes ServiceAccount (KSA). Google’s recommended approach is to grant IAM roles to that KSA directly, using an IAM principal identifier; the workload then gets a federated access token. The alternative is to link the KSA to a Google Cloud IAM service account (GSA), and GKE exchanges tokens behind the scenes via the IAM Service Account Credentials API. You need the alternative when the API you’re calling doesn’t support federated identities, or its limitations block your use case. Authentication flows through the same metadata-server mechanism as Compute Engine, so client libraries that already read the metadata server typically need zero code changes.
Enable it, once per Standard cluster (Autopilot clusters always have it enabled, on every node):
gcloud container clusters update CLUSTER_NAME \
--location=CONTROL_PLANE_LOCATION \
--workload-pool=PROJECT_ID.svc.id.goog
Node pools need the GKE metadata server enabled too (and Pods must be scheduled onto those pools):
gcloud container node-pools create NODEPOOL_NAME \
--cluster=CLUSTER_NAME \
--location=CONTROL_PLANE_LOCATION \
--workload-metadata=GKE_METADATA
Once a node pool has this enabled, Pods on it can no longer fall back to the node’s own service account — a side effect worth knowing, since it’s what actually closes the original security gap.
Recommended: grant a role directly to the KSA. For example, read access to one bucket:
gcloud storage buckets add-iam-policy-binding gs://BUCKET \
--role=roles/storage.objectViewer \
--member=principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/NAMESPACE/sa/KSA_NAME \
--condition=None
Note the principal uses the project number in the path and the project ID in the pool name.
Alternative: link a KSA to a GSA, step by step:
# 1. Create the namespace and KSA
kubectl create namespace NAMESPACE
kubectl create serviceaccount KSA_NAME --namespace=NAMESPACE
# 2. Create the IAM service account
gcloud iam service-accounts create IAM_SA_NAME --project=IAM_SA_PROJECT_ID
# 3. Grant the GSA the roles the workload actually needs
gcloud projects add-iam-policy-binding IAM_SA_PROJECT_ID \
--member "serviceAccount:IAM_SA_NAME@IAM_SA_PROJECT_ID.iam.gserviceaccount.com" \
--role "ROLE_NAME"
# 4. Allow the KSA to impersonate the GSA
gcloud iam service-accounts add-iam-policy-binding IAM_SA_NAME@IAM_SA_PROJECT_ID.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"
# 5. Annotate the KSA to link it to the GSA
kubectl annotate serviceaccount KSA_NAME \
--namespace NAMESPACE \
iam.gke.io/gcp-service-account=IAM_SA_NAME@IAM_SA_PROJECT_ID.iam.gserviceaccount.com
With this linking approach, both step 4 (the IAM allow policy) and step 5 (the annotation) are required — either alone isn’t enough.
Exam Takeaways
- Role/RoleBinding = namespace-scoped; ClusterRole/ClusterRoleBinding = cluster-wide. A ClusterRole bound via a RoleBinding is still limited to that one namespace.
- RBAC has no deny rules — it’s purely additive, and you can’t grant a permission you don’t already hold (unless you have the
escalateverb for roles or thebindverb for bindings). kubectl auth can-i ... --as=USERis how you verify RBAC rules without actually attempting the action.- Workload Identity Federation scopes GCP API access to the individual Pod’s KSA, instead of every Pod on a node inheriting the node’s service account.
- Enabling the GKE metadata server on a node pool (
--workload-metadata=GKE_METADATA) is what blocks Pods there from falling back to the node’s own identity. - Best practice is granting IAM roles directly to the KSA’s
principal://...svc.id.goog/subject/ns/NAMESPACE/sa/KSA_NAMEidentifier. Autopilot clusters have Workload Identity Federation always on. - The alternative, linking a KSA to a GSA, needs both
roles/iam.workloadIdentityUseron the GSA and theiam.gke.io/gcp-service-accountannotation on the KSA — missing either one breaks the link. - If a question is about permissions on the Kubernetes API itself → RBAC. If it’s about a Pod calling a Google Cloud API (Cloud Storage, BigQuery, Pub/Sub) → Workload Identity Federation.