GCP Workload Identity Federation
Workload identity federation in GCP allows us to exchange tokens with different Identity providers such as AWS, and Azure for short-lived access tokens to impersonate service accounts.
When we have to work with GCP resources from other cloud providers we traditionally use service account keys, which pose a security risk of leakage and have to deal with key rotation correctly to keep it secure. Workload identity federation helps us to avoid using these issues as the tokens are short-lived.
In this blog, we will see how to create a workload identity pool and invoke a GCP Cloud function from AWS lambda using Service account tokens.
AWS lambda setup
Create a new IAM role for lambda
Create a new Lambda (Nodejs) and set the newly created role as the default execution role
Setup Workload Identity Pool in GCP
Enable the below API & Service if we have not enabled it already
Identity and Access Management (IAM)
Cloud Resource Manager API
IAM Service account Credential API
Security Token Service API
Update organization policies to whitelist AWS provider and AWS account
To allow AWS as an IDP, edit the below policy in the Organisation policies under the IAM page
Select policy enforcement “Merge with parent” if we want to append or “Replace” to replace the policy
Select custom type, allow rule and add https://sts.amazonaws.com as the value
Save the policy.
To whitelist our AWS account for workload identity federation, edit the below policy
Select policy enforcement “Merge with parent” if we want to append or “Replace” to replace the policy
Select custom type, allow rule and add a 12-digit AWS account ID as the value
Now, that we have allowed AWS as IDP and whitelisted the AWS account, we can create a workload identity pool. To create, click on workload identity federation under the IAM page and click the Create pool button.
Enter the Pool Name and description, and make a note of PoolId.
Select Provider as AWS from the option
Enter Provider Name and make note of ProviderId
Enter the AWS account used in the previous step
A pool can have multiple providers as well.
Once the Pool is created, let's create a service account we will associate with this pool and add a cloud function invoke role to the SA.
To associate a service account, click on the Grant access option in the pool
Select the service account and optionally set the filter condition if you want to allow access only from specific AWS role ARN, Add the role name we created for lambda
We will be prompted with an option to download a client library config which we can use to work with the GCP client libraries.
Once this is saved, under the service account we can see the new Workload Identity User role added and the principal set to filter value added in the above step
Cloud function setup
Create a new cloud function in GCP
Under the HTTP trigger check the Requires Authentication option so that only the authenticated user will be able to invoke the function.
Code
- Create a new folder, and create an index.mjs file and add the dependencies
touch index.mjs
npm i querystring axios aws4
- Add the below code to the index.mjs file
import querystring from 'querystring';
import aws4 from 'aws4';
import axios from 'axios'
/**
* NOTE: the below values can also be found in the
* config (audience, service_account_impersonation_url) we downloaded
* after adding the service account to Pool
*/
// !! update below values
const GCP_PROJECT_ID = ''
const POOL_ID = ''
const PROVIDER_ID = ''
const SERVICE_ACCOUNT_EMAIL = ''
const targetResource = `//iam.googleapis.com/projects/${GCP_PROJECT_ID}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}`;
const cloudFunctionUrl = 'https://us-central1-workflow-id-pool.cloudfunctions.net/function-1';
/**
We create GetCallerIdentity Token and sign with aws4
Reference: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#rest
*/
function createTokenAWS() {
const params = {
Action: 'GetCallerIdentity',
Version: '2011-06-15'
};
const requestUrl = `https://sts.amazonaws.com/?${querystring.stringify(params)}`;
const request = {
host: 'sts.amazonaws.com',
path: '/?' + querystring.stringify(params),
method: 'POST',
headers: {
Host: 'sts.amazonaws.com',
'x-goog-cloud-target-resource': targetResource
}
};
aws4.sign(request)
const token = {
url: requestUrl,
method: 'POST',
headers: []
};
for (const key in request.headers) {
token.headers.push({ key, value: request.headers[key] });
}
// The token lets workload identity federation verify
// the identity without revealing the AWS secret access key.
return token;
}
// We pass the AWS caller identity token to get the STS token
async function getStsToken(token) {
try {
var data = querystring.stringify({
'audience': targetResource,
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
'requested_token_type': 'urn:ietf:params:oauth:token-type:access_token',
'scope': 'https://www.googleapis.com/auth/cloud-platform',
'subject_token_type': 'urn:ietf:params:aws:token-type:aws4_request',
'subject_token': encodeURIComponent(JSON.stringify(token))
});
var config = {
method: 'post',
url: 'https://sts.googleapis.com/v1/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
};
const response = await axios(config)
return response.data;
} catch (err) {
console.log(err)
}
}
/**
* We get the Identity token using the Ststoken
* and cloudfunction url as audience
* https://cloud.google.com/docs/authentication/get-id-token#external-idp
*/
async function getIdToken(stsToken) {
let data = JSON.stringify({
"audience": cloudFunctionUrl
});
let config = {
method: 'post',
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateIdToken`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${stsToken.access_token}`
},
data: data
};
const result = await axios.request(config);
return result.data;
}
// finally we call the cloud function url with id token
async function getRequest(token) {
let config = {
method: 'get',
url: cloudFunctionUrl,
headers: {
'Authorization': `Bearer ${token}`
}
};
const result = await axios.request(config);
return result.data
}
export const handler = async (event) => {
try {
const token = createTokenAWS();
const stsToken = await getStsToken(token);
const idToken = await getIdToken(stsToken);
const result = await getRequest(idToken.token);
const response = {
statusCode: 200,
body: JSON.stringify(result),
};
return response;
} catch (e) {
return {
statusCode: 401,
body: 'failed'
}
}
};
Note: We had to write this code for exchanging tokens as there is a bug in the GCP client SDK to generate ID tokens when using workload identity federation and the cloud function requires ID tokens to invoke.
But if we are using any other GCP resource such as cloud storage, pubsub etc, we can just use the config we downloaded as an env variable and the client library internally takes care of generating the aws token, sts token, access token etc.
For example
References:
Subscribe to my newsletter
Read articles from Kannan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kannan
Kannan
Senior Software Engineer at Zoominfo