fbpx

Access AWS using a Google Cloud Platform native workload identity

Organizations undergoing cloud migrations and business transformations often find themselves managing IT operations in hybrid or multicloud environments. This can make it more complex to safeguard workloads, applications, and data, and to securely handle identities and permissions across Amazon Web Services (AWS), hybrid, and multicloud setups.

   <p>In this post, we show you how to assume an <a href="https://aws.amazon.com/iam/" target="_blank" rel="noopener">AWS Identity and Access Management (IAM)</a> role in your AWS accounts to securely issue temporary credentials for applications that run on the <a href="https://cloud.google.com/" target="_blank" rel="noopener">Google Cloud Platform (GCP)</a>. We also present best practices and key considerations in this authentication flow. Furthermore, this post provides references to supplementary GCP documentation that offer additional context and provide steps relevant to setup on GCP.</p> 
   <h2>Access control across security realms</h2> 
   <p>As your multicloud environment grows, managing access controls across providers becomes more complex. By implementing the right access controls from the beginning, you can help scale your cloud operations effectively without compromising security. When you deploy apps across multiple cloud providers, you should implement a homogenous and consistent authentication and authorization mechanism across both cloud environments, to help maintain a secure and cost-effective environment. In the following sections, you’ll learn how to enforce such objectives across AWS and workloads hosted on GCP, as shown in Figure 1.</p> 
   <div id="attachment_32818" class="wp-caption aligncenter"> 
    <img aria-describedby="caption-attachment-32818" src="https://infracom.com.sg/wp-content/uploads/2023/12/img1-3.png" alt="Figure 1: Authentication flow between GCP and AWS" width="780" class="size-full wp-image-32818"> 
    <p id="caption-attachment-32818" class="wp-caption-text">Figure 1: Authentication flow between GCP and AWS</p> 
   </div> 
   <h2>Prerequisites</h2> 
   <p>To follow along with this walkthrough, complete the following prerequisites.</p> 
   <ol> 
    <li><a href="https://cloud.google.com/iam/docs/service-accounts-create" target="_blank" rel="noopener">Create a service account in GCP</a>. Resources in GCP use service accounts to make API calls. When you create a GCP resource, such as a compute engine instance in GCP, a <a href="https://cloud.google.com/iam/docs/service-account-types#default" target="_blank" rel="noopener">default service account</a> gets created automatically. Although you can use this default service account in the solution described in this post, we recommend that you create a dedicated user-managed service account, because you can control what permissions to assign to the service account within GCP. <p>To learn more about best practices for service accounts, see <a href="https://cloud.google.com/iam/docs/best-practices-service-accounts" target="_blank" rel="noopener">Best practices for using service accounts</a> in the Google documentation. In this post, we use a GCP virtual machine (VM) instance for demonstration purposes. To attach service accounts to other GCP resources, see <a href="https://cloud.google.com/iam/docs/attach-service-accounts" target="_blank" rel="noopener">Attach service accounts to resources</a>.</p> </li> 
    <li id="Prerequisites_step2"><a href="https://cloud.google.com/compute/docs/instances/create-start-instance" target="_blank" rel="noopener">Create a VM instance in GCP</a> and attach the service account that you created in Step 1. Resources in GCP store their metadata information in a <a href="https://cloud.google.com/compute/docs/metadata/overview" target="_blank" rel="noopener">metadata server</a>, and you can <a href="https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature" target="_blank" rel="noopener">request an instance’s identity token from the server</a>. You will use this identity token in the authentication flow later in this post.</li> 
    <li><a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" target="_blank" rel="noopener">Install the AWS Command Line Interface (AWS CLI)</a> on the GCP VM instance that you created in Step 2.</li> 
    <li>Install <a href="https://jqlang.github.io/jq/download/" target="_blank" rel="noopener">jq</a> and <a href="https://curl.se/docs/install.html" target="_blank" rel="noopener">curl</a>.</li> 
   </ol> 
   <h2>GCP VM identity authentication flow</h2> 
   <p>Obtaining temporary AWS credentials for workloads that run on GCP is a multi-step process. In this flow, you use the identity token from the GCP compute engine <a href="https://cloud.google.com/compute/docs/metadata/overview" target="_blank" rel="noopener">metadata server</a> to call the <a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html" target="_blank" rel="noopener">AssumeRoleWithWebIdentity</a> API to request AWS temporary credentials. This flow gives your application greater flexibility to request credentials for an IAM role that you have configured with a sufficient trust policy, and the corresponding Amazon Resource Name (ARN) for the IAM role must be known to the application.</p> 
   <h3 id="define_IAM_role">Define an IAM role on AWS</h3> 
   <p>Because AWS already supports <a href="https://openid.net/developers/how-connect-works/" target="_blank" rel="noopener">OpenID Connect (OIDC)</a> federation, you can use the OIDC token provided in GCP as described in <a href="https://aws.amazon.com/blogs/security/access-aws-using-a-google-cloud-platform-native-workload-identity/#Prerequisites_step2">Step 2 of the Prerequisites</a>, and you don’t need to create a separate OIDC provider in your AWS account. Instead, to create an IAM role for OIDC federation, follow the steps in <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html" target="_blank" rel="noopener">Creating a role for web identity or OpenID Connect Federation (console)</a>. Using an OIDC principal without a condition can be overly permissive. To make sure that only the intended identity provider assumes the role, you need to provide a <span>StringEquals</span> condition in the trust policy for this IAM role. Add the condition keys <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#ck_wif-aud" target="_blank" rel="noopener">accounts.google.com:aud</a>, <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#ck_oaud" target="_blank" rel="noopener">accounts.google.com:oaud</a>, and <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#ck_wif-sub" target="_blank" rel="noopener">accounts.google.com:sub</a> to the role’s trust policy, as shown in the following.</p> 
   <div class="hide-language"> 
    <pre class="unlimited-height-code"><code class="lang-text">{
"Version": "2012-10-17",
"Statement": [
    {
        "Effect": "Allow",
        "Principal": {"Federated": "accounts.google.com"},
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {
            "StringEquals": {
                "accounts.google.com:aud": "<span></span>",
                "accounts.google.com:oaud": "<span></span>",
                "accounts.google.com:sub": "<span></span>"
            }
        }
    }
]

}

   Make sure to replace the <span></span> with your values from the <a href="https://cloud.google.com/compute/docs/instances/verifying-instance-identity#token_format" target="_blank" rel="noopener">Google ID Token</a>. The ID token issued for the service accounts has the <span>azp</span> (AUTHORIZED_PARTY) field set, so condition keys are mapped to the Google ID Token fields as follows:</p> 
   <ul> 
    <li><span>accounts.google.com:oaud</span> condition key matches the <span>aud</span> (AUDIENCE) field on the Google ID token.</li> 
    <li><span>accounts.google.com:aud</span> condition key matches the <span>azp</span> (AUTHORIZED_PARTY) field on the Google ID token.</li> 
    <li><span>accounts.google.com:sub</span> condition key matches the <span>sub</span> (SUBJECT) field on the Google ID token.</li> 
   </ul> 
   <p>For more information about the Google <span>aud</span> and <span>azp</span> fields, see the <a href="https://developers.google.com/identity/protocols/OpenIDConnect" target="_blank" rel="noopener">Google Identity Platform OpenID Connect</a> guide.</p> 
   <h3>Authentication flow</h3> 
   <p>The authentication flow for the scenario is shown in Figure 2.</p> 
   <div id="attachment_32821" class="wp-caption aligncenter"> 
    <img aria-describedby="caption-attachment-32821" src="https://infracom.com.sg/wp-content/uploads/2023/12/img2-2.png" alt="Figure 2: Detailed authentication flow with AssumeRoleWithWebIdentity API" width="780" class="size-full wp-image-32821" /> 
    <p id="caption-attachment-32821" class="wp-caption-text">Figure 2: Detailed authentication flow with AssumeRoleWithWebIdentity API</p> 
   </div> 
   <p>The authentication flow has the following steps:</p> 
   <ol> 
    <li>On AWS, you <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html" target="_blank" rel="noopener">can source external credentials</a> by configuring the <span>credential_process</span> setting in the <span>config</span> file. For the syntax and operating system requirements, see <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html" target="_blank" rel="noopener">Source credentials with an external process</a>. For this post, we have created a custom profile <span>TeamA-S3ReadOnlyAccess</span> as follows in the <span>config</span> file: 
     <div class="hide-language"> 
      <pre class="unlimited-height-code"><code class="lang-text">[profile TeamA-S3ReadOnlyAccess]

