

The very first recommendation for accessing GCP resources for workloads outside of GCP is Workload Identity Federation(or WIF) because it offers a highly secure and credential-less way to grant access using federated identities. And the same applies to GitLab.
This blog focuses on setting up GCP Workload Identity Federation(WIF) for a multi-GCP project deployment, specifically for GitLab by deploying a hello-world app on Google Cloud Run jobs across two distinct projects. The setup comprises 3 GCP projects in total — the 2 target projects and a third, centralized project dedicated to the WIF configuration.
Read — “How OIDC can simplify authentication of GitLab CI/CD pipelines with Google Cloud”
While this tutorial will showcase WIF in action with this single, centralized setup, it’s crucial to acknowledge that in large organizations — particularly those with multiple BUs, teams, or diverse application portfolios — a single Workload Identity Pool and Provider often won’t suffice.
This leads to the powerful concept of ‘Multi-Workload Identity Federation’ setup, which involves deploying multiple, isolated Workload Identity Federation configurations. These could be tailored per BU, per app group, or even per environment, each complete with its own Workload Identity Pool and specific attribute conditions. This advanced architectural pattern enables robust segregation of duties, highly granular access control, and strict adherence to the principle of least privilege, ultimately ensuring that every distinct workload or team accesses precisely the GCP resources it requires, mediated by its uniquely and securely configured federated identity.
The steps for setting up multiple WIFs per BU, env, or app are fundamentally the same as setting up a single instance.
Create Workload Identity Federation Pool and Provider
From UI: IAM & Admin ➜ Workload Identity Federation ➜ Create Pool.
Issuer(URL) will be https://gitlab.com
or https://gitlab.
. Below are the attributes mapping and attribute conditions that I have added:
attributeCondition: attribute.project_id==""
attributeMapping:
attribute.aud: assertion.aud
attribute.ci_config_sha: assertion.ci_config_sha
attribute.namespace_id: assertion.namespace_id
attribute.namespace_path: assertion.namespace_path
attribute.project_id: assertion.project_id
attribute.project_path: assertion.project_path
attribute.ref: assertion.ref
attribute.ref_protected: 'assertion.ref_protected ? "true" : "false"'
attribute.ref_type: assertion.ref_type
attribute.sha: assertion.sha
attribute.sub: assertion.sub
google.subject: assertion.project_id + "::" + assertion.ref
attribute.aud: assertion.aud
: Maps the token’s audience — fundamental for GCP to verify the token’s intended recipient.
attribute.ci_config_sha: assertion.ci_config_sha
: The SHA256 hash of the .gitlab-ci.yml
file that triggered the pipeline. Useful for advanced auditing or enforcing policies based on specific, immutable pipeline definitions.
attribute.namespace_id: assertion.namespace_id
: The numerical ID of the GitLab group (namespace) that owns the project. Enables policies based on the top-level GitLab group.
attribute.namespace_path: assertion.namespace_path
: The full path of the GitLab group (e.g., my-org/my-team
). Excellent for defining IAM conditions based on your GitLab group hierarchy, especially in multi-team or multi-BU setups.
attribute.project_id: assertion.project_id
: The numerical ID of the GitLab project. As seen in the attributeCondition
, this is highly valuable for targeting specific projects.
attribute.project_path: assertion.project_path
: The full path of the GitLab project (e.g., my-org/my-team/my-app
). Provides granular control for IAM conditions specific to a single repository.
attribute.ref: assertion.ref
: The exact Git reference (e.g., main
, feature/new-feature
, v1.0.0
). Allows policies that differentiate based on the source branch or tag.
attribute.ref_protected: 'assertion.ref_protected ? "true" : "false"'
: Indicates whether the Git reference is a protected branch or tag in GitLab (true
or false
). Crucial for implementing stricter security policies for deployments from protected environments.
attribute.ref_type: assertion.ref_type
: The type of Git reference (e.g., branch
, tag
, commit
). Can be used to create different rules for branch deployments versus tagged releases.
attribute.sha: assertion.sha
: The full Git commit SHA that triggered the pipeline. Useful for deep auditing and traceability.
attribute.sub: assertion.sub
: The subject claim, a unique identifier for the specific GitLab CI/CD job instance. This often forms the raw federated identity if a google.subject
is not explicitly mapped.
google.subject: assertion.project_id + "::" + assertion.ref
: This creates a composite Google Cloud principal identifier. By combining the GitLab project ID and the Git reference, you get a highly specific identity (e.g., GITLAB_PROJECT_ID::BRANCH
). This allows you to write IAM policies that grant permissions not just to a GitLab project, but to that project only when deploying from a specific branch or tag.
attribute.project_id=="
: ensures that only CI/CD jobs originating from the specific GitLab project ID that you specify here will be allowed to authenticate via this Workload Identity Provider.
Create a Service Account for Impersonation and binding
When configuring WIF, choosing between centralized and per-project SA strategies for GitLab CI/CD pipelines(for impersonation) is crucial. You may either go with one of the below or both —
- May have one SA per BU — centralized in a dedicated WIF project, and then granted permissions across all relevant projects within that BU. This means for, say, 5 BUs, you’d manage 5 WIF configurations and 5 centralized SAs, streamlining management at the BU level.
- May have one SA per GCP project — would mean your number of SAs is directly proportional to the number of GCP projects. This offers fine-grained isolation but can increase administrative overhead in large, multi-project environments.
For this blog, I chose the former over the latter.
From IAM & Admin ➜ Service Accounts ➜ Create service account in WIF project. Grant this SA the necessary permissions in other projects — here I have given it Cloud Run Admin and Service Account User in deployment projects and just Viewer in WIF.
Then run the below command to grant the Workload Identity User role to a federated principal or principal set.
gcloud iam service-accounts add-iam-policy-binding gitlab-sa@middleman.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects//locations/global/workloadIdentityPools/gitlb/*"
Setup .gitlab-ci.yml file
Below is the .gitlab-ci.yml
with GCP WIF authentication job that can be stored remotely or locally, ensuring reusability at ease, better developer experience, and easy maintainability.
# .gcp-wif-auth.gitlab-ci.yml
# This template provides a job to authenticate with GCP
# using Workload Identity Federation for GitLab CI/CD..gcp-wif-auth:
stage: gcp-wif-auth
image: google/cloud-sdk:slim
# This section tells GitLab to acquire an OIDC JWT token for the job.
# The 'aud' (audience) must match what your GCP Workload Identity Provider expects.
id_tokens:
GCP_OIDC_TOKEN: # The variable name that will hold the OIDC JWT
aud: "//iam.googleapis.com/${GCP_WI_PROVIDER_PATH}"
variables:
# Full resource path of your GCP Workload Identity Provider.
# Format: projects//locations/global/workloadIdentityPools//providers/
GCP_WI_PROVIDER_PATH: ""
# Email of the GCP Service Account to impersonate in the target GCP project
GCP_SERVICE_ACCOUNT_EMAIL: ""
# Optional: Explicitly set the target GCP Project ID.
# If not set, it will be extracted from GCP_SERVICE_ACCOUNT_EMAIL.
GCP_TARGET_PROJECT_ID: ""
before_script:
- echo "--- Starting GCP Workload Identity Authentication ---"
- if [ -z "$GCP_WI_PROVIDER_PATH" ]; then echo "Error GCP_WI_PROVIDER_PATH is not set."; exit 1; fi;
- if [ -z "$GCP_SERVICE_ACCOUNT_EMAIL" ]; then echo "Error GCP_SERVICE_ACCOUNT_EMAIL is not set."; exit 1; fi;
- |
if [ -z "$GCP_TARGET_PROJECT_ID" ]; then
GCP_TARGET_PROJECT_ID=$(echo "$GCP_SERVICE_ACCOUNT_EMAIL" | awk -F'@' '{print $2}' | awk -F'.' '{print $1}')
echo "Detected GCP_TARGET_PROJECT_ID-$GCP_TARGET_PROJECT_ID from SERVICE_ACCOUNT_EMAIL"
fi
- if [ -z "$GCP_TARGET_PROJECT_ID" ]; then echo "Error GCP_TARGET_PROJECT_ID could not be determined."; exit 1; fi;
# Store the OIDC JWT in a temporary file for gcloud
- echo "${GCP_OIDC_TOKEN}" > "${CI_PROJECT_DIR}/.gitlab-oidc-jwt"
- unset GCP_OIDC_TOKEN # Unset the variable for security
# Configure gcloud to use Workload Identity Federation
# This command exchanges the GitLab OIDC JWT for a GCP federated token,
# which then impersonates the specified GCP service account.
- >
gcloud iam workload-identity-pools create-cred-config "${GCP_WI_PROVIDER_PATH}"
--service-account="${GCP_SERVICE_ACCOUNT_EMAIL}"
--output-file="${CI_PROJECT_DIR}/.gcp_temp_cred.json"
--credential-source-file="${CI_PROJECT_DIR}/.gitlab-oidc-jwt"
- gcloud auth login --cred-file="${CI_PROJECT_DIR}/.gcp_temp_cred.json"
- gcloud config set project "${GCP_TARGET_PROJECT_ID}"
- echo "--- GCP Authentication Successful! ---"
- gcloud auth list
- gcloud config list project
While the .gitlab-ci.yml
snippet might seem straightforward, there’s a powerful and secure authentication dance happening behind the scenes. At its core, GitLab is configured to automatically issue an OIDC JWT for each job run, crafted with an audience (aud
) that your GCP WIP expects, making it a verifiable credential from GitLab.
Once this OIDC JWT is obtained, the CI/CD job leverages the gcloud iam workload-identity-pools create-cred-config
command. This crucial step acts as the bridge, exchanging the GitLab-issued OIDC JWT for a short-lived GCP federated token. Critically, this federated token then allows us to impersonate a designated GCP SA, granting the GitLab job the precise permissions needed within your GCP project.
Consume the template
To consume the template in your Gitlab CI/CD Pipeline use include
keyword and pass the variable values.
For this blog, I have deployed Hello World Cloud Run jobs in two GCP projects.
# .gitlab-ci.yml
# The consumption pipelie is for demonstration purpose.
# It can be made more mature and flexible, the idea is to show
# the consumption of template.stages:
- deploy
include:
- local: 'configure-gcp-wif-for-gitlab/templates/.gcp-wif-auth.gitlab-ci.yml'
# for remote reference, use below code
# include:
# - project: 'my-group/my-project'
# file: '/templates/.gitlab-ci-template.yml'
# - project: 'my-group/my-subgroup/my-project-2'
# file:
# - '/templates/.builds.yml'
# - '/templates/.tests.yml'
variables:
CENTRALIZED_WIF_PROVIDER: "projects//locations/global/workloadIdentityPools/gitlab/providers/middleman-provider"
CENTRALIZED_SA: "gitlab-sa@middleman.iam.gserviceaccount.com"
deploy-falcon:
image: google/cloud-sdk:slim
stage: deploy
extends: .gcp-wif-auth
variables:
GCP_WI_PROVIDER_PATH: "${CENTRALIZED_WIF_PROVIDER}"
GCP_SERVICE_ACCOUNT_EMAIL: "${CENTRALIZED_SA}" # pass the dedicated SA value if you are using per-project SA strategy
GCP_TARGET_PROJECT_ID: "falcon"
script:
- echo "GCP is now configured and authenticated for deployment."
- echo "--- Deploying to Project Falcon---"
- |
gcloud run jobs deploy hello-world-falcon \
--project="falcon" \
--region="us-central1" \
--image=us-docker.pkg.dev/cloudrun/container/job:latest
deploy-vulture:
image: google/cloud-sdk:slim
stage: deploy
extends: .gcp-wif-auth
variables:
GCP_WI_PROVIDER_PATH: "${CENTRALIZED_WIF_PROVIDER}"
GCP_SERVICE_ACCOUNT_EMAIL: "${CENTRALIZED_SA}" # pass the dedicated SA value if you are using per-project SA strategy
GCP_TARGET_PROJECT_ID: "vulture"
script:
- echo "GCP is now configured and authenticated for deployment."
- echo "--- Deploying to Project Vulture ---"
- |
gcloud run jobs deploy hello-world-vulture \
--project="vulture" \
--region="us-central1" \
--image=us-docker.pkg.dev/cloudrun/container/job:latest
Once the code is committed, the pipeline will download the .gcp-wif-auth.gitlab-ci.yml
template using include:local
in the pipeline. And can also be stored remotely on a private repo, and fetched by using include:project
. The deploy-falcon
and deploy-vulture
jobs leverage the extends
that inherits the auth logic from the template instead of repeating the entire auth logic.
That’s how you set up GCP Workload Identity Federation with GitLab.
Clap if you find this blog informative.
Source Credit: https://medium.com/google-cloud/configure-gcp-workload-identity-federation-for-gitlab-c526e6eb0517?source=rss—-e52cf94d98af—4