diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 7b64cc84fa424..45281abc539af 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -52,9 +52,6 @@ const ( // ever have a need to allow a newer API version. expectedSTSIdentityRequestBody = "Action=GetCallerIdentity&Version=2011-06-15" - // Used to check if we were unable to resolve the regional STS endpoint. - globalSTSEndpoint = "https://sts.amazonaws.com" - // AWS SignedHeaders will always be lowercase // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html#sigv4-auth-header-overview challengeHeaderKey = "x-teleport-challenge" @@ -371,10 +368,42 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c return certs, trace.Wrap(err) } +type stsIdentityRequestConfig struct { + regionalEndpointOption endpoints.STSRegionalEndpoint + fipsEndpointOption endpoints.FIPSEndpointState +} + +type stsIdentityRequestOption func(cfg *stsIdentityRequestConfig) + +func withRegionalEndpoint(useRegionalEndpoint bool) stsIdentityRequestOption { + return func(cfg *stsIdentityRequestConfig) { + if useRegionalEndpoint { + cfg.regionalEndpointOption = endpoints.RegionalSTSEndpoint + } else { + cfg.regionalEndpointOption = endpoints.LegacySTSEndpoint + } + } +} + +func withFIPSEndpoint(useFIPS bool) stsIdentityRequestOption { + return func(cfg *stsIdentityRequestConfig) { + if useFIPS { + cfg.fipsEndpointOption = endpoints.FIPSEndpointStateEnabled + } else { + cfg.fipsEndpointOption = endpoints.FIPSEndpointStateDisabled + } + } +} + // createSignedSTSIdentityRequest is called on the client side and returns an // sts:GetCallerIdentity request signed with the local AWS credentials -func createSignedSTSIdentityRequest(ctx context.Context, endpointOption stsEndpointOption, challenge string) ([]byte, error) { - stsClient, err := endpointOption(ctx) +func createSignedSTSIdentityRequest(ctx context.Context, challenge string, opts ...stsIdentityRequestOption) ([]byte, error) { + cfg := &stsIdentityRequestConfig{} + for _, opt := range opts { + opt(cfg) + } + + stsClient, err := newSTSClient(ctx, cfg) if err != nil { return nil, trace.Wrap(err) } @@ -396,75 +425,52 @@ func createSignedSTSIdentityRequest(ctx context.Context, endpointOption stsEndpo return signedRequest.Bytes(), nil } -type stsEndpointOption func(context.Context) (*sts.STS, error) - -var ( - stsEndpointOptionGlobal = newGlobalSTSClient - stsEndpointOptionRegional = newRegionalSTSClient -) - -// newRegionalSTSClient returns an STS client will resolve the "global" endpoint -// for the STS service. -func newGlobalSTSClient(ctx context.Context) (*sts.STS, error) { - // sess will be used as a ConfigProvider to be passed to sts.New. It will - // load AWS configuration options from the environment, which means that AWS - // credentials may come from environment variables, files in ~/.aws/, or - // from the attached role on an EC2 instance. - sess, err := session.NewSessionWithOptions(session.Options{ - SharedConfigState: session.SharedConfigEnable, - }) - if err != nil { - return nil, trace.Wrap(err) +func newSTSClient(ctx context.Context, cfg *stsIdentityRequestConfig) (*sts.STS, error) { + awsConfig := awssdk.Config{ + UseFIPSEndpoint: cfg.fipsEndpointOption, + STSRegionalEndpoint: cfg.regionalEndpointOption, } - return sts.New(sess), nil -} - -// newRegionalSTSClient returns an STS client which attempts to resolve the local -// regional endpoint for the STS service, rather than the "global" endpoint -// which is not supported in non-default AWS partitions. -func newRegionalSTSClient(ctx context.Context) (*sts.STS, error) { - // sess will be used as a ConfigProvider to be passed to sts.New. It will - // load AWS configuration options from the environment, which means that AWS - // credentials may come from environment variables, files in ~/.aws/, or - // from the attached role on an EC2 instance. The regional STS endpoint will - // be used instead of the global endopint if the local (or preferred) region - // can be resolved from the environment. sess, err := session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, - Config: *awssdk.NewConfig().WithSTSRegionalEndpoint(endpoints.RegionalSTSEndpoint), + Config: awsConfig, }) if err != nil { return nil, trace.Wrap(err) } - // will set the local region on extraConfigOptions if we can find it from - // the environment or IMDS - extraConfigOptions := awssdk.NewConfig() - - // If the region was not resolved from the environment the client will try to - // use the global STS endpoint, which will not be supported if the AWS identity - // being used is for a non-default AWS partition (such as China or - // GovCloud.) This is the default behavior on EC2, so let's try to find the - // region from the IMDS. - if clientConfig := sess.ClientConfig(sts.ServiceName); clientConfig.Endpoint == globalSTSEndpoint { - region, err := getEC2LocalRegion(ctx) - if trace.IsNotFound(err) { - // Unfortunately we could not find the region from the IMDS, go with - // the default global endpoint and hope it works. - log.Info("Unable to find the local AWS region from the environment or IMDSv2. " + - "Attempting to use the global STS endpoint for the IAM join method. " + - "This will probably fail in non-default AWS partitions such as China or GovCloud. " + - "Consider setting the AWS_REGION environment variable, setting the region in ~/.aws/config, or enabling the IMDSv2.") - } else if err != nil { - // Return the unexpected error. - return nil, trace.Wrap(err) + stsClient := sts.New(sess) + + if apiutils.SliceContainsStr(globalSTSEndpoints, strings.TrimPrefix(stsClient.Endpoint, "https://")) { + // If the caller wants to use the regional endpoint but it was not resolved + // from the environment, attempt to find the region from the EC2 IMDS + if cfg.regionalEndpointOption == endpoints.RegionalSTSEndpoint { + region, err := getEC2LocalRegion(ctx) + if err != nil { + return nil, trace.Wrap(err, "failed to resolve local AWS region from environment or IMDS") + } + stsClient = sts.New(sess, awssdk.NewConfig().WithRegion(region)) } else { - // Found the region, set it on the config. - extraConfigOptions.Region = ®ion + log.Info("Attempting to use the global STS endpoint for the IAM join method. " + + "This will probably fail in non-default AWS partitions such as China or GovCloud, or if FIPS mode is enabled. " + + "Consider setting the AWS_REGION environment variable, setting the region in ~/.aws/config, or enabling the IMDSv2.") } } - return sts.New(sess, extraConfigOptions), nil + if cfg.fipsEndpointOption == endpoints.FIPSEndpointStateEnabled && + !apiutils.SliceContainsStr(validSTSEndpoints, strings.TrimPrefix(stsClient.Endpoint, "https://")) { + // The AWS SDK will generate invalid endpoints when attempting to + // resolve the FIPS endpoint for a region which does not have one. + // In this case, try to use the FIPS endpoint in us-east-1. This should + // work for all regions in the standard partition. In GovCloud we should + // not hit this because all regional endpoints support FIPS. In China or + // other partitions this will fail and FIPS mode will not be supported. + log.Infof("AWS SDK resolved FIPS STS endpoint %s, which does not appear to be valid. "+ + "Attempting to use the FIPS STS endpoint for us-east-1.", + stsClient.Endpoint) + stsClient = sts.New(sess, awssdk.NewConfig().WithRegion("us-east-1")) + } + + return stsClient, nil } // getEC2LocalRegion returns the AWS region this EC2 instance is running in, or diff --git a/lib/auth/register.go b/lib/auth/register.go index 44315dfa1554d..149fb0c71080b 100644 --- a/lib/auth/register.go +++ b/lib/auth/register.go @@ -120,6 +120,8 @@ type RegisterParams struct { ec2IdentityDocument []byte // CircuitBreakerConfig defines how the circuit breaker should behave. CircuitBreakerConfig breaker.Config + // FIPS means FedRAMP/FIPS 140-2 compliant configuration was requested. + FIPS bool } func (r *RegisterParams) setDefaults() { @@ -471,22 +473,31 @@ func registerUsingIAMMethod(joinServiceClient joinServiceClient, token string, p var errs []error for _, s := range []struct { desc string - opt stsEndpointOption + opts []stsIdentityRequestOption }{ { desc: "regional", - opt: stsEndpointOptionRegional, + opts: []stsIdentityRequestOption{ + withFIPSEndpoint(params.FIPS), + withRegionalEndpoint(true), + }, }, { desc: "global", - opt: stsEndpointOptionGlobal, + opts: []stsIdentityRequestOption{ + // Global endpoint does not support FIPS, this is a fallback + // when joining a cluster with an auth server on an older + // version which does not yet support regional endpoints. + withFIPSEndpoint(false), + withRegionalEndpoint(false), + }, }, } { log.Infof("Attempting to register %s with IAM method using %s STS endpoint", params.ID.Role, s.desc) // Call RegisterUsingIAMMethod and pass a callback to respond to the challenge with a signed join request. certs, err := joinServiceClient.RegisterUsingIAMMethod(ctx, func(challenge string) (*proto.RegisterUsingIAMMethodRequest, error) { // create the signed sts:GetCallerIdentity request and include the challenge - signedRequest, err := createSignedSTSIdentityRequest(ctx, s.opt, challenge) + signedRequest, err := createSignedSTSIdentityRequest(ctx, challenge, s.opts...) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/sts_endpoints.go b/lib/auth/sts_endpoints.go new file mode 100644 index 0000000000000..04de51cc1b787 --- /dev/null +++ b/lib/auth/sts_endpoints.go @@ -0,0 +1,67 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +var ( + // validSTSEndpoints holds a sorted list of all known valid public endpoints for + // the AWS STS service. You can generate this list by running + // $ go run github.com/nklaassen/sts-endpoints@latest --go-list + // Update aws-sdk-go in that package to learn about new endpoints. + validSTSEndpoints = []string{ + "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.af-south-1.amazonaws.com", + "sts.amazonaws.com", + "sts.ap-east-1.amazonaws.com", + "sts.ap-northeast-1.amazonaws.com", + "sts.ap-northeast-2.amazonaws.com", + "sts.ap-northeast-3.amazonaws.com", + "sts.ap-south-1.amazonaws.com", + "sts.ap-southeast-1.amazonaws.com", + "sts.ap-southeast-2.amazonaws.com", + "sts.ap-southeast-3.amazonaws.com", + "sts.ca-central-1.amazonaws.com", + "sts.cn-north-1.amazonaws.com.cn", + "sts.cn-northwest-1.amazonaws.com.cn", + "sts.eu-central-1.amazonaws.com", + "sts.eu-north-1.amazonaws.com", + "sts.eu-south-1.amazonaws.com", + "sts.eu-west-1.amazonaws.com", + "sts.eu-west-2.amazonaws.com", + "sts.eu-west-3.amazonaws.com", + "sts.me-south-1.amazonaws.com", + "sts.sa-east-1.amazonaws.com", + "sts.us-east-1.amazonaws.com", + "sts.us-east-2.amazonaws.com", + "sts.us-gov-east-1.amazonaws.com", + "sts.us-gov-west-1.amazonaws.com", + "sts.us-iso-east-1.c2s.ic.gov", + "sts.us-iso-west-1.c2s.ic.gov", + "sts.us-isob-east-1.sc2s.sgov.gov", + "sts.us-west-1.amazonaws.com", + "sts.us-west-2.amazonaws.com", + } + + globalSTSEndpoints = []string{ + "sts.amazonaws.com", + // This is not a real endpoint, but the SDK will select it if + // AWS_USE_FIPS_ENDPOINT is set and a region is not. + "sts-fips.aws-global.amazonaws.com", + } +) diff --git a/lib/auth/valid_sts_endpoints.go b/lib/auth/valid_sts_endpoints.go deleted file mode 100644 index 48f89c2ca9e33..0000000000000 --- a/lib/auth/valid_sts_endpoints.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2022 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package auth - -// validSTSEndpoints holds a sorted list of all known valid public endpoints for -// the AWS STS service. You can generate this list by running -// $ go run github.com/nklaassen/sts-endpoints@latest --go-list -// Update aws-sdk-go in that package to learn about new endpoints. -var validSTSEndpoints = []string{ - "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.af-south-1.amazonaws.com", - "sts.amazonaws.com", - "sts.ap-east-1.amazonaws.com", - "sts.ap-northeast-1.amazonaws.com", - "sts.ap-northeast-2.amazonaws.com", - "sts.ap-northeast-3.amazonaws.com", - "sts.ap-south-1.amazonaws.com", - "sts.ap-southeast-1.amazonaws.com", - "sts.ap-southeast-2.amazonaws.com", - "sts.ap-southeast-3.amazonaws.com", - "sts.ca-central-1.amazonaws.com", - "sts.cn-north-1.amazonaws.com.cn", - "sts.cn-northwest-1.amazonaws.com.cn", - "sts.eu-central-1.amazonaws.com", - "sts.eu-north-1.amazonaws.com", - "sts.eu-south-1.amazonaws.com", - "sts.eu-west-1.amazonaws.com", - "sts.eu-west-2.amazonaws.com", - "sts.eu-west-3.amazonaws.com", - "sts.me-south-1.amazonaws.com", - "sts.sa-east-1.amazonaws.com", - "sts.us-east-1.amazonaws.com", - "sts.us-east-2.amazonaws.com", - "sts.us-gov-east-1.amazonaws.com", - "sts.us-gov-west-1.amazonaws.com", - "sts.us-iso-east-1.c2s.ic.gov", - "sts.us-iso-west-1.c2s.ic.gov", - "sts.us-isob-east-1.sc2s.sgov.gov", - "sts.us-west-1.amazonaws.com", - "sts.us-west-2.amazonaws.com", -} diff --git a/lib/service/connect.go b/lib/service/connect.go index 1741f882b0081..113485d0f9b7e 100644 --- a/lib/service/connect.go +++ b/lib/service/connect.go @@ -604,6 +604,7 @@ func (process *TeleportProcess) firstTimeConnect(role types.SystemRole) (*Connec Clock: process.Clock, JoinMethod: process.Config.JoinMethod, CircuitBreakerConfig: process.Config.CircuitBreakerConfig, + FIPS: process.Config.FIPS, }) if err != nil { return nil, trace.Wrap(err)