Skip to content

Commit

Permalink
Abstract region discovery
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
micahhausler committed Sep 4, 2024
1 parent b154c1d commit 45fbf23
Show file tree
Hide file tree
Showing 20 changed files with 1,018 additions and 268 deletions.
47 changes: 39 additions & 8 deletions cmd/aws-iam-authenticator/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ limitations under the License.
package main

import (
"context"
"errors"
"fmt"
"os"

"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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
16 changes: 9 additions & 7 deletions cmd/aws-iam-authenticator/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"))

Expand Down
60 changes: 47 additions & 13 deletions cmd/aws-iam-authenticator/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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"))

}
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
14 changes: 14 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
74 changes: 74 additions & 0 deletions pkg/regions/api_discoverer.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 45fbf23

Please sign in to comment.