From db5e513e6f2e6d9dd80a20292df5c0e3a1d68916 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:08:33 +0100 Subject: [PATCH] Add external_id support for AWS role assumption AWS recommends using external IDs to prevent the confused deputy problem when granting cross-account access. This adds an optional external_id configuration field that is passed to STS AssumeRole when role_arn is specified. The implementation follows the same pattern as Prometheus core (prometheus/prometheus#17171) and uses the AWS SDK v2 functional options pattern to conditionally set the external ID only when provided. Fixes #46. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- sigv4.go | 10 ++++++++- sigv4_config.go | 4 ++++ sigv4_config_test.go | 32 +++++++++++++++++++++++------ testdata/sigv4_bad_external_id.yaml | 4 ++++ testdata/sigv4_good.yaml | 1 + 5 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 testdata/sigv4_bad_external_id.yaml diff --git a/sigv4.go b/sigv4.go index e225ab5..15d984d 100644 --- a/sigv4.go +++ b/sigv4.go @@ -99,7 +99,15 @@ func NewSigV4RoundTripper(cfg *SigV4Config, next http.RoundTripper) (http.RoundT } if cfg.RoleARN != "" { - awscfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(awscfg), cfg.RoleARN) + awscfg.Credentials = stscreds.NewAssumeRoleProvider( + sts.NewFromConfig(awscfg), + cfg.RoleARN, + func(o *stscreds.AssumeRoleOptions) { + if cfg.ExternalID != "" { + o.ExternalID = aws.String(cfg.ExternalID) + } + }, + ) } serviceName := "aps" diff --git a/sigv4_config.go b/sigv4_config.go index f756151..0057ddb 100644 --- a/sigv4_config.go +++ b/sigv4_config.go @@ -28,6 +28,7 @@ type SigV4Config struct { //nolint:revive SecretKey config.Secret `yaml:"secret_key,omitempty"` Profile string `yaml:"profile,omitempty"` RoleARN string `yaml:"role_arn,omitempty"` + ExternalID string `yaml:"external_id,omitempty"` UseFIPSSTSEndpoint bool `yaml:"use_fips_sts_endpoint,omitempty"` ServiceName string `yaml:"service_name,omitempty"` } @@ -36,6 +37,9 @@ func (c *SigV4Config) Validate() error { if (c.AccessKey == "") != (c.SecretKey == "") { return fmt.Errorf("must provide a AWS SigV4 Access key and Secret Key if credentials are specified in the SigV4 config") } + if c.ExternalID != "" && c.RoleARN == "" { + return fmt.Errorf("external_id can only be used with role_arn") + } return nil } diff --git a/sigv4_config_test.go b/sigv4_config_test.go index 0d3cc15..5d057c0 100644 --- a/sigv4_config_test.go +++ b/sigv4_config_test.go @@ -49,12 +49,32 @@ func TestGoodSigV4Configs(t *testing.T) { } func TestBadSigV4Config(t *testing.T) { - filename := "testdata/sigv4_bad.yaml" - _, err := loadSigv4Config(filename) - if err == nil { - t.Fatalf("Did not receive expected error unmarshaling bad sigv4 config") + tc := []struct { + name string + filename string + expectedError string + }{ + { + name: "missing secret key", + filename: "testdata/sigv4_bad.yaml", + expectedError: "must provide a AWS SigV4 Access key and Secret Key", + }, + { + name: "external_id without role_arn", + filename: "testdata/sigv4_bad_external_id.yaml", + expectedError: "external_id can only be used with role_arn", + }, } - if !strings.Contains(err.Error(), "must provide a AWS SigV4 Access key and Secret Key") { - t.Errorf("Received unexpected error from unmarshal of %s: %s", filename, err.Error()) + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + _, err := loadSigv4Config(tt.filename) + if err == nil { + t.Fatalf("Did not receive expected error unmarshaling bad sigv4 config") + } + if !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("Received unexpected error from unmarshal of %s: %s", tt.filename, err.Error()) + } + }) } } diff --git a/testdata/sigv4_bad_external_id.yaml b/testdata/sigv4_bad_external_id.yaml new file mode 100644 index 0000000..6bb9301 --- /dev/null +++ b/testdata/sigv4_bad_external_id.yaml @@ -0,0 +1,4 @@ +region: us-east-2 +access_key: AccessKey +secret_key: SecretKey +external_id: external123 diff --git a/testdata/sigv4_good.yaml b/testdata/sigv4_good.yaml index 9a788cf..98bc443 100644 --- a/testdata/sigv4_good.yaml +++ b/testdata/sigv4_good.yaml @@ -3,5 +3,6 @@ access_key: AccessKey secret_key: SecretKey profile: profile role_arn: blah:role/arn +external_id: external123 use_fips_sts_endpoint: true service_name: test \ No newline at end of file