-
Notifications
You must be signed in to change notification settings - Fork 44
refactor: switch GCP Cloud Connectors auth to AWS Workload Identity Federation #3851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Licensed to Elasticsearch B.V. under one or more contributor | ||
| // license agreements. See the NOTICE file distributed with | ||
| // this work for additional information regarding copyright | ||
| // ownership. Elasticsearch B.V. licenses this file to you under | ||
| // the Apache License, Version 2.0 (the "License"); you may | ||
| // not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| package awslib | ||
|
|
||
| import ( | ||
| "github.com/aws/aws-sdk-go-v2/aws" | ||
| "github.com/aws/aws-sdk-go-v2/credentials/stscreds" | ||
| "github.com/aws/aws-sdk-go-v2/service/sts" | ||
| ) | ||
|
|
||
| // NewWebIdentityCredentialsCache creates a credentials cache for JWT/OIDC-based authentication. | ||
| // It wraps a WebIdentityRoleProvider that assumes the specified role using the token from the file. | ||
| // The returned cache automatically refreshes credentials when they expire. | ||
| func NewWebIdentityCredentialsCache( | ||
| client *sts.Client, | ||
| roleARN string, | ||
| tokenFilePath string, | ||
| options func(*stscreds.WebIdentityRoleOptions), | ||
| ) *aws.CredentialsCache { | ||
| tokenRetriever := stscreds.IdentityTokenFile(tokenFilePath) | ||
| webIdentityProvider := stscreds.NewWebIdentityRoleProvider( | ||
| client, | ||
| roleARN, | ||
| tokenRetriever, | ||
| options, | ||
| ) | ||
| return aws.NewCredentialsCache(webIdentityProvider) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,25 +19,31 @@ package auth | |
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/aws/aws-sdk-go-v2/aws" | ||
| "github.com/aws/aws-sdk-go-v2/credentials/stscreds" | ||
| "github.com/aws/aws-sdk-go-v2/service/sts" | ||
| "golang.org/x/oauth2/google" | ||
| "golang.org/x/oauth2/google/externalaccount" | ||
| "google.golang.org/api/option" | ||
|
|
||
| "github.com/elastic/cloudbeat/internal/config" | ||
| "github.com/elastic/cloudbeat/internal/resources/providers/awslib" | ||
| ) | ||
|
|
||
| const ( | ||
| // GCP Security Token Service endpoint for token exchange | ||
| gcpSTSTokenURL = "https://sts.googleapis.com/v1/token" | ||
| // GCP IAM Credentials API endpoint for service account impersonation | ||
| gcpIAMCredentialsURL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" | ||
| // Token type for JWT-based authentication | ||
| jwtTokenType = "urn:ietf:params:oauth:token-type:jwt" | ||
| // Token type for AWS-based authentication | ||
| awsTokenType = "urn:ietf:params:aws:token-type:aws4_request" | ||
| // Default scope for GCP Cloud Platform access | ||
| gcpCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" | ||
| // Default AWS region for STS operations | ||
| defaultAWSRegion = "us-east-1" | ||
| ) | ||
|
|
||
| type GoogleAuthProvider struct{} | ||
|
|
@@ -47,23 +53,51 @@ func (p *GoogleAuthProvider) FindDefaultCredentials(ctx context.Context) (*googl | |
| return google.FindDefaultCredentials(ctx) | ||
| } | ||
|
|
||
| // FindCloudConnectorsCredentials creates GCP client options using OIDC/Web Identity token-based authentication | ||
| // with direct service account impersonation. The target Service Account must trust the OIDC provider. | ||
| func (p *GoogleAuthProvider) FindCloudConnectorsCredentials(ctx context.Context, audience string, serviceAccountEmail string) ([]option.ClientOption, error) { | ||
| jwtFilePath := os.Getenv(config.CloudConnectorsJWTPathEnvVar) | ||
| if jwtFilePath == "" { | ||
| return nil, fmt.Errorf("environment variable %s is required for cloud connectors credentials", config.CloudConnectorsJWTPathEnvVar) | ||
| // FindCloudConnectorsCredentials creates GCP client options using AWS Workload Identity Federation | ||
| // with direct service account impersonation. | ||
| // | ||
| // The authentication flow: | ||
| // 1. Reads JWT from file (ccConfig.JWTFilePath) | ||
| // 2. Assumes Elastic's AWS role using AssumeRoleWithWebIdentity | ||
| // 3. Uses AWS credentials for GCP Workload Identity Federation token exchange | ||
| // 4. Impersonates the target service account in the customer's GCP project | ||
| func (p *GoogleAuthProvider) FindCloudConnectorsCredentials(ctx context.Context, ccConfig config.CloudConnectorsConfig, audience string, serviceAccountEmail string) ([]option.ClientOption, error) { | ||
| // Validate required configuration | ||
| if ccConfig.JWTFilePath == "" { | ||
| return nil, errors.New("cloud connectors config JWTFilePath is required") | ||
| } | ||
|
|
||
| cfg := externalaccount.Config{ | ||
| Audience: audience, | ||
| SubjectTokenType: jwtTokenType, | ||
| TokenURL: gcpSTSTokenURL, | ||
| Scopes: []string{gcpCloudPlatformScope}, | ||
| CredentialSource: &externalaccount.CredentialSource{ | ||
| File: jwtFilePath, | ||
| Format: externalaccount.Format{Type: "text"}, | ||
| if ccConfig.GlobalRoleARN == "" { | ||
| return nil, errors.New("cloud connectors config GlobalRoleARN is required") | ||
| } | ||
|
|
||
| if ccConfig.ResourceID == "" { | ||
| return nil, errors.New("cloud connectors config ResourceID is required") | ||
| } | ||
|
|
||
| // Create STS client and credentials cache at initialization (like role chaining) | ||
| stsClient := sts.New(sts.Options{Region: defaultAWSRegion}) | ||
| credsCache := awslib.NewWebIdentityCredentialsCache( | ||
| stsClient, | ||
| ccConfig.GlobalRoleARN, | ||
| ccConfig.JWTFilePath, | ||
| func(o *stscreds.WebIdentityRoleOptions) { | ||
| o.RoleSessionName = ccConfig.ResourceID | ||
| }, | ||
| ) | ||
|
|
||
| // Create the AWS credentials supplier with the pre-initialized cache | ||
| credSupplier := &awsCredentialsSupplier{ | ||
| region: defaultAWSRegion, | ||
| credsCache: credsCache, | ||
| } | ||
|
|
||
| cfg := externalaccount.Config{ | ||
| Audience: audience, | ||
| SubjectTokenType: awsTokenType, | ||
| TokenURL: gcpSTSTokenURL, | ||
| Scopes: []string{gcpCloudPlatformScope}, | ||
| AwsSecurityCredentialsSupplier: credSupplier, | ||
| ServiceAccountImpersonationURL: gcpIAMCredentialsURL + serviceAccountEmail + ":generateAccessToken", | ||
| } | ||
|
|
||
|
|
@@ -74,3 +108,31 @@ func (p *GoogleAuthProvider) FindCloudConnectorsCredentials(ctx context.Context, | |
|
|
||
| return []option.ClientOption{option.WithTokenSource(tokenSource)}, nil | ||
| } | ||
|
|
||
| // awsCredentialsSupplier implements externalaccount.AwsSecurityCredentialsSupplier | ||
| // It provides cached AWS credentials to GCP for Workload Identity Federation. | ||
| // The credentials cache is initialized once and automatically refreshes when expired. | ||
| type awsCredentialsSupplier struct { | ||
| region string | ||
| credsCache *aws.CredentialsCache | ||
| } | ||
|
|
||
| // AwsRegion returns the AWS region for the credentials. | ||
| func (s *awsCredentialsSupplier) AwsRegion(_ context.Context, _ externalaccount.SupplierOptions) (string, error) { | ||
| return s.region, nil | ||
| } | ||
|
|
||
| // AwsSecurityCredentials retrieves cached AWS credentials for GCP WIF. | ||
| // The cache automatically refreshes credentials when they expire. | ||
| func (s *awsCredentialsSupplier) AwsSecurityCredentials(ctx context.Context, _ externalaccount.SupplierOptions) (*externalaccount.AwsSecurityCredentials, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks fairly similar to code that we have in: I think that this can be shared and not duplicated.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is not a lot of shared code but I took it out into a new file |
||
| creds, err := s.credsCache.Retrieve(ctx) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to retrieve AWS credentials: %w", err) | ||
| } | ||
|
|
||
| return &externalaccount.AwsSecurityCredentials{ | ||
| AccessKeyID: creds.AccessKeyID, | ||
| SecretAccessKey: creds.SecretAccessKey, | ||
| SessionToken: creds.SessionToken, | ||
| }, nil | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for required parameters 'audience' and 'serviceAccountEmail'. These parameters are used directly in the configuration without checking if they are empty strings. If either is empty, it could lead to runtime errors when constructing the GCP IAM credentials URL or configuring the external account. Consider adding validation checks similar to the other required fields.