Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ type GcpConfig struct {
GcpCallOpt GcpCallOpt `config:"call_options"`

GcpClientOpt `config:"credentials"`

CloudConnectorsConfig CloudConnectorsConfig
}

type GcpClientOpt struct {
Expand Down Expand Up @@ -204,6 +206,11 @@ func New(cfg *config.C) (*Config, error) {
c.CloudConfig.Aws.CloudConnectorsConfig = newCloudConnectorsConfig()
}

// GCP Cloud Connectors flow is indicated by service_account_email being set
if c.CloudConfig.Gcp.ServiceAccountEmail != "" {
c.CloudConfig.Gcp.CloudConnectorsConfig = newCloudConnectorsConfig()
}

return c, nil
}

Expand Down Expand Up @@ -266,13 +273,15 @@ type CloudConnectorsConfig struct {
LocalRoleARN string
GlobalRoleARN string
ResourceID string
JWTFilePath string
}

func newCloudConnectorsConfig() CloudConnectorsConfig {
return CloudConnectorsConfig{
LocalRoleARN: os.Getenv(CloudConnectorsLocalRoleEnvVar),
GlobalRoleARN: os.Getenv(CloudConnectorsGlobalRoleEnvVar),
ResourceID: os.Getenv(CloudResourceIDEnvVar),
JWTFilePath: os.Getenv(CloudConnectorsJWTPathEnvVar),
}
}

Expand Down
9 changes: 1 addition & 8 deletions internal/resources/providers/awslib/role_chaining.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,7 @@ type WebIdentityRoleStep struct {

// BuildCredentialsCache implements AWSRoleChainingStep for AssumeRoleWithWebIdentity operations.
func (s *WebIdentityRoleStep) BuildCredentialsCache(client *sts.Client) *aws.CredentialsCache {
tokenRetriever := stscreds.IdentityTokenFile(s.WebIdentityTokenFile)
webIdentityProvider := stscreds.NewWebIdentityRoleProvider(
client,
s.RoleARN,
tokenRetriever,
s.Options,
)
return aws.NewCredentialsCache(webIdentityProvider)
return NewWebIdentityCredentialsCache(client, s.RoleARN, s.WebIdentityTokenFile, s.Options)
}

// Compile-time checks to ensure types implement AWSRoleChainingStep
Expand Down
43 changes: 43 additions & 0 deletions internal/resources/providers/awslib/web_identity.go
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)
}
96 changes: 79 additions & 17 deletions internal/resources/providers/gcplib/auth/auth_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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")
}

Copy link

Copilot AI Jan 13, 2026

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.

Suggested change
if audience == "" {
return nil, fmt.Errorf("cloud connectors audience is required")
}
if serviceAccountEmail == "" {
return nil, fmt.Errorf("cloud connectors serviceAccountEmail is required")
}

Copilot uses AI. Check for mistakes.
// 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",
}

Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 web_identity.go

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
}
6 changes: 3 additions & 3 deletions internal/resources/providers/gcplib/auth/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type ConfigProviderAPI interface {

type GoogleAuthProviderAPI interface {
FindDefaultCredentials(ctx context.Context) (*google.Credentials, error)
FindCloudConnectorsCredentials(ctx context.Context, audience string, serviceAccountEmail string) ([]option.ClientOption, error)
FindCloudConnectorsCredentials(ctx context.Context, ccConfig config.CloudConnectorsConfig, audience string, serviceAccountEmail string) ([]option.ClientOption, error)
}

type ConfigProvider struct {
Expand Down Expand Up @@ -87,9 +87,9 @@ func (p *ConfigProvider) getApplicationDefaultCredentials(ctx context.Context, c
}

func (p *ConfigProvider) getCloudConnectorsCredentials(ctx context.Context, cfg config.GcpConfig, log *clog.Logger) (*GcpFactoryConfig, error) {
log.Info("creating credentials using OIDC token and service account impersonation", "provider", "GCP")
log.Info("creating credentials using AWS Workload Identity Federation and service account impersonation", "provider", "GCP")

opts, err := p.AuthProvider.FindCloudConnectorsCredentials(ctx, cfg.Audience, cfg.ServiceAccountEmail)
opts, err := p.AuthProvider.FindCloudConnectorsCredentials(ctx, cfg.CloudConnectorsConfig, cfg.Audience, cfg.ServiceAccountEmail)
if err != nil {
return nil, fmt.Errorf("failed to get cloud connectors credentials: %w", err)
}
Expand Down
34 changes: 20 additions & 14 deletions internal/resources/providers/gcplib/auth/credentials_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ func mockGoogleAuthProvider(err error) *MockGoogleAuthProviderAPI {

func mockGoogleAuthProviderWithCloudConnectors(err error) *MockGoogleAuthProviderAPI {
googleProviderAPI := &MockGoogleAuthProviderAPI{}
on := googleProviderAPI.EXPECT().FindCloudConnectorsCredentials(mock.Anything, testAudience, testServiceAccountEmail)
on := googleProviderAPI.EXPECT().FindCloudConnectorsCredentials(mock.Anything, mock.Anything, testAudience, testServiceAccountEmail)
if err == nil {
on.Return(
[]option.ClientOption{option.WithTokenSource(nil)},
Expand Down