From 45fbf23bd1962761abb66afa9ba25b2f162c2b6a Mon Sep 17 00:00:00 2001 From: Micah Hausler Date: Fri, 30 Aug 2024 16:00:23 -0500 Subject: [PATCH] Abstract region discovery With the deprecation of AWS SDK Go, and the removal of the endpoints package, we need a new way to discover valid regions. This change preserves the previous behavior, while adding a new method that calls accounts:ListRegions, or looking up endpoints from a file. --- cmd/aws-iam-authenticator/root.go | 47 +++++- cmd/aws-iam-authenticator/server.go | 16 +- cmd/aws-iam-authenticator/verify.go | 60 ++++++-- go.mod | 16 +- go.sum | 28 ++++ pkg/config/types.go | 14 ++ pkg/regions/api_discoverer.go | 74 +++++++++ pkg/regions/api_discoverer_test.go | 186 +++++++++++++++++++++++ pkg/regions/endpoint_verifier.go | 90 +++++++++++ pkg/regions/endpoint_verifier_test.go | 42 ++++++ pkg/regions/file_discoverer.go | 42 ++++++ pkg/regions/file_discoverer_test.go | 89 +++++++++++ pkg/regions/sdk_v1_discoverer.go | 92 ++++++++++++ pkg/regions/sdk_v1_discoverer_test.go | 107 +++++++++++++ pkg/server/server.go | 37 ++++- pkg/server/types.go | 8 +- pkg/token/token.go | 85 +---------- pkg/token/token_test.go | 209 +++++++------------------- tests/integration/go.mod | 14 ++ tests/integration/go.sum | 30 ++++ 20 files changed, 1018 insertions(+), 268 deletions(-) create mode 100644 pkg/regions/api_discoverer.go create mode 100644 pkg/regions/api_discoverer_test.go create mode 100644 pkg/regions/endpoint_verifier.go create mode 100644 pkg/regions/endpoint_verifier_test.go create mode 100644 pkg/regions/file_discoverer.go create mode 100644 pkg/regions/file_discoverer_test.go create mode 100644 pkg/regions/sdk_v1_discoverer.go create mode 100644 pkg/regions/sdk_v1_discoverer_test.go diff --git a/cmd/aws-iam-authenticator/root.go b/cmd/aws-iam-authenticator/root.go index f234e61e4..66fcd906f 100644 --- a/cmd/aws-iam-authenticator/root.go +++ b/cmd/aws-iam-authenticator/root.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "context" "errors" "fmt" "os" @@ -24,7 +25,10 @@ import ( "sigs.k8s.io/aws-iam-authenticator/pkg/config" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper" - "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" + aws_config "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -111,6 +115,9 @@ func getConfig() (config.Config, error) { DynamicFileUserIDStrict: viper.GetBool("server.dynamicfileUserIDStrict"), //DynamicBackendModePath: the file path containing the backend mode DynamicBackendModePath: viper.GetString("server.dynamicBackendModePath"), + + EndpointValidationMode: viper.GetString("server.endpointValidationMode"), + EndpointValidationFile: viper.GetString("server.endpointValidationFile"), } if err := viper.UnmarshalKey("server.mapRoles", &cfg.RoleMappings); err != nil { return cfg, fmt.Errorf("invalid server role mappings: %v", err) @@ -143,14 +150,38 @@ func getConfig() (config.Config, error) { return cfg, errors.New("cluster ID cannot be empty") } - partitionKeys := []string{} - partitionMap := map[string]endpoints.Partition{} - for _, p := range endpoints.DefaultPartitions() { - partitionMap[p.ID()] = p - partitionKeys = append(partitionKeys, p.ID()) + // We now discover PartitionID via the STS GCI response + if cfg.PartitionID != "" { + logrus.Warn("partition ID via config is deprecated") + } + + awscfg, err := aws_config.LoadDefaultConfig(context.TODO()) + if err != nil { + logrus.WithError(err).Fatal("unable to create AWS config") + } + client := sts.NewFromConfig(awscfg) + identity, err := client.GetCallerIdentity(context.TODO(), nil) + if err != nil { + logrus.WithError(err).Fatal("unable to call sts:GetCallerIdentity") } - if _, ok := partitionMap[cfg.PartitionID]; !ok { - return cfg, errors.New("Invalid partition") + serverArn, err := arn.Parse(aws.ToString(identity.Arn)) + if err != nil { + logrus.WithError(err).WithField("arn", aws.ToString(identity.Arn)).Fatal("unable to parse sts:GetCallerIdentity ARN response") + } + cfg.PartitionID = serverArn.Partition + + switch cfg.EndpointValidationMode { + case "Legacy", "": + cfg.EndpointValidationMode = "Legacy" + case "API": + case "File": + if _, err := os.Stat(cfg.EndpointValidationFile); err != nil { + logrus.WithField("path", cfg.EndpointValidationFile).Fatal("Error calling stat() on server.endpointValidationFile") + } + default: + // Defensive check here in case the cmd validation fails + logrus.WithField("server.endpointValidationMode", cfg.EndpointValidationMode).Fatalf( + `invalid EndpointValidationMode, must be one of "Legacy", "API", or "File" (empty defaults to "Legacy")`) } // DynamicFile BackendMode and DynamicFilePath are mutually inclusive. diff --git a/cmd/aws-iam-authenticator/server.go b/cmd/aws-iam-authenticator/server.go index 91192a7da..eb995fcd6 100644 --- a/cmd/aws-iam-authenticator/server.go +++ b/cmd/aws-iam-authenticator/server.go @@ -29,7 +29,6 @@ import ( "sigs.k8s.io/aws-iam-authenticator/pkg/metrics" "sigs.k8s.io/aws-iam-authenticator/pkg/server" - "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -66,13 +65,16 @@ var serverCmd = &cobra.Command{ } func init() { - partitionKeys := []string{} - for _, p := range endpoints.DefaultPartitions() { - partitionKeys = append(partitionKeys, p.ID()) + partitionKeys := []string{ + "aws", + "aws-cn", + "aws-us-gov", + "aws-iso", + "aws-iso-b", + "aws-iso-e", + "aws-iso-f", } - - serverCmd.Flags().String("partition", - endpoints.AwsPartitionID, + serverCmd.Flags().String("partition", "aws", fmt.Sprintf("The AWS partition. Must be one of: %v", partitionKeys)) viper.BindPFlag("server.partition", serverCmd.Flags().Lookup("partition")) diff --git a/cmd/aws-iam-authenticator/verify.go b/cmd/aws-iam-authenticator/verify.go index 9bb37f4f4..885535d2b 100644 --- a/cmd/aws-iam-authenticator/verify.go +++ b/cmd/aws-iam-authenticator/verify.go @@ -19,15 +19,17 @@ limitations under the License. package main import ( + "context" "encoding/json" "fmt" "os" + "sigs.k8s.io/aws-iam-authenticator/pkg/regions" "sigs.k8s.io/aws-iam-authenticator/pkg/token" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/aws/endpoints" - "github.com/aws/aws-sdk-go/aws/session" + aws_config "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/account" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -41,6 +43,8 @@ var verifyCmd = &cobra.Command{ output := viper.GetString("output") clusterID := viper.GetString("clusterID") partition := viper.GetString("partition") + endpointValidationMode := viper.GetString("server.endpointValidationMode") + endpointValidationFile := viper.GetString("server.endpointValidationFile") if tok == "" { fmt.Fprintf(os.Stderr, "error: token not specified\n") @@ -54,14 +58,31 @@ var verifyCmd = &cobra.Command{ os.Exit(1) } - sess := session.Must(session.NewSession()) - ec2metadata := ec2metadata.New(sess) - instanceRegion, err := ec2metadata.Region() + var discoverer regions.Discoverer + switch endpointValidationMode { + case "Legacy": + // TODO: get region? + discoverer = regions.NewSdkV1Discoverer(partition, "") + case "API": + awscfg, err := aws_config.LoadDefaultConfig(context.TODO()) + if err != nil { + logrus.WithError(err).Fatal("unable to create AWS config") + } + client := account.NewFromConfig(awscfg) + discoverer = regions.NewAPIDiscoverer(client, partition) + case "File": + discoverer = regions.NewFileDiscoverer(endpointValidationFile) + default: + // Defensive check here in case the cmd validation fails + logrus.WithField("server.endpointValidationMode", endpointValidationMode).Fatalf( + `invalid EndpointValidationMode, must be one of "Legacy", "API", or "File"`) + } + endpointVerifier, err := regions.NewEndpointVerifier(discoverer) if err != nil { - fmt.Printf("[Warn] Region not found in instance metadata, err: %v", err) + logrus.WithError(err).Fatal("could not create endpoint verifier") } - id, err := token.NewVerifier(clusterID, partition, instanceRegion).Verify(tok) + id, err := token.NewVerifier(clusterID, endpointVerifier).Verify(tok) if err != nil { fmt.Fprintf(os.Stderr, "could not verify token: %v\n", err) os.Exit(1) @@ -86,14 +107,27 @@ func init() { viper.BindPFlag("token", verifyCmd.Flags().Lookup("token")) viper.BindPFlag("output", verifyCmd.Flags().Lookup("output")) - partitionKeys := []string{} - for _, p := range endpoints.DefaultPartitions() { - partitionKeys = append(partitionKeys, p.ID()) + partitionKeys := []string{ + "aws", + "aws-cn", + "aws-us-gov", + "aws-iso", + "aws-iso-b", + "aws-iso-e", + "aws-iso-f", } - verifyCmd.Flags().String("partition", - endpoints.AwsPartitionID, + "aws", fmt.Sprintf("The AWS partition. Must be one of: %v", partitionKeys)) viper.BindPFlag("partition", verifyCmd.Flags().Lookup("partition")) + verifyCmd.Flags().String("endpoint-validation-mode", + "Legacy", + `The method for discovering valid regions. Must be one of "Legacy", "File", or "API"`) + viper.BindPFlag("server.endpointValidationMode", verifyCmd.Flags().Lookup("endpoint-validation-mode")) + + verifyCmd.Flags().String("endpoint-validation-file", "", + `The file to use for endpoint validation. Only used if endpoint-validation-mode is "File"`) + viper.BindPFlag("server.endpointValidationFile", verifyCmd.Flags().Lookup("endpoint-validation-file")) + } diff --git a/go.mod b/go.mod index eb704fd2c..c7dc6d023 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,17 @@ go 1.22.5 require ( github.com/aws/aws-sdk-go v1.54.6 + github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go-v2/config v1.27.32 + github.com/aws/aws-sdk-go-v2/service/account v1.19.4 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 github.com/fsnotify/fsnotify v1.7.0 github.com/gofrs/flock v0.8.1 github.com/google/go-cmp v0.6.0 github.com/manifoldco/promptui v0.9.0 github.com/prometheus/client_golang v1.19.1 github.com/sirupsen/logrus v1.9.3 + github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.18.2 golang.org/x/time v0.5.0 @@ -24,6 +29,16 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2/credentials v1.17.31 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect + github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -61,7 +76,6 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect diff --git a/go.sum b/go.sum index 2a3bcb3a0..c7dcc6b47 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,33 @@ github.com/aws/aws-sdk-go v1.54.6 h1:HEYUib3yTt8E6vxjMWM3yAq5b+qjj/6aKA62mkgux9g= github.com/aws/aws-sdk-go v1.54.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= +github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.32 h1:jnAMVTJTpAQlePCUUlnXnllHEMGVWmvUJOiGjgtS9S0= +github.com/aws/aws-sdk-go-v2/config v1.27.32/go.mod h1:JibtzKJoXT0M/MhoYL6qfCk7nm/MppwukDFZtdgVRoY= +github.com/aws/aws-sdk-go-v2/credentials v1.17.31 h1:jtyfcOfgoqWA2hW/E8sFbwdfgwD3APnF9CLCKE8dTyw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.31/go.mod h1:RSgY5lfCfw+FoyKWtOpLolPlfQVdDBQWTUniAaE+NKY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/account v1.19.4 h1:v/rx7sJ6N9y3XObIyfJOLQnu0G6V/eBVkC5X79N/32Y= +github.com/aws/aws-sdk-go-v2/service/account v1.19.4/go.mod h1:uBBYm9idEyHenbZGnKp7RsFDeatpU3j1eYGpctlHS4A= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 h1:o++HUDXlbrTl4PSal3YHtdErQxB8mDGAtkKNXBWPfIU= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.6/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 h1:yCHcQCOwTfIsc8DoEhM3qXPxD+j8CbI6t1K3dNzsWV0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 h1:TrQadF7GcqvQ63kgwEcjlrVc2Fa0wpgLT0xtc73uAd8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.6/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= diff --git a/pkg/config/types.go b/pkg/config/types.go index f470f5073..539b4e59b 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -189,6 +189,20 @@ type Config struct { ReservedPrefixConfig map[string]ReservedPrefixConfig // Dynamic File Path for BackendMode DynamicBackendModePath string + + // EndpointDiscovererMode is a method for determining how to validate STS domain names in presigned URLs. + // Must be one of "Legacy" (default until June 2025), "API", or "File". + // + // Legacy uses the AWS SDK Go v1 endpoint list to validate that the STS region in an incoming token is valid + // for a given partition. + // + // API makes the server invoke the AWS API call `account:ListRegions` to find a list of valid regions. + // + // File has the server read a file containing a JSON list of strings with valid STS endpoints. + EndpointValidationMode string + + // EndpointValidationFile the file path when EndpointValidationMode's value is "File" + EndpointValidationFile string } type ReservedPrefixConfig struct { diff --git a/pkg/regions/api_discoverer.go b/pkg/regions/api_discoverer.go new file mode 100644 index 000000000..fa8f85257 --- /dev/null +++ b/pkg/regions/api_discoverer.go @@ -0,0 +1,74 @@ +package regions + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/account" +) + +// NewAPIDiscoverer returns a new Discoverer for the given partition. Hostname +// discovery uses the `account:ListRegions` API call to identifiy +// +// The partition is used to determine the correct hostnames for the STS endpoints +// The supported partitions are "aws", "aws-us-gov", and "aws-cn". Future partitions +// will need to be added manually, or use a different Discoverer +func NewAPIDiscoverer(c account.ListRegionsAPIClient, partition string) Discoverer { + return &apiDiscoverer{ + client: c, + partition: partition, + } +} + +type apiDiscoverer struct { + client account.ListRegionsAPIClient + partition string +} + +func (d *apiDiscoverer) Find(ctx context.Context) (map[string]bool, error) { + stsHostnames := map[string]bool{} + + var tlds []string + serviceNames := []string{"sts", "sts-fips"} + switch d.partition { + case "aws": + tlds = []string{"amazonaws.com", "api.aws"} + stsHostnames["sts.amazonaws.com"] = true + case "aws-us-gov": + tlds = []string{"amazonaws.com", "api.aws"} + case "aws-cn": + serviceNames = []string{"sts"} + tlds = []string{"amazonaws.com.cn"} + case "aws-iso": + tlds = []string{"c2s.ic.gov"} + case "aws-iso-b": + tlds = []string{"sc2s.sgov.gov"} + case "aws-iso-e": + tlds = []string{"cloud.adc-e.uk"} + case "aws-iso-f": + tlds = []string{"csp.hci.ic.gov"} + default: + return nil, fmt.Errorf("unrecognized partition %s", d.partition) + } + + result, err := d.client.ListRegions(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to get regions: %w", err) + } + + for _, region := range result.Regions { + // TODO: We could assess if a region is opted in, but we may need to + // increase the frequency of Verifier's ticker or make it configurable + // so clients don't have to wait up to 24h to trust tokens from a + // recently opted-in region. + for _, serviceName := range serviceNames { + for _, tld := range tlds { + hostname := fmt.Sprintf("%s.%s.%s", serviceName, aws.ToString(region.RegionName), tld) + stsHostnames[hostname] = true + } + } + } + + return stsHostnames, nil +} diff --git a/pkg/regions/api_discoverer_test.go b/pkg/regions/api_discoverer_test.go new file mode 100644 index 000000000..3f34b9657 --- /dev/null +++ b/pkg/regions/api_discoverer_test.go @@ -0,0 +1,186 @@ +package regions + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/account" + account_types "github.com/aws/aws-sdk-go-v2/service/account/types" + "github.com/aws/aws-sdk-go/aws" + "github.com/google/go-cmp/cmp" +) + +type fakeRegionsClient struct { + resp account.ListRegionsOutput + err error +} + +func (c *fakeRegionsClient) ListRegions(context.Context, *account.ListRegionsInput, ...func(*account.Options)) (*account.ListRegionsOutput, error) { + return &c.resp, c.err +} + +func TestAPIDiscoverer(t *testing.T) { + testCases := []struct { + name string + discoverer apiDiscoverer + want map[string]bool + wantErr error + }{ + { + name: "aws partition", + discoverer: apiDiscoverer{ + partition: "aws", + client: &fakeRegionsClient{ + resp: account.ListRegionsOutput{ + Regions: []account_types.Region{ + {RegionName: aws.String("us-east-1")}, + {RegionName: aws.String("us-west-2")}, + }, + }, + }, + }, + want: map[string]bool{ + "sts-fips.us-east-1.amazonaws.com": true, + "sts-fips.us-east-1.api.aws": true, + "sts.us-east-1.amazonaws.com": true, + "sts.us-east-1.api.aws": true, + "sts.amazonaws.com": true, + "sts-fips.us-west-2.amazonaws.com": true, + "sts-fips.us-west-2.api.aws": true, + "sts.us-west-2.amazonaws.com": true, + "sts.us-west-2.api.aws": true, + }, + }, + { + name: "aws-us-gov partition", + discoverer: apiDiscoverer{ + partition: "aws-us-gov", + client: &fakeRegionsClient{ + resp: account.ListRegionsOutput{ + Regions: []account_types.Region{ + {RegionName: aws.String("us-gov-east-1")}, + {RegionName: aws.String("us-gov-west-1")}, + }, + }, + }, + }, + want: map[string]bool{ + "sts-fips.us-gov-east-1.amazonaws.com": true, + "sts-fips.us-gov-east-1.api.aws": true, + "sts.us-gov-east-1.amazonaws.com": true, + "sts.us-gov-east-1.api.aws": true, + "sts-fips.us-gov-west-1.amazonaws.com": true, + "sts-fips.us-gov-west-1.api.aws": true, + "sts.us-gov-west-1.amazonaws.com": true, + "sts.us-gov-west-1.api.aws": true, + }, + }, + { + name: "aws-cn partition", + discoverer: apiDiscoverer{ + partition: "aws-cn", + client: &fakeRegionsClient{ + resp: account.ListRegionsOutput{ + Regions: []account_types.Region{ + {RegionName: aws.String("cn-north-1")}, + {RegionName: aws.String("cn-northwest-1")}, + }, + }, + }, + }, + want: map[string]bool{ + "sts.cn-northwest-1.amazonaws.com.cn": true, + "sts.cn-north-1.amazonaws.com.cn": true, + }, + }, + { + name: "error", + discoverer: apiDiscoverer{ + partition: "aws", + client: &fakeRegionsClient{ + err: errors.New("some error"), + }, + }, + wantErr: errors.New("failed to get regions: some error"), + }, + { + name: "empty response", + discoverer: apiDiscoverer{ + partition: "aws", + client: &fakeRegionsClient{ + resp: account.ListRegionsOutput{ + Regions: []account_types.Region{}, + }, + }, + }, + want: map[string]bool{ + "sts.amazonaws.com": true, + }, + }, + { + name: "nil response", + discoverer: apiDiscoverer{ + partition: "aws", + client: &fakeRegionsClient{ + resp: account.ListRegionsOutput{ + Regions: nil, + }, + }, + }, + want: map[string]bool{ + "sts.amazonaws.com": true, + }, + }, + { + name: "duplicate regions", + discoverer: apiDiscoverer{ + partition: "aws", + client: &fakeRegionsClient{ + resp: account.ListRegionsOutput{ + Regions: []account_types.Region{ + {RegionName: aws.String("us-east-1")}, + {RegionName: aws.String("us-east-1")}, + }, + }, + }, + }, + want: map[string]bool{ + "sts-fips.us-east-1.amazonaws.com": true, + "sts-fips.us-east-1.api.aws": true, + "sts.us-east-1.amazonaws.com": true, + "sts.us-east-1.api.aws": true, + "sts.amazonaws.com": true, + }, + }, + { + name: "invalid partition", + discoverer: apiDiscoverer{ + partition: "aws-eu", + client: &fakeRegionsClient{}, + }, + wantErr: errors.New("unrecognized partition aws-eu"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.discoverer.Find(context.Background()) + if err != nil && tc.wantErr == nil { + t.Errorf("unexpected error: got '%v', wanted nil", err) + return + } + if err == nil && tc.wantErr != nil { + t.Errorf("missing expected error '%v', got nil", tc.wantErr) + return + } + if err != nil && tc.wantErr != nil && err.Error() != tc.wantErr.Error() { + t.Errorf("apiDiscoverer.Hostnames() error = '%v', wantErr '%v'", err, tc.wantErr) + return + } + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got unexpected result\n%s", diff) + } + }) + } +} diff --git a/pkg/regions/endpoint_verifier.go b/pkg/regions/endpoint_verifier.go new file mode 100644 index 000000000..aa000200d --- /dev/null +++ b/pkg/regions/endpoint_verifier.go @@ -0,0 +1,90 @@ +package regions + +import ( + "context" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + stsServiceID = "sts" +) + +// Discoverer returns a list of valid STS service hostsnames +type Discoverer interface { + Find(context.Context) (map[string]bool, error) +} + +// EndpointVerifier reports if a given hostname is valid +type EndpointVerifier interface { + Verify(string) bool // Verify if a given hostname is valid + Stop() error // Stop the verifier from updating +} + +// NewEndpointVerifier returns a populated Verifier that updates based a given +// Discoverer every 24 hours. The EndpointVerifier is thread-safe and locks for +// updates. +// +// If the Discoverer returns an error on a future update, the error is logged +// and previous Discoverer values are used. Invocations can call v.Stop() to +// end future updates. +func NewEndpointVerifier(d Discoverer) (EndpointVerifier, error) { + resp := &endpointVerifier{ + d: d, + mu: sync.RWMutex{}, + done: make(chan bool), + period: time.Hour * 24, + } + hosts, err := d.Find(context.Background()) + if err != nil { + return nil, err + } + resp.hosts = hosts + + go resp.run() + + return resp, nil +} + +type endpointVerifier struct { + d Discoverer + hosts map[string]bool + mu sync.RWMutex + done chan bool + + period time.Duration +} + +func (v *endpointVerifier) Verify(host string) bool { + v.mu.RLock() + defer v.mu.RUnlock() + _, ok := v.hosts[host] + return ok +} + +func (v *endpointVerifier) Stop() error { + v.done <- true + return nil +} + +func (v *endpointVerifier) run() { + ticker := time.NewTicker(v.period) + defer ticker.Stop() + for { + select { + case <-v.done: + logrus.Info("stopping Verifier updates") + return + case <-ticker.C: + v.mu.Lock() + if hosts, err := v.d.Find(context.Background()); err == nil { + v.hosts = hosts + } else { + logrus.Errorf("failed to discover sts hosts: %v", err) + } + v.mu.Unlock() + } + } +} diff --git a/pkg/regions/endpoint_verifier_test.go b/pkg/regions/endpoint_verifier_test.go new file mode 100644 index 000000000..bf2bd5a8a --- /dev/null +++ b/pkg/regions/endpoint_verifier_test.go @@ -0,0 +1,42 @@ +package regions + +import ( + "context" + "testing" +) + +type testDiscoverer struct { + resp map[string]bool + err error +} + +func (t *testDiscoverer) Find(context.Context) (map[string]bool, error) { + return t.resp, t.err +} + +var _ Discoverer = &testDiscoverer{} + +func TestEndpointVerifier(t *testing.T) { + verifier, err := NewEndpointVerifier( + &testDiscoverer{ + resp: map[string]bool{"us-east-1": true}, + err: nil, + }, + ) + if err != nil { + t.Fatal(err) + } + + if !verifier.Verify("us-east-1") { + t.Fatal("expected true") + } + + if verifier.Verify("us-east-2") { + t.Fatal("expected false") + } + + err = verifier.Stop() + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/regions/file_discoverer.go b/pkg/regions/file_discoverer.go new file mode 100644 index 000000000..a1686bcd4 --- /dev/null +++ b/pkg/regions/file_discoverer.go @@ -0,0 +1,42 @@ +package regions + +import ( + "context" + "encoding/json" + "io/fs" + "os" +) + +// NewFileDiscoverer creates a Discoverer that reads hostnames from a JSON file +// containing a list of strings. +func NewFileDiscoverer(filename string) Discoverer { + return &fileDiscoverer{ + fs: os.DirFS("."), + filename: filename, + } +} + +type fileDiscoverer struct { + fs fs.FS + filename string +} + +// HostnameFileContent is a type to decode STS hostnames into +type HostnameFileContent []string + +func (d *fileDiscoverer) Find(ctx context.Context) (map[string]bool, error) { + f, err := d.fs.Open(d.filename) + if err != nil { + return nil, err + } + hfc := &HostnameFileContent{} + err = json.NewDecoder(f).Decode(hfc) + if err != nil { + return nil, err + } + resp := make(map[string]bool) + for _, h := range *hfc { + resp[h] = true + } + return resp, nil +} diff --git a/pkg/regions/file_discoverer_test.go b/pkg/regions/file_discoverer_test.go new file mode 100644 index 000000000..32091d449 --- /dev/null +++ b/pkg/regions/file_discoverer_test.go @@ -0,0 +1,89 @@ +package regions + +import ( + "context" + "errors" + "io/fs" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/spf13/afero" +) + +const testFileName = "/test.json" + +type aferoFSWrapper struct{ afero.Fs } + +// Open satisfies the fs.Open interface with the afero implementation +func (a aferoFSWrapper) Open(name string) (fs.File, error) { + return a.Fs.Open(name) +} + +func TestFileDiscoverer(t *testing.T) { + + testCases := []struct { + name string + fs fs.FS + want map[string]bool + wantErr error + }{ + { + name: "MissingFile", + fs: func() fs.FS { + return aferoFSWrapper{afero.NewMemMapFs()} + }(), + want: nil, + wantErr: fs.ErrNotExist, + }, + { + name: "InvalidJSON", + fs: func() fs.FS { + tmpfs := afero.NewMemMapFs() + afero.WriteFile(tmpfs, testFileName, []byte(`["sts.us-east-1.amazonaws.com]`), 0644) + return aferoFSWrapper{tmpfs} + }(), + want: nil, + wantErr: errors.New("unexpected EOF"), + }, + { + name: "ValidFile", + fs: func() fs.FS { + tmpfs := afero.NewMemMapFs() + afero.WriteFile(tmpfs, testFileName, []byte(`["sts.us-east-1.amazonaws.com","sts.us-east-2.amazonaws.com"]`), 0644) + return aferoFSWrapper{tmpfs} + }(), + want: map[string]bool{ + "sts.us-east-1.amazonaws.com": true, + "sts.us-east-2.amazonaws.com": true, + }, + wantErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + f := &fileDiscoverer{ + fs: tc.fs, + filename: testFileName, + } + got, err := f.Find(context.Background()) + if err != nil && tc.wantErr == nil { + t.Errorf("unexpected error: got '%v', wanted nil", err) + return + } + if err == nil && tc.wantErr != nil { + t.Errorf("missing expected error '%v', got nil", tc.wantErr) + return + } + if err != nil && tc.wantErr != nil && !(err.Error() == tc.wantErr.Error() || + errors.Is(err, tc.wantErr)) { + t.Errorf("fileDiscoverer.Hostnames() error = '%v', wantErr '%v'", err, tc.wantErr) + return + } + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got unexpected result\n%s", diff) + } + }) + } + +} diff --git a/pkg/regions/sdk_v1_discoverer.go b/pkg/regions/sdk_v1_discoverer.go new file mode 100644 index 000000000..f37ef230a --- /dev/null +++ b/pkg/regions/sdk_v1_discoverer.go @@ -0,0 +1,92 @@ +package regions + +import ( + "context" + "fmt" + "net/url" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/sirupsen/logrus" +) + +func NewSdkV1Discoverer(partition, region string) Discoverer { + return &sdkV1Discoverer{ + partition: partition, + region: region, + } +} + +type sdkV1Discoverer struct { + partition string + region string +} + +func (d *sdkV1Discoverer) Find(context.Context) (map[string]bool, error) { + return stsHostsForPartition(d.partition, d.region) +} + +func stsHostsForPartition(partitionID, region string) (map[string]bool, error) { + validSTShostnames := map[string]bool{} + + var partition *endpoints.Partition + for _, p := range endpoints.DefaultPartitions() { + if partitionID == p.ID() { + partition = &p + break + } + } + if partition == nil { + return nil, fmt.Errorf("partition %s not found", partitionID) + } + + stsSvc, ok := partition.Services()[stsServiceID] + if !ok { + logrus.Errorf("STS service not found in partition %s", partitionID) + // Add the host of the current instances region if the service doesn't already exists in the partition + // so we don't fail if the service is not present in the go sdk but matches the instances region. + stsHostName, err := getDefaultHostNameForRegion(partition, region, stsServiceID) + if err != nil { + return nil, err + } else { + validSTShostnames[stsHostName] = true + } + return validSTShostnames, nil + } + stsSvcEndPoints := stsSvc.Endpoints() + for _, ep := range stsSvcEndPoints { + rep, err := ep.ResolveEndpoint(endpoints.STSRegionalEndpointOption) + if err != nil { + return nil, err + } + parsedURL, err := url.Parse(rep.URL) + if err != nil { + return nil, err + } + validSTShostnames[parsedURL.Hostname()] = true + } + + // Add the host of the current instances region if not already exists so we don't fail if the region is not + // present in the go sdk but matches the instances region. + if _, ok := stsSvcEndPoints[region]; !ok { + stsHostName, err := getDefaultHostNameForRegion(partition, region, stsServiceID) + if err != nil { + logrus.WithError(err).Error("Error getting default hostname") + return nil, err + } + validSTShostnames[stsHostName] = true + } + + return validSTShostnames, nil +} + +func getDefaultHostNameForRegion(partition *endpoints.Partition, region, service string) (string, error) { + rep, err := partition.EndpointFor(service, region, endpoints.STSRegionalEndpointOption, endpoints.ResolveUnknownServiceOption) + if err != nil { + return "", fmt.Errorf("error resolving endpoint for %s in partition %s. err: %v", region, partition.ID(), err) + } + parsedURL, err := url.Parse(rep.URL) + if err != nil { + return "", fmt.Errorf("error parsing STS URL %s. err: %v", rep.URL, err) + } + return parsedURL.Hostname(), nil +} diff --git a/pkg/regions/sdk_v1_discoverer_test.go b/pkg/regions/sdk_v1_discoverer_test.go new file mode 100644 index 000000000..cefe0bd99 --- /dev/null +++ b/pkg/regions/sdk_v1_discoverer_test.go @@ -0,0 +1,107 @@ +package regions + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStsHostsForPartition(t *testing.T) { + + testCases := []struct { + name string + partition, region string + want map[string]bool + wantErr error + }{ + { + name: "aws-cn", + partition: "aws-cn", + region: "cn-north-1", + want: map[string]bool{ + "sts.cn-northwest-1.amazonaws.com.cn": true, + "sts.cn-north-1.amazonaws.com.cn": true, + }, + }, + { + name: "aws-us-gov", + partition: "aws-us-gov", + region: "us-gov-west-1", + want: map[string]bool{ + "sts.us-gov-east-1.amazonaws.com": true, + "sts.us-gov-west-1.amazonaws.com": true, + }, + }, + { + name: "aws", + partition: "aws", + region: "us-west-1", + want: map[string]bool{ + "sts-fips.us-east-1.amazonaws.com": true, + "sts-fips.us-east-2.amazonaws.com": true, + "sts-fips.us-west-1.amazonaws.com": true, + "sts-fips.us-west-2.amazonaws.com": true, + "sts.af-south-1.amazonaws.com": true, + "sts.amazonaws.com": true, + "sts.ap-east-1.amazonaws.com": true, + "sts.ap-northeast-1.amazonaws.com": true, + "sts.ap-northeast-2.amazonaws.com": true, + "sts.ap-northeast-3.amazonaws.com": true, + "sts.ap-south-1.amazonaws.com": true, + "sts.ap-south-2.amazonaws.com": true, + "sts.ap-southeast-1.amazonaws.com": true, + "sts.ap-southeast-2.amazonaws.com": true, + "sts.ap-southeast-3.amazonaws.com": true, + "sts.ap-southeast-4.amazonaws.com": true, + "sts.ca-central-1.amazonaws.com": true, + "sts.ca-west-1.amazonaws.com": true, + "sts.eu-central-1.amazonaws.com": true, + "sts.eu-central-2.amazonaws.com": true, + "sts.eu-north-1.amazonaws.com": true, + "sts.eu-south-1.amazonaws.com": true, + "sts.eu-south-2.amazonaws.com": true, + "sts.eu-west-1.amazonaws.com": true, + "sts.eu-west-2.amazonaws.com": true, + "sts.eu-west-3.amazonaws.com": true, + "sts.il-central-1.amazonaws.com": true, + "sts.me-central-1.amazonaws.com": true, + "sts.me-south-1.amazonaws.com": true, + "sts.sa-east-1.amazonaws.com": true, + "sts.us-east-1.amazonaws.com": true, + "sts.us-east-2.amazonaws.com": true, + "sts.us-west-1.amazonaws.com": true, + "sts.us-west-2.amazonaws.com": true, + }, + }, + { + name: "unknown partition", + partition: "aws-eu-gov", + region: "eu-gov-west-1", + want: nil, + wantErr: errors.New("partition aws-eu-gov not found"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := stsHostsForPartition(tc.partition, tc.region) + if err != nil && tc.wantErr == nil { + t.Errorf("unexpected error: got '%v', wanted nil", err) + return + } + if err == nil && tc.wantErr != nil { + t.Errorf("missing expected error '%v', got nil", tc.wantErr) + return + } + if err != nil && tc.wantErr != nil && !(err.Error() == tc.wantErr.Error() || + errors.Is(err, tc.wantErr)) { + t.Errorf("stsHostsForPartition() error = '%v', wantErr '%v'", err, tc.wantErr) + return + } + + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("got unexpected result\n%s", diff) + } + }) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 045f948c2..28fdbfd88 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -28,6 +28,8 @@ import ( "sync" "time" + aws_config "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/account" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" "sigs.k8s.io/aws-iam-authenticator/pkg/config" @@ -40,6 +42,7 @@ import ( "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/dynamicfile" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/file" "sigs.k8s.io/aws-iam-authenticator/pkg/metrics" + "sigs.k8s.io/aws-iam-authenticator/pkg/regions" "sigs.k8s.io/aws-iam-authenticator/pkg/token" awsarn "github.com/aws/aws-sdk-go/aws/arn" @@ -142,6 +145,30 @@ func New(cfg config.Config, stopCh <-chan struct{}) *Server { errLog := logrus.WithField("http", "error").Writer() defer errLog.Close() + var discoverer regions.Discoverer + switch cfg.EndpointValidationMode { + case "Legacy": + // TODO: get region? + discoverer = regions.NewSdkV1Discoverer(cfg.PartitionID, "") + case "API": + awscfg, err := aws_config.LoadDefaultConfig(context.TODO()) + if err != nil { + logrus.WithError(err).Fatal("unable to create AWS config") + } + client := account.NewFromConfig(awscfg) + discoverer = regions.NewAPIDiscoverer(client, cfg.PartitionID) + case "File": + discoverer = regions.NewFileDiscoverer(cfg.EndpointValidationFile) + default: + // Defensive check here in case the cmd validation fails + logrus.WithField("server.endpointValidationMode", cfg.EndpointValidationMode).Fatalf( + `invalid EndpointValidationMode, must be one of "Legacy", "API", or "File"`) + } + c.endpointVerifier, err = regions.NewEndpointVerifier(discoverer) + if err != nil { + logrus.WithError(err).Fatal("could not create endpoint verifier") + } + logrus.Infof("listening on %s", listener.Addr()) logrus.Infof("reconfigure your apiserver with `--authentication-token-webhook-config-file=%s` to enable (assuming default hostPath mounts)", c.GenerateKubeconfigPath) internalHandler := c.getHandler(backendMapper, c.EC2DescribeInstancesQps, c.EC2DescribeInstancesBurst, stopCh) @@ -162,16 +189,17 @@ func (c *Server) Run(stopCh <-chan struct{}) { go func() { http.ListenAndServe(":21363", &healthzHandler{}) }() - go func() { + go func(s *Server) { for { select { case <-stopCh: logrus.Info("shut down mapper before return from Run") - close(c.internalHandler.backendMapper.mapperStopCh) + close(s.internalHandler.backendMapper.mapperStopCh) + c.endpointVerifier.Stop() return } } - }() + }(c) if err := c.httpServer.Serve(c.listener); err != nil { logrus.WithError(err).Warning("http server exited") } @@ -191,6 +219,7 @@ type healthzHandler struct{} func (m *healthzHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "ok") } + func (c *Server) getHandler(backendMapper BackendMapper, ec2DescribeQps int, ec2DescribeBurst int, stopCh <-chan struct{}) *handler { if c.ServerEC2DescribeInstancesRoleARN != "" { _, err := awsarn.Parse(c.ServerEC2DescribeInstancesRoleARN) @@ -206,7 +235,7 @@ func (c *Server) getHandler(backendMapper BackendMapper, ec2DescribeQps int, ec2 } h := &handler{ - verifier: token.NewVerifier(c.ClusterID, c.PartitionID, instanceRegion), + verifier: token.NewVerifier(c.ClusterID, c.endpointVerifier), ec2Provider: ec2provider.New(c.ServerEC2DescribeInstancesRoleARN, c.SourceARN, instanceRegion, ec2DescribeQps, ec2DescribeBurst), clusterID: c.ClusterID, backendMapper: backendMapper, diff --git a/pkg/server/types.go b/pkg/server/types.go index 45e059a9b..787744092 100644 --- a/pkg/server/types.go +++ b/pkg/server/types.go @@ -22,15 +22,17 @@ import ( "sigs.k8s.io/aws-iam-authenticator/pkg/config" "sigs.k8s.io/aws-iam-authenticator/pkg/mapper" + "sigs.k8s.io/aws-iam-authenticator/pkg/regions" ) // Server for the authentication webhook. type Server struct { // Config is the whole configuration of aws-iam-authenticator used for valid keys and certs, kubeconfig, and so on config.Config - httpServer http.Server - listener net.Listener - internalHandler *handler + httpServer http.Server + listener net.Listener + internalHandler *handler + endpointVerifier regions.EndpointVerifier } type BackendMapper struct { diff --git a/pkg/token/token.go b/pkg/token/token.go index 16ab8d92b..cf5d9be51 100644 --- a/pkg/token/token.go +++ b/pkg/token/token.go @@ -38,13 +38,13 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/aws/aws-sdk-go/service/sts/stsiface" "github.com/prometheus/client_golang/prometheus" - "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/pkg/apis/clientauthentication" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" "sigs.k8s.io/aws-iam-authenticator/pkg" "sigs.k8s.io/aws-iam-authenticator/pkg/arn" "sigs.k8s.io/aws-iam-authenticator/pkg/metrics" + "sigs.k8s.io/aws-iam-authenticator/pkg/regions" ) // Identity is returned on successful Verify() results. It contains a parsed @@ -365,82 +365,13 @@ type Verifier interface { } type tokenVerifier struct { - client *http.Client - clusterID string - validSTShostnames map[string]bool -} - -func getDefaultHostNameForRegion(partition *endpoints.Partition, region, service string) (string, error) { - rep, err := partition.EndpointFor(service, region, endpoints.STSRegionalEndpointOption, endpoints.ResolveUnknownServiceOption) - if err != nil { - return "", fmt.Errorf("Error resolving endpoint for %s in partition %s. err: %v", region, partition.ID(), err) - } - parsedURL, err := url.Parse(rep.URL) - if err != nil { - return "", fmt.Errorf("Error parsing STS URL %s. err: %v", rep.URL, err) - } - return parsedURL.Hostname(), nil -} - -func stsHostsForPartition(partitionID, region string) map[string]bool { - validSTShostnames := map[string]bool{} - - var partition *endpoints.Partition - for _, p := range endpoints.DefaultPartitions() { - if partitionID == p.ID() { - partition = &p - break - } - } - if partition == nil { - logrus.Errorf("Partition %s not valid", partitionID) - return validSTShostnames - } - - stsSvc, ok := partition.Services()[stsServiceID] - if !ok { - logrus.Errorf("STS service not found in partition %s", partitionID) - // Add the host of the current instances region if the service doesn't already exists in the partition - // so we don't fail if the service is not present in the go sdk but matches the instances region. - stsHostName, err := getDefaultHostNameForRegion(partition, region, stsServiceID) - if err != nil { - logrus.WithError(err).Error("Error getting default hostname") - } else { - validSTShostnames[stsHostName] = true - } - return validSTShostnames - } - stsSvcEndPoints := stsSvc.Endpoints() - for epName, ep := range stsSvcEndPoints { - rep, err := ep.ResolveEndpoint(endpoints.STSRegionalEndpointOption) - if err != nil { - logrus.WithError(err).Errorf("Error resolving endpoint for %s in partition %s", epName, partitionID) - continue - } - parsedURL, err := url.Parse(rep.URL) - if err != nil { - logrus.WithError(err).Errorf("Error parsing STS URL %s", rep.URL) - continue - } - validSTShostnames[parsedURL.Hostname()] = true - } - - // Add the host of the current instances region if not already exists so we don't fail if the region is not - // present in the go sdk but matches the instances region. - if _, ok := stsSvcEndPoints[region]; !ok { - stsHostName, err := getDefaultHostNameForRegion(partition, region, stsServiceID) - if err != nil { - logrus.WithError(err).Error("Error getting default hostname") - return validSTShostnames - } - validSTShostnames[stsHostName] = true - } - - return validSTShostnames + client *http.Client + clusterID string + endpointVerifier regions.EndpointVerifier } // NewVerifier creates a Verifier that is bound to the clusterID and uses the default http client. -func NewVerifier(clusterID, partitionID, region string) Verifier { +func NewVerifier(clusterID string, endpointVerifier regions.EndpointVerifier) Verifier { // Initialize metrics if they haven't already been initialized to avoid a // nil pointer panic when setting metric values. if !metrics.Initialized() { @@ -453,14 +384,14 @@ func NewVerifier(clusterID, partitionID, region string) Verifier { return http.ErrUseLastResponse }, }, - clusterID: clusterID, - validSTShostnames: stsHostsForPartition(partitionID, region), + clusterID: clusterID, + endpointVerifier: endpointVerifier, } } // verify a sts host, doc: http://docs.amazonaws.cn/en_us/general/latest/gr/rande.html#sts_region func (v tokenVerifier) verifyHost(host string) error { - if _, ok := v.validSTShostnames[host]; !ok { + if !v.endpointVerifier.Verify(host) { return FormatError{fmt.Sprintf("unexpected hostname %q in pre-signed URL", host)} } return nil diff --git a/pkg/token/token_test.go b/pkg/token/token_test.go index a8e997c86..80be76472 100644 --- a/pkg/token/token_test.go +++ b/pkg/token/token_test.go @@ -33,19 +33,29 @@ func TestMain(m *testing.M) { m.Run() } -func validationErrorTest(t *testing.T, partition string, token string, expectedErr string) { +type testEndpointVerifier struct { + resp bool +} + +func (v *testEndpointVerifier) Verify(string) bool { + return v.resp +} + +func (v *testEndpointVerifier) Stop() error { return nil } + +func validationErrorTest(t *testing.T, token string, expectedErr string) { t.Helper() - _, err := NewVerifier("", partition, "").(tokenVerifier).Verify(token) + _, err := NewVerifier("", &testEndpointVerifier{true}).(tokenVerifier).Verify(token) errorContains(t, err, expectedErr) } -func validationSuccessTest(t *testing.T, partition, token string) { +func validationSuccessTest(t *testing.T, token string) { t.Helper() arn := "arn:aws:iam::123456789012:user/Alice" account := "123456789012" userID := "Alice" - _, err := newVerifier(partition, 200, jsonResponse(arn, account, userID), nil).Verify(token) + _, err := newVerifier(true, 200, jsonResponse(arn, account, userID), nil).Verify(token) if err != nil { t.Errorf("received unexpected error: %s", err) } @@ -83,7 +93,7 @@ func toToken(url string) string { return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(url)) } -func newVerifier(partition string, statusCode int, body string, err error) Verifier { +func newVerifier(validRegion bool, statusCode int, body string, err error) Verifier { var rc io.ReadCloser if body != "" { rc = io.NopCloser(bytes.NewReader([]byte(body))) @@ -98,7 +108,7 @@ func newVerifier(partition string, statusCode int, body string, err error) Verif }, }, }, - validSTShostnames: stsHostsForPartition(partition, ""), + endpointVerifier: &testEndpointVerifier{validRegion}, } } @@ -131,96 +141,52 @@ func jsonResponse(arn, account, userid string) string { return string(data) } -func TestSTSEndpoints(t *testing.T) { - cases := []struct { - partition string - domain string - valid bool - region string - }{ - {"aws-cn", "sts.cn-northwest-1.amazonaws.com.cn", true, ""}, - {"aws-cn", "sts.cn-north-1.amazonaws.com.cn", true, ""}, - {"aws-cn", "sts.us-iso-east-1.c2s.ic.gov", false, ""}, - {"aws", "sts.amazonaws.com", true, ""}, - {"aws", "sts-fips.us-west-2.amazonaws.com", true, ""}, - {"aws", "sts-fips.us-east-1.amazonaws.com", true, ""}, - {"aws", "sts.us-east-1.amazonaws.com", true, ""}, - {"aws", "sts.us-east-2.amazonaws.com", true, ""}, - {"aws", "sts.us-west-1.amazonaws.com", true, ""}, - {"aws", "sts.us-west-2.amazonaws.com", true, ""}, - {"aws", "sts.ap-south-1.amazonaws.com", true, ""}, - {"aws", "sts.ap-northeast-1.amazonaws.com", true, ""}, - {"aws", "sts.ap-northeast-2.amazonaws.com", true, ""}, - {"aws", "sts.ap-southeast-1.amazonaws.com", true, ""}, - {"aws", "sts.ap-southeast-2.amazonaws.com", true, ""}, - {"aws", "sts.ca-central-1.amazonaws.com", true, ""}, - {"aws", "sts.eu-central-1.amazonaws.com", true, ""}, - {"aws", "sts.eu-west-1.amazonaws.com", true, ""}, - {"aws", "sts.eu-west-2.amazonaws.com", true, ""}, - {"aws", "sts.eu-west-3.amazonaws.com", true, ""}, - {"aws", "sts.eu-north-1.amazonaws.com", true, ""}, - {"aws", "sts.amazonaws.com.cn", false, ""}, - {"aws", "sts.not-a-region.amazonaws.com", false, ""}, - {"aws", "sts.default-region.amazonaws.com", true, "default-region"}, - {"aws-iso", "sts.us-iso-east-1.c2s.ic.gov", true, ""}, - {"aws-iso", "sts.cn-north-1.amazonaws.com.cn", false, ""}, - {"aws-iso-b", "sts.cn-north-1.amazonaws.com.cn", false, ""}, - {"aws-us-gov", "sts.us-gov-east-1.amazonaws.com", true, ""}, - {"aws-us-gov", "sts.amazonaws.com", false, ""}, - {"aws-not-a-partition", "sts.amazonaws.com", false, ""}, - } - - for _, c := range cases { - verifier := NewVerifier("", c.partition, c.region).(tokenVerifier) - if err := verifier.verifyHost(c.domain); err != nil && c.valid { - t.Errorf("%s is not valid endpoint for partition %s", c.domain, c.partition) - } - } -} - func TestVerifyTokenPreSTSValidations(t *testing.T) { - b := make([]byte, maxTokenLenBytes+1, maxTokenLenBytes+1) + b := make([]byte, maxTokenLenBytes+1) s := string(b) - validationErrorTest(t, "aws", s, "token is too large") - validationErrorTest(t, "aws", "k8s-aws-v2.asdfasdfa", "token is missing expected \"k8s-aws-v1.\" prefix") - validationErrorTest(t, "aws", "k8s-aws-v1.decodingerror", "illegal base64 data") - - validationErrorTest(t, "aws", toToken(":ab:cd.af:/asda"), "missing protocol scheme") - validationErrorTest(t, "aws", toToken("http://"), "unexpected scheme") - validationErrorTest(t, "aws", toToken("https://google.com"), fmt.Sprintf("unexpected hostname %q in pre-signed URL", "google.com")) - validationErrorTest(t, "aws-cn", toToken("https://sts.cn-north-1.amazonaws.com.cn/abc"), "unexpected path in pre-signed URL") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/abc"), "unexpected path in pre-signed URL") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/?NoInWhiteList=abc"), "non-whitelisted query parameter") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/?action=get&action=post"), "query parameter with multiple values not supported") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/?action=NotGetCallerIdenity"), "unexpected action parameter in pre-signed URL") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=abc%3bx-k8s-aws-i%3bdef"), "client did not sign the x-k8s-aws-id header in the pre-signed URL") - validationErrorTest(t, "aws", toToken(fmt.Sprintf("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=9999999", timeStr)), "invalid X-Amz-Expires parameter in pre-signed URL") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=xxxxxxx&x-amz-expires=60"), "error parsing X-Amz-Date parameter") - validationErrorTest(t, "aws", toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=19900422T010203Z&x-amz-expires=60"), "X-Amz-Date parameter is expired") - validationErrorTest(t, "aws", toToken(fmt.Sprintf("https://sts.sa-east-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60%%gh", timeStr)), "input token was not properly formatted: malformed query parameter") - validationSuccessTest(t, "aws", toToken(fmt.Sprintf("https://sts.us-east-2.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) - validationSuccessTest(t, "aws", toToken(fmt.Sprintf("https://sts.ap-northeast-2.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) - validationSuccessTest(t, "aws", toToken(fmt.Sprintf("https://sts.ca-central-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) - validationSuccessTest(t, "aws", toToken(fmt.Sprintf("https://sts.eu-west-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) - validationSuccessTest(t, "aws", toToken(fmt.Sprintf("https://sts.sa-east-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) - validationErrorTest(t, "aws", toToken(fmt.Sprintf("https://sts.us-west-2.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAAAAAAAAAAAAAAAAA%%2F20220601%%2Fus-west-2%%2Fsts%%2Faws4_request&X-Amz-Date=%s&X-Amz-Expires=900&X-Amz-Security-Token=XXXXXXXXXXXXX&X-Amz-SignedHeaders=host%%3Bx-k8s-aws-id&x-amz-credential=eve&X-Amz-Signature=999999999999999999", timeStr)), "input token was not properly formatted: duplicate query parameter found:") + validationErrorTest(t, s, "token is too large") + validationErrorTest(t, "k8s-aws-v2.asdfasdfa", "token is missing expected \"k8s-aws-v1.\" prefix") + validationErrorTest(t, "k8s-aws-v1.decodingerror", "illegal base64 data") + + validationErrorTest(t, toToken(":ab:cd.af:/asda"), "missing protocol scheme") + validationErrorTest(t, toToken("http://"), "unexpected scheme") + + _, err := NewVerifier("", &testEndpointVerifier{false}).(tokenVerifier).Verify(toToken("https://google.com")) + errorContains(t, err, fmt.Sprintf("unexpected hostname %q in pre-signed URL", "google.com")) + + validationErrorTest(t, toToken("https://sts.cn-north-1.amazonaws.com.cn/abc"), "unexpected path in pre-signed URL") + validationErrorTest(t, toToken("https://sts.amazonaws.com/abc"), "unexpected path in pre-signed URL") + validationErrorTest(t, toToken("https://sts.amazonaws.com/?NoInWhiteList=abc"), "non-whitelisted query parameter") + validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=get&action=post"), "query parameter with multiple values not supported") + validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=NotGetCallerIdenity"), "unexpected action parameter in pre-signed URL") + validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=abc%3bx-k8s-aws-i%3bdef"), "client did not sign the x-k8s-aws-id header in the pre-signed URL") + validationErrorTest(t, toToken(fmt.Sprintf("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=9999999", timeStr)), "invalid X-Amz-Expires parameter in pre-signed URL") + validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=xxxxxxx&x-amz-expires=60"), "error parsing X-Amz-Date parameter") + validationErrorTest(t, toToken("https://sts.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=19900422T010203Z&x-amz-expires=60"), "X-Amz-Date parameter is expired") + validationErrorTest(t, toToken(fmt.Sprintf("https://sts.sa-east-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60%%gh", timeStr)), "input token was not properly formatted: malformed query parameter") + validationSuccessTest(t, toToken(fmt.Sprintf("https://sts.us-east-2.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) + validationSuccessTest(t, toToken(fmt.Sprintf("https://sts.ap-northeast-2.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) + validationSuccessTest(t, toToken(fmt.Sprintf("https://sts.ca-central-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) + validationSuccessTest(t, toToken(fmt.Sprintf("https://sts.eu-west-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) + validationSuccessTest(t, toToken(fmt.Sprintf("https://sts.sa-east-1.amazonaws.com/?action=GetCallerIdentity&x-amz-signedheaders=x-k8s-aws-id&x-amz-date=%s&x-amz-expires=60", timeStr))) + validationErrorTest(t, toToken(fmt.Sprintf("https://sts.us-west-2.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAAAAAAAAAAAAAAAAA%%2F20220601%%2Fus-west-2%%2Fsts%%2Faws4_request&X-Amz-Date=%s&X-Amz-Expires=900&X-Amz-Security-Token=XXXXXXXXXXXXX&X-Amz-SignedHeaders=host%%3Bx-k8s-aws-id&x-amz-credential=eve&X-Amz-Signature=999999999999999999", timeStr)), "input token was not properly formatted: duplicate query parameter found:") } func TestVerifyHTTPThrottling(t *testing.T) { - testVerifier := newVerifier("aws", 400, "{\\\"Error\\\":{\\\"Code\\\":\\\"Throttling\\\",\\\"Message\\\":\\\"Rate exceeded\\\",\\\"Type\\\":\\\"Sender\\\"},\\\"RequestId\\\":\\\"8c2d3520-24e1-4d5c-ac55-7e226335f447\\\"}", nil) + testVerifier := newVerifier(true, 400, "{\\\"Error\\\":{\\\"Code\\\":\\\"Throttling\\\",\\\"Message\\\":\\\"Rate exceeded\\\",\\\"Type\\\":\\\"Sender\\\"},\\\"RequestId\\\":\\\"8c2d3520-24e1-4d5c-ac55-7e226335f447\\\"}", nil) _, err := testVerifier.Verify(validToken) errorContains(t, err, "sts getCallerIdentity was throttled") assertSTSThrottling(t, err) } func TestVerifyHTTPError(t *testing.T) { - _, err := newVerifier("aws", 0, "", errors.New("an error")).Verify(validToken) + _, err := newVerifier(true, 0, "", errors.New("an error")).Verify(validToken) errorContains(t, err, "error during GET: an error") assertSTSError(t, err) } func TestVerifyHTTP403(t *testing.T) { - _, err := newVerifier("aws", 403, " ", nil).Verify(validToken) + _, err := newVerifier(true, 403, " ", nil).Verify(validToken) errorContains(t, err, "error from AWS (expected 200, got") assertSTSError(t, err) } @@ -236,7 +202,7 @@ func TestVerifyNoRedirectsFollowed(t *testing.T) { })) defer ts.Close() - tokVerifier := NewVerifier("", "aws", "").(tokenVerifier) + tokVerifier := NewVerifier("", &testEndpointVerifier{true}).(tokenVerifier) resp, err := tokVerifier.client.Get(ts.URL) if err != nil { @@ -263,7 +229,7 @@ func TestVerifyBodyReadError(t *testing.T) { }, }, }, - validSTShostnames: stsHostsForPartition("aws", ""), + endpointVerifier: &testEndpointVerifier{true}, } _, err := verifier.Verify(validToken) errorContains(t, err, "error reading HTTP result") @@ -271,19 +237,19 @@ func TestVerifyBodyReadError(t *testing.T) { } func TestVerifyUnmarshalJSONError(t *testing.T) { - _, err := newVerifier("aws", 200, "xxxx", nil).Verify(validToken) + _, err := newVerifier(true, 200, "xxxx", nil).Verify(validToken) errorContains(t, err, "invalid character") assertSTSError(t, err) } func TestVerifyInvalidCanonicalARNError(t *testing.T) { - _, err := newVerifier("aws", 200, jsonResponse("arn", "1000", "userid"), nil).Verify(validToken) + _, err := newVerifier(true, 200, jsonResponse("arn", "1000", "userid"), nil).Verify(validToken) errorContains(t, err, "arn 'arn' is invalid:") assertSTSError(t, err) } func TestVerifyInvalidUserIDError(t *testing.T) { - _, err := newVerifier("aws", 200, jsonResponse("arn:aws:iam::123456789012:role/Alice", "123456789012", "not:vailid:userid"), nil).Verify(validToken) + _, err := newVerifier(true, 200, jsonResponse("arn:aws:iam::123456789012:role/Alice", "123456789012", "not:vailid:userid"), nil).Verify(validToken) errorContains(t, err, "malformed UserID") assertSTSError(t, err) } @@ -293,7 +259,7 @@ func TestVerifyNoSession(t *testing.T) { account := "123456789012" userID := "Alice" accessKeyID := "ASIABCDEFGHIJKLMNOPQ" - identity, err := newVerifier("aws", 200, jsonResponse(arn, account, userID), nil).Verify(validToken) + identity, err := newVerifier(true, 200, jsonResponse(arn, account, userID), nil).Verify(validToken) if err != nil { t.Errorf("expected error to be nil was %q", err) } @@ -316,7 +282,7 @@ func TestVerifySessionName(t *testing.T) { account := "123456789012" userID := "Alice" session := "session-name" - identity, err := newVerifier("aws", 200, jsonResponse(arn, account, userID+":"+session), nil).Verify(validToken) + identity, err := newVerifier(true, 200, jsonResponse(arn, account, userID+":"+session), nil).Verify(validToken) if err != nil { t.Errorf("expected error to be nil was %q", err) } @@ -334,7 +300,7 @@ func TestVerifyCanonicalARN(t *testing.T) { account := "123456789012" userID := "Alice" session := "session-name" - identity, err := newVerifier("aws", 200, jsonResponse(arn, account, userID+":"+session), nil).Verify(validToken) + identity, err := newVerifier(true, 200, jsonResponse(arn, account, userID+":"+session), nil).Verify(validToken) if err != nil { t.Errorf("expected error to be nil was %q", err) } @@ -520,73 +486,6 @@ func response(account, userID, arn string) getCallerIdentityWrapper { return wrapper } -func Test_getDefaultHostNameForRegion(t *testing.T) { - type args struct { - partition endpoints.Partition - region string - service string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "service doesn't exist should return default host name", - args: args{ - partition: endpoints.AwsIsoEPartition(), - region: "eu-isoe-west-1", - service: "test", - }, - want: "test.eu-isoe-west-1.cloud.adc-e.uk", - wantErr: false, - }, - { - name: "service and region doesn't exist should return default host name", - args: args{ - partition: endpoints.AwsIsoEPartition(), - region: "eu-isoe-test-1", - service: "test", - }, - want: "test.eu-isoe-test-1.cloud.adc-e.uk", - wantErr: false, - }, - { - name: "region doesn't exist should return default host name", - args: args{ - partition: endpoints.AwsIsoPartition(), - region: "us-iso-test-1", - service: "sts", - }, - want: "sts.us-iso-test-1.c2s.ic.gov", - wantErr: false, - }, - { - name: "invalid region should return error", - args: args{ - partition: endpoints.AwsIsoPartition(), - region: "test_123", - service: "sts", - }, - want: "", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getDefaultHostNameForRegion(&tt.args.partition, tt.args.region, tt.args.service) - if (err != nil) != tt.wantErr { - t.Errorf("getDefaultHostNameForRegion() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("getDefaultHostNameForRegion() = %v, want %v", got, tt.want) - } - }) - } -} - func TestGetWithSTS(t *testing.T) { clusterID := "test-cluster" diff --git a/tests/integration/go.mod b/tests/integration/go.mod index 451b89c6d..c7602cae6 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -18,6 +18,20 @@ require ( github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.32 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.31 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/account v1.19.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect + github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect diff --git a/tests/integration/go.sum b/tests/integration/go.sum index f4a756a0e..43379ff43 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -12,6 +12,34 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.54.6 h1:HEYUib3yTt8E6vxjMWM3yAq5b+qjj/6aKA62mkgux9g= github.com/aws/aws-sdk-go v1.54.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= +github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.32 h1:jnAMVTJTpAQlePCUUlnXnllHEMGVWmvUJOiGjgtS9S0= +github.com/aws/aws-sdk-go-v2/config v1.27.32/go.mod h1:JibtzKJoXT0M/MhoYL6qfCk7nm/MppwukDFZtdgVRoY= +github.com/aws/aws-sdk-go-v2/credentials v1.17.31 h1:jtyfcOfgoqWA2hW/E8sFbwdfgwD3APnF9CLCKE8dTyw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.31/go.mod h1:RSgY5lfCfw+FoyKWtOpLolPlfQVdDBQWTUniAaE+NKY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/account v1.19.4 h1:v/rx7sJ6N9y3XObIyfJOLQnu0G6V/eBVkC5X79N/32Y= +github.com/aws/aws-sdk-go-v2/service/account v1.19.4/go.mod h1:uBBYm9idEyHenbZGnKp7RsFDeatpU3j1eYGpctlHS4A= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 h1:o++HUDXlbrTl4PSal3YHtdErQxB8mDGAtkKNXBWPfIU= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.6/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 h1:yCHcQCOwTfIsc8DoEhM3qXPxD+j8CbI6t1K3dNzsWV0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 h1:TrQadF7GcqvQ63kgwEcjlrVc2Fa0wpgLT0xtc73uAd8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.6/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -172,6 +200,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=