credential_process = /opt/bin/credentials.sh

To use different settings, you can create and reference additional profiles.

  • Specify a program or a script that credential_process will invoke. For this post, credential_process invokes the script /opt/bin/credentials.sh which has the following code. Make sure to replace with your own account ID.
    #!/bin/bash
    
    AUDIENCE="dev-aws-account-teama"
    ROLE_ARN="arn:aws:iam:::role/RoleForAccessFromGCPTeamA"
    
    jwt_token=$(curl -sH "Metadata-Flavor: Google" "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=${AUDIENCE}&format=full&licenses=FALSE")
    
    jwt_sub=$(jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$jwt_token" | jq -r '.sub')
    
    credentials=$(aws sts assume-role-with-web-identity --role-arn $ROLE_ARN --role-session-name $jwt_sub --web-identity-token $jwt_token | jq '.Credentials' | jq '.Version=1')
    
    echo $credentials

    The script performs the following steps:

    1. Google generates a new unique instance identity token in the JSON Web Token (JWT) format.
      jwt_token=$(curl -sH "Metadata-Flavor: Google" "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=${AUDIENCE}&format=full&licenses=FALSE")

      The payload of the token includes several details about the instance and the audience URI, as shown in the following.

      {
         "iss": "[TOKEN_ISSUER]",
         "iat": [ISSUED_TIME],
         "exp": [EXPIRED_TIME],
         "aud": "[AUDIENCE]",
         "sub": "[SUBJECT]",
         "azp": "[AUTHORIZED_PARTY]",
         "google": {
          "compute_engine": {
            "project_id": "[PROJECT_ID]",
            "project_number": [PROJECT_NUMBER],
            "zone": "[ZONE]",
            "instance_id": "[INSTANCE_ID]",
            "instance_name": "[INSTANCE_NAME]",
            "instance_creation_timestamp": [CREATION_TIMESTAMP],
            "instance_confidentiality": [INSTANCE_CONFIDENTIALITY],
            "license_id": [
              "[LICENSE_1]",
                ...
              "[LICENSE_N]"
            ]
          }
        }
      }

      The IAM trust policy uses the aud (AUDIENCE), azp (AUTHORIZED_PARTY) and sub (SUBJECT) values from the JWT token to help ensure that the IAM role defined in the section Define an IAM role in AWS can be assumed only by the intended GCP service account.

    2. The script invokes the AssumeRoleWithWebIdentity API call, passing in the identity token from the previous step and specifying which IAM role to assume. The script uses the Identity subject claim as the session name, which can facilitate auditing or forensic operations on this AssumeRoleWithWebIdentity API call. AWS verifies the authenticity of the token before returning temporary credentials. In addition, you can verify the token in your credential program by using the process described at Obtaining the instance identity token.

      The script then returns the temporary credentials to the credential_process as the JSON output on STDOUT; we used jq to parse the output in the desired JSON format.

      jwt_sub=$(jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$jwt_token" | jq -r '.sub')
      
      credentials=$(aws sts assume-role-with-web-identity --role-arn $ROLE_ARN --role-session-name $jwt_sub --web-identity-token $jwt_token | jq '.Credentials' | jq '.Version=1')
      
      echo $credentials

    The following is an example of temporary credentials returned by the credential_process script:

    {
      "Version": 1,
      "AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
      "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
      "SessionToken": "FwoGZXIvYXdzEBUaDOSY+1zJwXi29+/reyLSASRJwSogY/Kx7NomtkCoSJyipWuu6sbDIwFEYtZqg9knuQQyJa9fP68/LCv4jH/efuo1WbMpjh4RZpbVCOQx/zggZTyk2H5sFvpVRUoCO4dc7eqftMhdKtcq67vAUljmcDkC9l0Fei5tJBvVpQ7jzsYeduX/5VM6uReJaSMeOXnIJnQZce6PI3GBiLfaX7Co4o216oS8yLNusTK1rrrwrY2g5e3Zuh1oXp/Q8niFy2FSLN62QHfniDWGO8rCEV9ZnZX0xc4ZN68wBc1N24wKgT+xfCjamcCnBjJYHI2rEtJdkE6bRQc2WAUtccsQk5u83vWae+SpB9ycE/dzfXurqcjCP0urAp4k9aFZFsRIGfLAI1cOABX6CzF30qrcEBnEXAMPLESESSIONTOKEN==",
      "Expiration": "2023-08-31T04:45:30Z"
    }

  • Note that AWS SDKs store the returned AWS credentials in memory when they call credential_process. AWS SDKs keep track of the credential expiration and generate new AWS session credentials through the credential process. In contrast, the AWS CLI doesn’t cache external process credentials; instead, the AWS CLI calls the credential_process for every CLI request, which creates a new role session and could result in slight delays when you run commands.

    Test access in the AWS CLI

    After you configure the config file for the credential_process, verify your setup by running the following command.

    aws sts get-caller-identity --profile TeamA-S3ReadOnlyAccess

    The output will look similar to the following.

    {
       "UserId":"AIDACKCEVSQ6C2EXAMPLE:[Identity subject claim]",
       "Account":"111122223333",
       "Arn":"arn:aws:iam::111122223333:role/RoleForAccessFromGCPTeamA:[Identity subject claim]"
    }

    Amazon CloudTrail logs the AssumeRoleWithWebIdentity API call, as shown in Figure 3. The log captures the audience in the identity token as well as the IAM role that is being assumed. It also captures the session name with a reference to the Identity subject claim, which can help simplify auditing or forensic operations on this AssumeRoleWithWebIdentity API call.

    Figure 3: CloudTrail event for AssumeRoleWithWebIdentity API call from GCP VM

    Figure 3: CloudTrail event for AssumeRoleWithWebIdentity API call from GCP VM

    Test access in the AWS SDK

    The next step is to test access in the AWS SDK. The following Python program shows how you can refer to the custom profile configured for the credential process.

    import boto3
    
    session = boto3.Session(profile_name='TeamA-S3ReadOnlyAccess')
    client = session.client('s3')
    
    response = client.list_buckets()
    for _bucket in response['Buckets']:
        print(_bucket['Name'])

    Before you run this program, run pip install boto3. Create an IAM role that has the AmazonS3ReadOnlyAccess policy attached to it. This program prints the names of the existing S3 buckets in your account. For example, if your AWS account has two S3 buckets named DOC-EXAMPLE-BUCKET1 and DOC-EXAMPLE-BUCKET2, then the output of the preceding program shows the following:

    DOC-EXAMPLE-BUCKET1
    DOC-EXAMPLE-BUCKET2

    If you don’t have an existing S3 bucket, then create an S3 bucket before you run the preceding program.

    The list_bucket API call is also logged in CloudTrail, capturing the identity and source of the calling application, as shown in Figure 4.

    Figure 4: CloudTrail event for S3 API call made with federated identity session

    Figure 4: CloudTrail event for S3 API call made with federated identity session

    Clean up

    If you don’t need to further use the resources that you created for this walkthrough, delete them to avoid future charges for the deployed resources:

    • Delete the VM instance and service account created in GCP.
    • Delete the resources that you provisioned on AWS to test the solution.

    Conclusion

    In this post, you learned how to exchange the identity token of a virtual machine running on a GCP compute engine to assume a role on AWS, so that you can seamlessly and securely access AWS resources from GCP hosted workloads.

    We walked you through the steps required to set up the credential process and shared best practices to consider in this authentication flow. You can also apply the same pattern to workloads deployed on GCP functions or Google Kubernetes Engine (GKE) when they request access to AWS resources.

     
    If you have feedback about this post, submit comments in the Comments section below. If you have questions about this post, contact AWS Support.

    Want more AWS Security news? Follow us on Twitter.

       <!-- '"` -->