Tutorial: GCP Federation
A Defakto-issued JSON Web Token-SPIFFE Verifiable Identity Document (JWT-SVID) can be used to authenticate to Google Cloud Platform (GCP) APIs and access GCP resources. This is done by setting up a Workload Identity Pool that supports OpenID Connect (OIDC). Defakto makes this process easier by automatically providing an OIDC Discovery document endpoint for your trust domain(s).
Preconditions:
- Active SPIFFE trust domain (e.g. "example.com")
- Existing Kubernetes cluster
- GCP project with permissions to create a Workload Identity Pool, Service Account, IAM bindings, and a Cloud Storage bucket for testing.
- Google Cloud CLI (
gcloud) installed and authenticated
This tutorial will demonstrate using a combination of the GCP console and the command-line, but deploying the necessary infrastructure can also be performed with tools like Terraform, Pulumi, or Google's Deployment Manager.
1. Create a Cloud Storage bucket and upload test file
- Create a text file on your local computer named
test.txtcontaining the following:SPIFFE-to-GCP authentication succeeded! - Set environment variables for your GCP project and desired bucket name:
export GCP_PROJECT_ID="your-project-id"
export GCP_PROJECT_NUMBER=$(gcloud projects describe ${GCP_PROJECT_ID} --format="value(projectNumber)")
export TEST_BUCKET="defakto-test-bucket-$(date +%s)" - Create the Cloud Storage bucket:
gcloud storage buckets create gs://${TEST_BUCKET} \
--project=${GCP_PROJECT_ID} \
--location=us-central1 - Upload the test file to the bucket:
gcloud storage cp test.txt gs://${TEST_BUCKET}/test.txt - Save the bucket name for later use:
echo ${TEST_BUCKET} > test-bucket
2. Determine the OIDC Discovery Endpoint
Defakto automatically publishes an OIDC Discovery document for all trust domains.
- Ensure that
spirlctlis installed., and usespirlctl loginto log in via SSO. - Run
spirlctl trust-domain info <TRUST_DOMAIN>to find the right URI for your trust domain. For example, here is the output for a demonstration trust domain:$ spirlctl trust-domain info defakto.example.com
Getting Trust Domain Info⠼
ID td-wyw33falzt
Name: defakto.example.com
Status: available
Self-Managed: false
SPIRL Agent Endpoint: td-wyw33falzt.agent.spirl.com:443
SPIFFE Bundle Endpoint: https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt/bundle
JWT Issuer: https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt
JWKS Endpoint: https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt/jwks
OIDC Discovery Endpoint: https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt/.well-known/openid-configuration
Created At: 2024-09-05 00:41:36.716 +0000 UTC
Last Updated At: 2025-07-07 15:15:14.718 +0000 UTC - Copy the URI for JWT Issuer and save in a file named
jwt-issuer.txt. Make sure it does not end with/.well-known/openid-configuration.
By convention, /.well-known/openid-configuration is appended to the OIDC
discovery endpoint to retrieve JWT issuer metadata. Here's an example of what that
document looks like:
$ curl -s https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt/.well-known/openid-configuration |jq
{
"issuer": "https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt",
"jwks_uri": "https://fed.spirl.org/t-o9cpowm5yo/td-wyw33falzt/jwks",
"authorization_endpoint": "",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256"
]
}
3. Create a Workload Identity Pool and Provider
GCP uses Workload Identity Federation to authenticate external identities. We'll create a Workload Identity Pool and an OIDC provider within it.
- Set environment variables for the pool and provider names:
export POOL_ID="defakto-identity-pool"
export PROVIDER_ID="defakto-oidc-provider" - Create the Workload Identity Pool:
gcloud iam workload-identity-pools create ${POOL_ID} \
--project=${GCP_PROJECT_ID} \
--location="global" \
--display-name="Defakto Identity Pool" - Create the OIDC provider within the pool using the JWT Issuer URL:
gcloud iam workload-identity-pools providers create-oidc ${PROVIDER_ID} \
--project=${GCP_PROJECT_ID} \
--location="global" \
--workload-identity-pool=${POOL_ID} \
--issuer-uri=$(cat jwt-issuer.txt) \
--allowed-audiences="gcp-demo" \
--attribute-mapping="google.subject=assertion.sub"infoIn production, the actual audience value depends on the application or workload configuration. The application either passes in a value when calling the Workload API, or Defakto can be used to configure custom JWT claims for a given cluster. ::
- Save the full provider resource name for later use:
gcloud iam workload-identity-pools providers describe ${PROVIDER_ID} \
--project=${GCP_PROJECT_ID} \
--location="global" \
--workload-identity-pool=${POOL_ID} \
--format="value(name)" > provider-name.txt
4. Create a Service Account
We'll create a GCP Service Account that workloads can impersonate after authenticating with the Defakto JWT-SVID.
- Set an environment variable for the service account name:
export SERVICE_ACCOUNT="defakto-federation-sa" - Create the service account:
gcloud iam service-accounts create ${SERVICE_ACCOUNT} \
--project=${GCP_PROJECT_ID} \
--display-name="Defakto Federation Service Account" - Grant the service account access to the test bucket:
gcloud storage buckets add-iam-policy-binding gs://${TEST_BUCKET} \
--member="serviceAccount:${SERVICE_ACCOUNT}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer" - Save the service account email for later use:
echo "${SERVICE_ACCOUNT}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" > service-account-email.txt
5. Allow the Workload Identity Pool to impersonate the Service Account
Now we'll create an IAM policy binding that allows authenticated workloads from the Workload Identity Pool to impersonate the service account.
- Create the IAM policy binding:
This command grants access to any workload authenticated by the OIDC provider, which already enforces the "gcp-demo" audience value. In the next section, we'll see how to further restrict access to specific SPIFFE IDs.
gcloud iam service-accounts add-iam-policy-binding \
${SERVICE_ACCOUNT}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
--project=${GCP_PROJECT_ID} \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/$(cat provider-name.txt | sed 's|/providers/defakto-oidc-provider||')/*"
6. Deploy the spiffe-demo app in debug mode on the test cluster
We'll modify the spiffe-demo deployment from the "Quick Start"
instructions. by adding a debugging container.
This will provide us with a terminal environment containing
spirldbg, a command-line
utility that will allow us to easily retrieve a JWT with the "gcp-demo" aud value.
- Ensure that
spirlctlis installed.. - Bootstrap an existing Kubernetes cluster (e.g. "demo-cluster") with SPIRL:
spirlctl cluster add "demo-cluster" --trust-domain "example.com" --platform k8s - Install the
spiffe-demoapp and enablespirldbg:helm repo add spiffe-demo https://spirl.github.io/spiffe-demo-app
helm -n spiffe-demo install spiffe-demo spiffe-demo/spiffe-demo-app --set app.enableDebug=true --create-namespacetipIf you have installed the
spiffe-demoapp before August 2025, you may need to runhelm repo updatefirst to get the latest version of the chart. - If this is successful, the following command will show a running pod with two containers:
kubectl get pods -n spiffe-demo
# Put pod name in environment variable
POD_NAME=$(kubectl get pods -n spiffe-demo --no-headers | awk 'END{if(NR==1)print $1; else system("kubectl get pods -n spiffe-demo >&2")}') - Connect to the
spirldbgcontainer via the terminal:kubectl exec -it $POD_NAME -n spiffe-demo -c spirldbg -- sh - Retrieve a JWT with the
gcp-demoaudience and copy the base64-encoded token on the line after "Token:"# spirldbg svid-jwt --audience gcp-demo
Successfully received JWT SVID
SPIFFE ID: spiffe://spirl-demos.example.com/kind-gcp-demo/ns/spiffe-demo/sa/spiffe-demo-app
Expiry: 2025-08-26T23:59:56Z
Token:
eyJhbGciOiJSUzI1NiIsImtpZCI6ImtzXzJ6VG5VV1RKd0YxWXBGUnVxd09EWXRZUUxadiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJnY3AtZGVtbyIsImV4cCI6MTc1NjI1Mjc5NiwiaWF0IjoxNzU2MTY2Mzk2LCJpc3MiOiJodHRwczovL2ZlZC5zcGlybC5vcmcvdC1vOWNwb3dtNXlvL3RkLXd5dzMzZmFsenQiLCJqdGkiOiJhYWVmM2MzOWYxMDdhZTRlYjdkYmJjYzRkZDY4NzlhNCIsInN1YiI6InNwaWZmZTovL3NwaXJsLWRlbW9zLmV4YW1wbGUuY29tL2tpbmQtZ2NwLWRlbW8vbnMvc3BpZmZlLWRlbW8vc2Evc3BpZmZlLWRlbW8tYXBwIn0.apEKOR2mnq8Qw-GSnQe00fSu4TjvvjPEJbiph1UW1DPstrlAUQalh-N2TPqHsl348wVyA1LyL9Tg5C9xuVXoc9zrY6QS77YfzGnU5muThRpLe7SFGaZH42DLjUh_BClnwNJLKWqTO9Uohsfd-yfXNCjs8X9E01pJHLM_St2qkHAofioGcM1bAbmlGdXfIKeBXBf-gFKWzrztsTWZZt9WYhOUiBIXUNr8IC4Kf_fZRsVxk5Z47uoBr2vKWpdz2QhlOWZUF8k7KQlR6FD23g3BqVWK_7xKUDVfacaxJI3IhnR92hopxNMkofBpmCFfBvDMLb-6WZvNCNPKcq2Tz6yRVA - Paste this token into a local file named
svid.jwt. - Also copy the SPIFFE ID from the
spirldbgoutput and paste this into a local file namedspiffe-id.txt.
7. Test Access to Cloud Storage
Now we're ready to test GCP federation with a Defakto-issued JWT-SVID.
-
Generate an federated token using the JWT-SVID and the Workload Identity Pool:
FEDERATED_TOKEN=$(curl -s -X POST https://sts.googleapis.com/v1/token \
-H "Content-Type: application/json" \
-d "{
\"grantType\": \"urn:ietf:params:oauth:grant-type:token-exchange\",
\"audience\": \"//iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}\",
\"scope\": \"https://www.googleapis.com/auth/cloud-platform\",
\"requestedTokenType\": \"urn:ietf:params:oauth:token-type:access_token\",
\"subjectToken\": \"$(cat svid.jwt)\",
\"subjectTokenType\": \"urn:ietf:params:oauth:token-type:jwt\"
}" | jq -r '.access_token') -
Impersonate service account by generating an access token with the federated token:
export ACCESS_TOKEN=$(curl -s -X POST \
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$(cat service-account-email.txt):generateAccessToken" \
-H "Authorization: Bearer ${FEDERATED_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"scope": ["https://www.googleapis.com/auth/cloud-platform"]
}' | jq -r '.accessToken') -
Use the access token to download the test file from Cloud Storage:
curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"https://storage.googleapis.com/storage/v1/b/${TEST_BUCKET}/o/test.txt?alt=media" \
-o local-test.txtIf successful, this will download the file to
local-test.txt. See the "Troubleshooting" section if you encounter any errors. -
Verify that the
local-test.txtfile exists and contains the correct text:$ cat local-test.txt
SPIFFE-to-GCP authentication succeeded!
8. (Optional) Use a SPIFFE ID to further lock down access
The Workload Identity Pool we created allows access to JWTs signed by
a single trust domain server, with a specific audience (aud)
value. We can further restrict access by requiring a specific SPIFFE
ID in the sub claim. We will have to update the IAM policy binding
to only allow a specific SPIFFE ID.
- First, remove the existing binding:
gcloud iam service-accounts remove-iam-policy-binding \
${SERVICE_ACCOUNT}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
--project=${GCP_PROJECT_ID} \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/$(cat provider-name.txt | sed 's|^projects/.*/locations/global/||')/attribute.sub/*" - Add a new binding that includes the specific SPIFFE ID:
export SPIFFE_ID=$(cat spiffe-id.txt)
gcloud iam service-accounts add-iam-policy-binding \
${SERVICE_ACCOUNT}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
--project=${GCP_PROJECT_ID} \
--role="roles/iam.workloadIdentityUser" \
--member="principal://iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/subject/${SPIFFE_ID}"
Now the Workload Identity Pool will not only verify the issuer and audience, but require a specific SPIFFE ID as well. GCP's attribute mapping and CEL conditions allow for more complex logic. Consult the GCP documentation for more information.
Troubleshooting
Permission Denied Error
If you get a "Permission Denied" error when trying to access the Cloud Storage bucket, verify that:
- The service account has the correct IAM binding on the bucket
- The Workload Identity Pool provider has the correct audience configured
- The JWT token includes the expected audience value
You can inspect the JWT token by pasting it into the tool at https://jwt.io to
verify the claims. Alternatively, you can use jq:
cat svid.jwt | jq -R 'split(".") | .[0],.[1] | @base64d | fromjson'
(InvalidToken) Error
If you encounter an error message that includes InvalidToken, verify that:
- The OIDC provider's issuer URI matches the JWT Issuer from
spirlctl trust-domain info - The audience in the JWT matches the allowed audiences in the provider configuration
- The JWT has not expired (check the
expclaim)
(ExpiredToken) Error
If you encounter an error message that includes ExpiredToken, the JWT token
has expired. Retrieve a new token using
spirldbg.
These JWT tokens have a limited expiration time, so you must complete the test
within that time.
You can decode the token by pasting it in the tool at https://jwt.io. There you
can check the token expiration time by hovering your mouse pointer over the exp
field.
In a production environment, you will need a way to automatically refresh the JWT tokens for access to GCP. SPIFFE libraries are available for a number of popular languages.
Failed to generate access token
If the gcloud iam workload-identity-pools generate-access-token command fails,
verify that:
- The Workload Identity Pool and provider exist and are correctly configured
- The service account exists and has the correct IAM bindings
- The JWT token is valid and not expired
- You have the necessary permissions to impersonate the service account
- The IAM Service Account Credentials API is enabled for the project:
gcloud services enable iamcredentials.googleapis.com --project=${GCP_PROJECT_ID}
You can list your Workload Identity Pools and providers with:
gcloud iam workload-identity-pools list --location=global --project=${GCP_PROJECT_ID}
gcloud iam workload-identity-pools providers list --location=global --workload-identity-pool=${POOL_ID} --project=${GCP_PROJECT_ID}