Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use matching signing region for IAM join method #47425

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions integration/ec2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/lite"
cloudimds "github.com/gravitational/teleport/lib/cloud/imds"
cloudaws "github.com/gravitational/teleport/lib/cloud/imds/aws"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/labels"
"github.com/gravitational/teleport/lib/service"
Expand Down Expand Up @@ -143,6 +144,12 @@ func getIID(ctx context.Context, t *testing.T) imds.InstanceIdentityDocument {
func getCallerIdentity(ctx context.Context, t *testing.T) *sts.GetCallerIdentityOutput {
cfg, err := config.LoadDefaultConfig(ctx)
require.NoError(t, err)
if cfg.Region == "" {
imdsClient, err := cloudaws.NewInstanceMetadataClient(ctx)
require.NoError(t, err)
cfg.Region, err = imdsClient.GetRegion(ctx)
require.NoError(t, err, "trying to get local region from IMDSv2")
}
stsClient := sts.NewFromConfig(cfg)
output, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
require.NoError(t, err)
Expand Down
16 changes: 13 additions & 3 deletions lib/auth/join/iam/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,24 @@ var (
// FIPSSTSEndpoints returns the set of known valid FIPS AWS STS endpoints.
FIPSSTSEndpoints = sync.OnceValue(func() []string {
return []string{
fipsSTSEndpointUSEast1,
"sts-fips.us-east-1.amazonaws.com",
"sts-fips.us-east-2.amazonaws.com",
"sts-fips.us-west-1.amazonaws.com",
"sts-fips.us-west-2.amazonaws.com",
"sts.us-gov-east-1.amazonaws.com",
"sts.us-gov-west-1.amazonaws.com",
}
})
)

const fipsSTSEndpointUSEast1 = "sts-fips.us-east-1.amazonaws.com"
// FIPSSTSRegions returns the set of known AWS regions with FIPS STS endpoints.
FIPSSTSRegions = sync.OnceValue(func() []string {
return []string{
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"us-gov-east-1",
"us-gov-west-1",
}
})
)
60 changes: 26 additions & 34 deletions lib/auth/join/iam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const (
)

type stsIdentityRequestOptions struct {
fipsEndpointOption aws.FIPSEndpointState
imdsClient imdsClient
useFIPS bool
imdsClient imdsClient
}

type stsIdentityRequestOption func(cfg *stsIdentityRequestOptions)
Expand All @@ -51,11 +51,7 @@ type stsIdentityRequestOption func(cfg *stsIdentityRequestOptions)
// regions, this will use the us-east-1 FIPS endpoint.
func WithFIPSEndpoint(useFIPS bool) stsIdentityRequestOption {
return func(opts *stsIdentityRequestOptions) {
if useFIPS {
opts.fipsEndpointOption = aws.FIPSEndpointStateEnabled
} else {
opts.fipsEndpointOption = aws.FIPSEndpointStateUnset
}
opts.useFIPS = useFIPS
}
}

Expand Down Expand Up @@ -87,11 +83,30 @@ func CreateSignedSTSIdentityRequest(ctx context.Context, challenge string, opts
return nil, trace.Wrap(err, "loading default AWS config")
}

if awsConfig.Region == "" {
// We can try to get the local region from IMDSv2 if running on EC2.
region, err := getEC2LocalRegion(ctx, &options)
if err != nil {
slog.InfoContext(ctx, "Failed to resolve local AWS region from environment or IMDS. Using us-east-1 by default. This will fail in non-default AWS partitions. Consider setting AWS_REGION or enabling IMDSv2.",
slog.Any("error", err))
region = "us-east-1"
}
awsConfig.Region = region
}

if options.useFIPS && !slices.Contains(FIPSSTSRegions(), awsConfig.Region) {
slog.InfoContext(ctx, "AWS region does not have a FIPS STS endpoint, attempting to use us-east-1 instead. This will fail in non-default AWS partitions.",
slog.String("region", awsConfig.Region))
awsConfig.Region = "us-east-1"
}

var signedRequest bytes.Buffer
stsClient := sts.NewFromConfig(awsConfig,
sts.WithEndpointResolverV2(newCustomResolver(challenge, &options)),
sts.WithEndpointResolverV2(newCustomResolver(challenge)),
func(stsOpts *sts.Options) {
stsOpts.EndpointOptions.UseFIPSEndpoint = options.fipsEndpointOption
if options.useFIPS {
stsOpts.EndpointOptions.UseFIPSEndpoint = aws.FIPSEndpointStateEnabled
}
// Use a fake HTTP client to record the request.
stsOpts.HTTPClient = &httpRequestRecorder{&signedRequest}
// httpRequestRecorder intentionally records the request and returns
Expand Down Expand Up @@ -132,43 +147,20 @@ func getEC2LocalRegion(ctx context.Context, opts *stsIdentityRequestOptions) (st
type customResolver struct {
defaultResolver sts.EndpointResolverV2
challenge string
opts *stsIdentityRequestOptions
}

func newCustomResolver(challenge string, opts *stsIdentityRequestOptions) *customResolver {
func newCustomResolver(challenge string) *customResolver {
return &customResolver{
defaultResolver: sts.NewDefaultEndpointResolverV2(),
challenge: challenge,
opts: opts,
}
}

// ResolveEndpoint implements [sts.EndpointResolverV2].
func (r customResolver) ResolveEndpoint(ctx context.Context, params sts.EndpointParameters) (smithyendpoints.Endpoint, error) {
if aws.ToString(params.Region) == "" {
// If we don't have a region from the environment here this will fail to
// resolve. We can try to get the local region from IMDSv2 if running on EC2.
region, err := getEC2LocalRegion(ctx, r.opts)
switch {
case trace.IsNotFound(err):
params.Region = aws.String("aws-global")
params.UseGlobalEndpoint = aws.Bool(true)
case err != nil:
return smithyendpoints.Endpoint{}, trace.Wrap(err, "failed to resolve local AWS region from environment or IMDS")
default:
params.Region = aws.String(region)
}
}
endpoint, err := r.defaultResolver.ResolveEndpoint(ctx, params)
if err != nil {
return smithyendpoints.Endpoint{}, trace.Wrap(err)
}
if aws.ToBool(params.UseFIPS) && !slices.Contains(FIPSSTSEndpoints(), endpoint.URI.Host) {
// The default resolver will return non-existent endpoints if FIPS was
// requested in regions outside the USA. Use the FIPS endpoint in
// us-east-1 instead.
slog.InfoContext(ctx, "The AWS SDK resolved an invalid FIPS STS endpoint, attempting to use the us-east-1 FIPS STS endpoint instead. This will fail in non-default AWS partitions.", "resolved", endpoint.URI.Host)
endpoint.URI.Host = fipsSTSEndpointUSEast1
return endpoint, trace.Wrap(err)
}
// Add challenge as a header to be signed.
endpoint.Headers.Add(challengeHeaderKey, r.challenge)
Expand Down
79 changes: 47 additions & 32 deletions lib/auth/join/iam/iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/auth/join/iam"
Expand All @@ -40,51 +41,61 @@ func TestCreateSignedSTSIdentityRequest(t *testing.T) {
const challenge = "asdf12345"

for desc, tc := range map[string]struct {
envRegion string
imdsRegion string
fips bool
expectEndpoint string
expectError string
envRegion string
imdsRegion string
fips bool
expectError string
expectEndpoint string
expectSignatureRegion string
}{
"no region": {
expectEndpoint: "sts.amazonaws.com",
expectEndpoint: "sts.us-east-1.amazonaws.com",
expectSignatureRegion: "us-east-1",
},
"no region fips": {
fips: true,
expectEndpoint: "sts-fips.us-east-1.amazonaws.com",
fips: true,
expectEndpoint: "sts-fips.us-east-1.amazonaws.com",
expectSignatureRegion: "us-east-1",
},
"us-west-2": {
envRegion: "us-west-2",
expectEndpoint: "sts.us-west-2.amazonaws.com",
envRegion: "us-west-2",
expectEndpoint: "sts.us-west-2.amazonaws.com",
expectSignatureRegion: "us-west-2",
},
"us-west-2 with region from imdsv2": {
imdsRegion: "us-west-2",
expectEndpoint: "sts.us-west-2.amazonaws.com",
imdsRegion: "us-west-2",
expectEndpoint: "sts.us-west-2.amazonaws.com",
expectSignatureRegion: "us-west-2",
},
"us-west-2 fips": {
envRegion: "us-west-2",
fips: true,
expectEndpoint: "sts-fips.us-west-2.amazonaws.com",
envRegion: "us-west-2",
fips: true,
expectEndpoint: "sts-fips.us-west-2.amazonaws.com",
expectSignatureRegion: "us-west-2",
},
"us-west-2 fips with region from imdsv2": {
imdsRegion: "us-west-2",
fips: true,
expectEndpoint: "sts-fips.us-west-2.amazonaws.com",
imdsRegion: "us-west-2",
fips: true,
expectEndpoint: "sts-fips.us-west-2.amazonaws.com",
expectSignatureRegion: "us-west-2",
},
"eu-central-1": {
envRegion: "eu-central-1",
expectEndpoint: "sts.eu-central-1.amazonaws.com",
envRegion: "eu-central-1",
expectEndpoint: "sts.eu-central-1.amazonaws.com",
expectSignatureRegion: "eu-central-1",
},
"eu-central-1 fips": {
envRegion: "eu-central-1",
fips: true,
// All non-US regions have no FIPS endpoint and use the FIPS
// endpoint in us-east-1.
expectEndpoint: "sts-fips.us-east-1.amazonaws.com",
expectEndpoint: "sts-fips.us-east-1.amazonaws.com",
expectSignatureRegion: "us-east-1",
},
"ap-southeast-1": {
envRegion: "ap-southeast-1",
expectEndpoint: "sts.ap-southeast-1.amazonaws.com",
envRegion: "ap-southeast-1",
expectEndpoint: "sts.ap-southeast-1.amazonaws.com",
expectSignatureRegion: "ap-southeast-1",
},
"ap-southeast-1 fips": {
envRegion: "ap-southeast-1",
Expand All @@ -95,17 +106,20 @@ func TestCreateSignedSTSIdentityRequest(t *testing.T) {
// recognized by STS in the default partition. It will fail when
// Auth sends the request to AWS, but this unit test only exercises
// the client-side request generation.
expectEndpoint: "sts-fips.us-east-1.amazonaws.com",
expectEndpoint: "sts-fips.us-east-1.amazonaws.com",
expectSignatureRegion: "us-east-1",
},
"govcloud": {
envRegion: "us-gov-east-1",
expectEndpoint: "sts.us-gov-east-1.amazonaws.com",
envRegion: "us-gov-east-1",
expectEndpoint: "sts.us-gov-east-1.amazonaws.com",
expectSignatureRegion: "us-gov-east-1",
},
"govcloud fips": {
envRegion: "us-gov-east-1",
fips: true,
// All govcloud endpoints are FIPS.
expectEndpoint: "sts.us-gov-east-1.amazonaws.com",
expectEndpoint: "sts.us-gov-east-1.amazonaws.com",
expectSignatureRegion: "us-gov-east-1",
},
} {
t.Run(desc, func(t *testing.T) {
Expand All @@ -132,8 +146,8 @@ func TestCreateSignedSTSIdentityRequest(t *testing.T) {
iam.WithFIPSEndpoint(tc.fips),
iam.WithIMDSClient(imdsClient))
if tc.expectError != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectError)
assert.Error(t, err)
assert.ErrorContains(t, err, tc.expectError)
return
}
require.NoError(t, err)
Expand All @@ -142,12 +156,13 @@ func TestCreateSignedSTSIdentityRequest(t *testing.T) {
// parameters were correctly included by the AWS SDK.
httpReq, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(req)))
require.NoError(t, err)
require.Equal(t, tc.expectEndpoint, httpReq.Host)
assert.Equal(t, tc.expectEndpoint, httpReq.Host)
authHeader := httpReq.Header.Get(aws.AuthorizationHeader)
sigV4, err := aws.ParseSigV4(authHeader)
require.NoError(t, err)
require.Contains(t, sigV4.SignedHeaders, "x-teleport-challenge")
require.Equal(t, challenge, httpReq.Header.Get("x-teleport-challenge"))
assert.Contains(t, sigV4.SignedHeaders, "x-teleport-challenge")
assert.Equal(t, challenge, httpReq.Header.Get("x-teleport-challenge"))
assert.Equal(t, tc.expectSignatureRegion, sigV4.Region, "signature region did not match expected")
})
}
}
Expand Down
Loading