From 8afb1fc14696304ce92ae40f370c91f1c25193e7 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 19 Apr 2024 16:24:23 -0400 Subject: [PATCH] common: use regional STS on non-default regions STS is the service used by AWS for emitting authentication tokens for API clients. This comes in two variants: v1 (global) and v2 (regional). As of today (2024-04-24), the default for the Go SDK is "legacy", i.e. if the connection is used to communicate with a non-default region it will use a regional endpoint, otherwise it'll use the global endpoint. Builds are generally not affected by operations like these as the SDK will pick the right type of endpoint for that, but problems may arise later, when copying AMIs for example, as they will need tokens compatible with both the source and destination regions. This means that if the build was performed in a default region, then copied to a non-default region, we'll have gotten a v1 (global) token, which will be rejected by the target region, causing the build to fail. This is already fixable by user-action, through either a setting in their AWS config file, or through an environment variable, but this may come as a surprise if users aren't aware of that pitfall. Therefore, this commit attempts to heuristically determine if an action may fail in the process, and enable regional endpoints for the EC2 session we create during a build. Note: the volume builder and the post-processor are not affected by this, as they only work within one region at a time, so the SDK will choose the right type of endpoint/token for the action, and no cross-region action will be done. --- builder/chroot/builder.go | 17 ++++++++ builder/common/access_config.go | 1 + builder/common/ami_config.go | 77 +++++++++++++++++++++++++++++++++ builder/ebs/builder.go | 16 +++++++ builder/ebs/builder_acc_test.go | 6 +-- builder/ebssurrogate/builder.go | 16 +++++++ builder/instance/builder.go | 17 ++++++++ 7 files changed, 147 insertions(+), 3 deletions(-) diff --git a/builder/chroot/builder.go b/builder/chroot/builder.go index ba8150eae..7b8f73648 100644 --- a/builder/chroot/builder.go +++ b/builder/chroot/builder.go @@ -16,6 +16,7 @@ import ( "fmt" "runtime" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/hcl/v2/hcldec" awscommon "github.com/hashicorp/packer-plugin-amazon/builder/common" @@ -420,6 +421,22 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) if err != nil { return nil, err } + + // If the AMI copies to a region that is not part of the default regions, + // we will switch to using regional STS endpoints for authentication, as + // these non-default regions only support STSv2 tokens, and by default + // we reach global endpoints which provide STSv1 tokens, leading to + // errors when copying to those non-default regional endpoints. + nonDefaultRegions := b.config.AMIConfig.NonDefaultRegions(&b.config.AccessConfig) + if nonDefaultRegions != nil && + session.Config.STSRegionalEndpoint == endpoints.LegacySTSEndpoint { + ui.Say(fmt.Sprintf("The configuration uses non-default regions: %v\n"+ + "This will likely fail when contacting those endpoints.\n"+ + "To make this message disappear, AWS_STS_REGIONAL_ENDPOINTS=regional "+ + "should be set in your environment", nonDefaultRegions)) + session.Config.STSRegionalEndpoint = endpoints.RegionalSTSEndpoint + } + ec2conn := ec2.New(session) wrappedCommand := func(command string) (string, error) { diff --git a/builder/common/access_config.go b/builder/common/access_config.go index 100aaa20d..825fde2a9 100644 --- a/builder/common/access_config.go +++ b/builder/common/access_config.go @@ -277,6 +277,7 @@ func (c *AccessConfig) Session() (*session.Session, error) { return nil, err } log.Printf("Found region %s", *sess.Config.Region) + c.session = sess cp, err := c.session.Config.Credentials.Get() diff --git a/builder/common/ami_config.go b/builder/common/ami_config.go index ea010b3d1..5714d5534 100644 --- a/builder/common/ami_config.go +++ b/builder/common/ami_config.go @@ -302,6 +302,83 @@ func (c *AMIConfig) prepareRegions(accessConfig *AccessConfig) (errs []error) { return errs } +func (c AMIConfig) getRegions() []string { + regions := map[string]struct{}{} + + for _, region := range c.AMIRegions { + regions[region] = struct{}{} + } + + for region := range c.AMIRegionKMSKeyIDs { + regions[region] = struct{}{} + } + + ret := make([]string, 0, len(regions)) + for region := range regions { + ret = append(ret, region) + } + + return ret +} + +// NonDefaultRegions attempts to detect usage of non-default regions. +// +// If a non-default region is defined, the build should use regional STS +// endpoints instead of the global one, as these do not support the type of +// token required by those endpoints. +// +// So this is meant to be called by builders/post-processors that need to +// interact with AWS in those regions, so they can change the value of the +// Session.STSEndpoint +func (c *AMIConfig) NonDefaultRegions(accessConfig *AccessConfig) []string { + var retRegions []string + + // If the default (build) region is already a non-default one, we will + // automatically use STSv2 tokens, even in 'legacy' (default) mode. Therefore + // in such a case, we can immediately return as we won't have a problem + // afterwards when it is time to copy the AMI to other regions. + if IsNonDefaultRegion(accessConfig.RawRegion) { + return retRegions + } + + regions := c.getRegions() + for _, reg := range regions { + if IsNonDefaultRegion(reg) { + retRegions = append(retRegions, reg) + } + } + + return retRegions +} + +// Return true if the `region` is not a default one. +// +// Any region that is not one of those that are defined here will require opt-in +// and STSv2, hence why we try to figure it out here. +func IsNonDefaultRegion(region string) bool { + switch region { + case "ap-south-1", + "eu-north-1", + "eu-west-3", + "eu-west-2", + "eu-west-1", + "ap-northeast-3", + "ap-northeast-2", + "ap-northeast-1", + "ca-central-1", + "sa-east-1", + "ap-southeast-1", + "ap-southeast-2", + "eu-central-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2": + return false + } + return true +} + // See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html func ValidateKmsKey(kmsKey string) (valid bool) { //Pattern for matching KMS Key ID for multi-region keys diff --git a/builder/ebs/builder.go b/builder/ebs/builder.go index b6c1da1b3..965300656 100644 --- a/builder/ebs/builder.go +++ b/builder/ebs/builder.go @@ -16,6 +16,7 @@ import ( "fmt" "time" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/hcl/v2/hcldec" @@ -208,6 +209,21 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) return nil, err } + // If the AMI copies to a region that is not part of the default regions, + // we will switch to using regional STS endpoints for authentication, as + // these non-default regions only support STSv2 tokens, and by default + // we reach global endpoints which provide STSv1 tokens, leading to + // errors when copying to those non-default regional endpoints. + nonDefaultRegions := b.config.AMIConfig.NonDefaultRegions(&b.config.AccessConfig) + if nonDefaultRegions != nil && + session.Config.STSRegionalEndpoint == endpoints.LegacySTSEndpoint { + ui.Say(fmt.Sprintf("The configuration uses non-default regions: %v\n"+ + "This will likely fail when contacting those endpoints.\n"+ + "To make this message disappear, AWS_STS_REGIONAL_ENDPOINTS=regional "+ + "should be set in your environment", nonDefaultRegions)) + session.Config.STSRegionalEndpoint = endpoints.RegionalSTSEndpoint + } + ec2conn := ec2.New(session) iam := iam.New(session) // Setup the state bag and initial state for the steps diff --git a/builder/ebs/builder_acc_test.go b/builder/ebs/builder_acc_test.go index afc70d853..5514ee844 100644 --- a/builder/ebs/builder_acc_test.go +++ b/builder/ebs/builder_acc_test.go @@ -64,7 +64,7 @@ func TestAccBuilder_EbsRegionCopy(t *testing.T) { } _ = ami.CleanUpAmi() ami = amazon_acc.AMIHelper{ - Region: "us-west-2", + Region: "ca-west-1", Name: amiName, } _ = ami.CleanUpAmi() @@ -76,7 +76,7 @@ func TestAccBuilder_EbsRegionCopy(t *testing.T) { return fmt.Errorf("Bad exit code. Logfile: %s", logfile) } } - return checkRegionCopy(amiName, []string{"us-east-1", "us-west-2"}) + return checkRegionCopy(amiName, []string{"us-east-1", "ca-west-1"}) }, } acctest.TestPlugin(t, testCase) @@ -1546,7 +1546,7 @@ const testBuilderAccRegionCopy = ` "source_ami": "ami-76b2a71e", "ssh_username": "ubuntu", "ami_name": "%s", - "ami_regions": ["us-east-1", "us-west-2"] + "ami_regions": ["us-east-1", "ca-west-1"] }] } ` diff --git a/builder/ebssurrogate/builder.go b/builder/ebssurrogate/builder.go index c35945def..6544f71be 100644 --- a/builder/ebssurrogate/builder.go +++ b/builder/ebssurrogate/builder.go @@ -13,6 +13,7 @@ import ( "errors" "fmt" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/hcl/v2/hcldec" @@ -232,6 +233,21 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) return nil, err } + // If the AMI copies to a region that is not part of the default regions, + // we will switch to using regional STS endpoints for authentication, as + // these non-default regions only support STSv2 tokens, and by default + // we reach global endpoints which provide STSv1 tokens, leading to + // errors when copying to those non-default regional endpoints. + nonDefaultRegions := b.config.AMIConfig.NonDefaultRegions(&b.config.AccessConfig) + if nonDefaultRegions != nil && + session.Config.STSRegionalEndpoint == endpoints.LegacySTSEndpoint { + ui.Say(fmt.Sprintf("The configuration uses non-default regions: %v\n"+ + "This will likely fail when contacting those endpoints.\n"+ + "To make this message disappear, AWS_STS_REGIONAL_ENDPOINTS=regional "+ + "should be set in your environment", nonDefaultRegions)) + session.Config.STSRegionalEndpoint = endpoints.RegionalSTSEndpoint + } + ec2conn := ec2.New(session) iam := iam.New(session) diff --git a/builder/instance/builder.go b/builder/instance/builder.go index a018cd28f..e199d9e03 100644 --- a/builder/instance/builder.go +++ b/builder/instance/builder.go @@ -15,6 +15,7 @@ import ( "os" "strings" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/hcl/v2/hcldec" @@ -258,6 +259,22 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) if err != nil { return nil, err } + + // If the AMI copies to a region that is not part of the default regions, + // we will switch to using regional STS endpoints for authentication, as + // these non-default regions only support STSv2 tokens, and by default + // we reach global endpoints which provide STSv1 tokens, leading to + // errors when copying to those non-default regional endpoints. + nonDefaultRegions := b.config.AMIConfig.NonDefaultRegions(&b.config.AccessConfig) + if nonDefaultRegions != nil && + session.Config.STSRegionalEndpoint == endpoints.LegacySTSEndpoint { + ui.Say(fmt.Sprintf("The configuration uses non-default regions: %v\n"+ + "This will likely fail when contacting those endpoints.\n"+ + "To make this message disappear, AWS_STS_REGIONAL_ENDPOINTS=regional "+ + "should be set in your environment", nonDefaultRegions)) + session.Config.STSRegionalEndpoint = endpoints.RegionalSTSEndpoint + } + ec2conn := ec2.New(session) iam := iam.New(session)