Skip to main content

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:

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

  1. Create a text file on your local computer named test.txt containing the following:
    SPIFFE-to-GCP authentication succeeded!
  2. 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)"
  3. Create the Cloud Storage bucket:
    gcloud storage buckets create gs://${TEST_BUCKET} \
    --project=${GCP_PROJECT_ID} \
    --location=us-central1
  4. Upload the test file to the bucket:
    gcloud storage cp test.txt gs://${TEST_BUCKET}/test.txt
  5. 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.

  1. Ensure that spirlctl is installed., and use spirlctl login to log in via SSO.
  2. 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
  3. 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.
note

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.

  1. Set environment variables for the pool and provider names:
    export POOL_ID="defakto-identity-pool"
    export PROVIDER_ID="defakto-oidc-provider"
  2. Create the Workload Identity Pool:
    gcloud iam workload-identity-pools create ${POOL_ID} \
    --project=${GCP_PROJECT_ID} \
    --location="global" \
    --display-name="Defakto Identity Pool"
  3. 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"
    info

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

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

  1. Set an environment variable for the service account name:
    export SERVICE_ACCOUNT="defakto-federation-sa"
  2. Create the service account:
    gcloud iam service-accounts create ${SERVICE_ACCOUNT} \
    --project=${GCP_PROJECT_ID} \
    --display-name="Defakto Federation Service Account"
  3. 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"
  4. 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.

  1. Create the IAM policy binding:
    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||')/*"
    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.

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.

  1. Ensure that spirlctl is installed..
  2. Bootstrap an existing Kubernetes cluster (e.g. "demo-cluster") with SPIRL:
    spirlctl cluster add "demo-cluster" --trust-domain "example.com" --platform k8s
  3. Install the spiffe-demo app and enable spirldbg:
    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-namespace
    tip

    If you have installed the spiffe-demo app before August 2025, you may need to run helm repo update first to get the latest version of the chart.

  4. 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")}')
  5. Connect to the spirldbg container via the terminal:
    kubectl exec -it $POD_NAME -n spiffe-demo -c spirldbg -- sh
  6. Retrieve a JWT with the gcp-demo audience 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
  7. Paste this token into a local file named svid.jwt.
  8. Also copy the SPIFFE ID from the spirldbg output and paste this into a local file named spiffe-id.txt.

7. Test Access to Cloud Storage

Now we're ready to test GCP federation with a Defakto-issued JWT-SVID.

  1. 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')
  2. 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')
  3. 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.txt

    If successful, this will download the file to local-test.txt. See the "Troubleshooting" section if you encounter any errors.

  4. Verify that the local-test.txt file 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.

  1. 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/*"
  2. 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 exp claim)

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

note

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}