Part 2. Token exchange from Azure to GCP
Λ\: Clément Bosc
Posted on February 14, 2023
In the previous article Part 1. Access token vs ID token, we saw why going multi-cloud is a security challenge and why we need a more sustainable solution than exporting and storing sensitive secrets. We also saw what is an access token, what is an ID token and the difference between them. Keep this information in mind, we will need it for the following!
Now let’s see in details the technical implementation for exchanging securely tokens from Azure to Google Cloud, to be able to query Google APIs from Azure Cloud without having to generate a Google service account JSON key.
The big picture
To request a service or API hosted on GCP, you need a GCP access token (or ID token if your service is Cloud Run). But all you have at this point is a token delivered by Azure, related to your Azure identity. That’s why you need to exchange it for a GCP token.
To exchange an Azure access token for a Google access token you need to configure a GCP service called Workload Identity Federation. This service allows you to configure external providers (Azure, AWS, GitLab, anything that uses OIDC and JWT tokens) and map entities from theses providers to Service Accounts in GCP. This will allow external entities to impersonate the GCP service account, that's to say inherit all the permissions the service account has on the platform.
The process goes in 3 steps:
- Generate an Azure Active Directory (AAD) access token for an App registration (more on them bellow), either using the
client_id
andclient_secret
or via the Metadata Server. - Exchange the Azure access token with a short-lived access token from Google’s Security Token Service API (STS).
- Exchange the STS access token with a Service Account’s access token and use this one to query Google APIs !
1. Azure App registration creation and token generation
In Azure world, the App registration is the identity of a service (or app). It’s kind of like a Service Account if you are coming from the Google Cloud world. You must first create an App registration in Azure Active Directory.
In Azure boundaries you can generate an access token on behalf of the application, either via the Authorization Server with the client_id
and client_secret
or via the Metadata Server.
a. Generate an access token with Authorization server and client_id and secret_id
In my case my project is in Azure China so the AAD Authority (host) is [https://login.partner.microsoftonline.cn](https://login.partner.microsoftonline.cn)
but for you it’s probably https://login.microsoftonline.com/
(Azure Global)
curl --location --request POST 'https://login.partner.microsoftonline.cn/TENANT_ID/oauth2/v2.0/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=APPLICATION_ID' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_secret=CLIENT_SECRET' \
--data-urlencode 'scope=.default' # or anything else that you would like for your token
# Response
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "eyJ0eXAiOiJK*********" # AZURE_TOKEN
}
b. Generate an access token with the Azure Instance Metadata Service
In the Cloud world there is a reserved magic IP “169.254.169.254” which is used to fetch user or service information when your workload is running in the Cloud compute context: it’s the Metadata server. You can request this service from inside an Azure VM to generate a token for the managed identity attached to the VM, without having the secret ! 🪄
curl GET 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=AZURE_APP_ID/.default'
--header 'Metadata: true'
# Response
{
"access_token": "eyJ0eXA********", # AZURE_TOKEN
"expires_in": 3599,
"token_type": "Bearer"
}
More info in Azure Metadata Server to acquire an access token here.
2. Setup Workload Identity Federation : Pool and Azure Provider
First, let’s create a Workload Identity Pool on GCP, you only need a name and ID for this one. You can have many providers by pool, and a provider is limited to one tenant. So if you are in a multi-tenant Azure pattern, you might need a pool for each of them.
To create the Azure provider, select type OpenId Connect (OIDC) : you will need a name, an ID and an issuer.
Let’s remind what we learned from the previous article, the issuer is the trusted entity which sign the original access token and it can be easily retrieved by decoding the JWT token. As usual, let’s go to jwt.io with you AZURE_ACCESS_TOKEN and find out the issuer.
In my case it’s https://sts.chinacloudapi.cn/TENANT_ID because my project is on Azure China, but for you it will probably look more like https://sts.windows.net/TENANT_ID (Azure Global).
You are then asked to setup the attribute mapping : this is used later to allow a subset of entities to impersonate the target GCP service account. You must at least set the google.subject
mapping and once again, let’s look for our subject in the decoded JWT payload, at sub
attribute. This is a unique ID for your Azure application. You might want to add other JWT mapping at your convenience.
gcloud iam workload-identity-pools create POOL_ID \
--location="global" \
--display-name=POOL_DISPLAY_NAME
gcloud iam workload-identity-pools providers create-oidc PROVIDER_ID \
--location="global" \
--workload-identity-pool=POOL_ID \
--display-name=PROVIDER_DISPLAY_NAME \
--issuer-uri="https://sts.chinacloudapi.cn/TENANT_ID" \
--allowed-audiences=AZURE_APP_ID/.default
3. Exchange your Azure token for a GCP token with STS
In this step, you request a short-lived access token to Google Security Token Service API in exchange for your Azure App registration access token. A simple curl call would do the job as shown below. Make sure to specify the previously created Workload Identity Provider as audience. grantType
, requestedTokenType
and subjectTokenType
are fixed by convention.
The result is an STS token, representing the principalSet
of you Workload Identity Pool.
curl POST 'https://sts.googleapis.com/v1/token' \
--header 'Content-Type: application/json' \
--data-raw '{
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/WORKLOAD_IDENTITY_POOL/providers/AZURE_PROVIDER",
"scope": "https://www.googleapis.com/auth/cloud-platform",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"subjectToken": "AZURE_TOKEN",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt"
}'
# Response
{
"access_token": "ya29.d.b0Aaekm1K9f******", # STS_ACCESS_TOKEN
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3587
}
4. A new GCP principal : the principalSet
Before jumping to the last exchange operation, let’s get back to fundamentals.
In GCP, a principal is a entity that can be allowed via IAM to perform certain actions. If you never used Workload Identity Federation you are probably convinced that there are only 3 kinds of principal : user, group & serviceAccount. But with Workload Identity Federation, Google introduced a fourth : the principalSet. The principalSet is the principal identity for a pool, but it can only be used with the role Workload Identity User to impersonate a real Service Account. Moreover, the particularity of this principal, it’s that the corresponding identity is dynamic, based on a pattern : you can apply filter base on the source JWT attributes that where previously mapped !
- To limit the impersonation permission on a specific subject you can set the permission on
principal://iam.googleapis.com/projects/**PROJECT_NUMBER**/locations/global/workloadIdentityPools/**POOL_ID**/subject/**SUBJECT_ATTRIBUTE_VALUE**
- But you can use any custom attribute by using
principalSet://iam.googleapis.com/projects/**PROJECT_NUMBER**/locations/global/workloadIdentityPools/**POOL_ID**/attribute.**ATTRIBUTE_NAME**/**ATTRIBUTE_VALUE**
Here are all the possible patterns:
5. Give workload identity principal access to target service account and exchange final token
Now that you have your STS access token you are nearly to the end !
The Workload Identity Principal must be authorized by GCP IAM to impersonate your final, target service account. To do so you need to add the Workload Identity User role, at service account level, to the principal represented by principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/subject/AZURE_APP_SUBJECT
.
gcloud iam service-accounts add-iam-policy-binding \
TARGET_SERVICE_ACCOUNT_EMAIL \
--member principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/subject/AZURE_APP_SUBJECT \
--role roles/iam.workloadIdentityUser
Exchange STS for a final access token
Here you are, you can now finally impersonate the target service account to access real Google Cloud APIs. Just pass the STS access token as Bearer token of your HTTP request against IAM access token generation endpoint (or ID token depending of your use case) 🙂
curl --location --request POST 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/TARGET_SERVICE_ACCOUNT_EMAIL:generateAccessToken' \
--header 'Authorization: Bearer STS_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{
"scope": [
"https://www.googleapis.com/auth/cloud-platform"
]
}'
# Response
{
"accessToken": "ya29.c.b0Aaekm1Izvf********", # GCP_ACCESS_TOKEN
"expireTime": "2023-02-12T20:36:27Z"
}
The resulting access token can be used to do anything that the TARGET_SERVICE_ACCOUNT can, for example running BigQuery queries 📊
curl POST 'https://bigquery.googleapis.com/bigquery/v2/projects/PROJECT_ID/queries' \
--header 'Authorization: Bearer GCP_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{
"query": "SELECT CURRENT_TIMESTAMP()",
"useLegacySql": false,
"location": "EU"
}'
We saw how to securely exchange an Azure App registration access token by impersonating a GCP service account and access Google APIs securely from other Clouds :)
Let’s see how to do the reverse operation in a next article !
Posted on February 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.