GCP to AWS workload identity federation

built in platform, lacklustre sdk

SEAN K.H. LIAO

GCP to AWS workload identity federation

built in platform, lacklustre sdk

GCP workload, AWS resources

More workload identity. This time we're running a workload within Google Cloud Platform (GCP), specifically Google Kubernetes Engine (GKE), and we're trying to access Amazon Web Service (AWS) resources.

AWS cloud setup

On AWS the setup is called IAM identity federation, or something similar. If you look around long enough, you'll eventually realize that some providers are special and built in, like the Google provider needed for access from GCP.

All we need is a Role for other resources/policies to reference when granting permissions, and a policy on the role detailing what to allow to assume the role. To control who to allow, we can use identity token fields, though for some reason, the audience is under oaud. In terraform this looks like:

 1terraform {
 2  required_providers {
 3    aws = {
 4      source = "hashicorp/aws"
 5      version = "4.39.0"
 6    }
 7  }
 8}
 9
10resource "aws_iam_role" "r" {
11  name = "role-to-be-assumed"
12
13  assume_role_policy = jsonencode({
14    Version = "2012-10-17"
15    Statement = [{
16      Action = "sts:AssumeRoleWithWebIdentity"
17      Effect = "Allow"
18      Sid    = "r1"
19      Principal = {
20        # built in federated identity provider
21        Federated = "accounts.google.com"
22      }
23      Condition = {
24        StringLike = {
25          # sub (subject) appears to be a required condition somewhere.
26          # For service accounts, this maps to a numeric account id,
27          # which makes it harder to identify.
28          # We're just going to allow everything use the other fields.
29          "accounts.google.com:sub" = "*"
30        }
31        StringEquals = {
32          # You can provide an audience when calling generateIdToken
33          # This represents the intended recipient of the identity token.
34          "accounts.google.com:oaud" = "sts",
35          # This is the main check we want to use since it's easier to match.
36          "accounts.google.com:email" = "xcloud-iam-fed-demo@snyk-main.iam.gserviceaccount.com"
37        }
38      }
39    }]
40  })
41}

This blog post was a good reference point on the same topic.

GCP cloud setup

On the GCP side where we're running our workload, we'll want Workload Identity on our GKE cluster.

We'll also need a service account and rolebindings to associate it with the Kubernetes service account our workload runs with.

 1terraform {
 2  required_providers {
 3    google = {
 4      source  = "hashicorp/google"
 5      version = "4.43.0"
 6    }
 7  }
 8}
 9
10locals {
11  project             = "project-id"
12  k8s_namespace       = "k8s-namespace-name"
13  k8s_service_account = "k8s-service-account-name"
14}
15
16# service account that's going to be associated with the kubernetes workload
17resource "google_service_account" "s" {
18  project    = local.project
19  account_id = "gcp-service-account-name"
20}
21
22# granting the kubernetes service account permissions to use this service account
23resource "google_service_account_iam_member" "gke" {
24  service_account_id = google_service_account.s.id
25  member             = "serviceAccount:${local.project}.svc.id.goog[${local.k8s_namespace}/${local.k8s_service_account}]"
26  role               = "roles/iam.workloadIdentityUser"
27}

GCP workload setup

Our Kubernetes pod will need a serviceaccount attached, with annotations pointing to the GCP service account:

1apiVersion: v1
2kind: ServiceAccount
3metadata:
4  name: service-account-name
5  annotations:
6    iam.gke.io/gcp-service-account: gcp-service-account-name@project-id.iam.gserviceaccount.com

GCP code setup

Finally, we get to our code. The AWS SDK doesn't appear to make any special considerations for running in external clouds, So we're responsible for wiring up the GCP token provider with the AWS role credentials provider.

 1package main
 2
 3import (
 4        "context"
 5        "fmt"
 6        "net/http"
 7        "os"
 8
 9        "github.com/aws/aws-sdk-go-v2/aws"
10        "github.com/aws/aws-sdk-go-v2/config"
11        "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
12        "github.com/aws/aws-sdk-go-v2/service/s3"
13        "github.com/aws/aws-sdk-go-v2/service/sts"
14        "golang.org/x/oauth2"
15        "golang.org/x/exp/slog"
16        "google.golang.org/api/idtoken"
17)
18
19func main() {
20        lgh := slog.HandlerOptions{
21                Level: slog.DebugLevel,
22        }.NewJSONHandler(os.Stderr)
23        lg := slog.New(lgh)
24
25        err := run(lg)
26        if err != nil {
27                lg.Error("run", err)
28                os.Exit(1)
29        }
30}
31
32type GCPTokenGenerator struct {
33        ts oauth2.TokenSource
34}
35
36// GetIdentityToken implements the stscreds.IdentityTokenGenerator interface for refreshing
37// identiy tokens on demand.
38func (g *GCPTokenGenerator) GetIdentityToken() ([]byte, error) {
39        token, err := g.ts.Token()
40        if err != nil {
41                return nil, fmt.Errorf("generate gcp id token: %w", err)
42        }
43        return []byte(token.AccessToken), nil
44}
45
46func run(lg *slog.Logger) error {
47        // potential input values
48        idTokenAudience := "sts"
49        targetAWSRegion := "us-east-1"
50        targetAWSRoleARN := "arn:aws:iam::account:role/role-to-be-assumed"
51
52        // setup a GCP id token generator
53        ctx := context.Background()
54        ts, err := idtoken.NewTokenSource(ctx, idTokenAudience)
55        if err != nil {
56                return fmt.Errorf("create GCP id token generator: %w", err)
57        }
58
59        // create a base AWS config
60        cfg, err := config.LoadDefaultConfig(ctx)
61        if err != nil {
62                return fmt.Errorf("create base AWS config: %w", err)
63        }
64
65        cfg.Region = targetAWSRegion
66
67        // update the credentials to use
68        cfg.Credentials = aws.NewCredentialsCache(
69                // credentials provider that will assume the role,
70                // refreshing tokens on demand.
71                stscreds.NewWebIdentityRoleProvider(
72                        sts.NewFromConfig(cfg)
73                        targetAWSRoleARN,
74                        &GCPTokenGenerator{ts: ts},
75                ),
76        )
77
78        // Example: AWS S3 client
79        client := s3.NewFromConfig(cfg)
80
81        // use the client
82        _ = client
83        return nil
84}