From 415a41ea4854d547ad313172a5fa15f0e7a31ffc Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 7 Mar 2022 13:22:12 -0800 Subject: [PATCH 1/7] support regional STS endpoints for IAM joining --- lib/auth/join_iam.go | 207 +++++++++++++++++++++++++++++++++++--- lib/auth/join_iam_test.go | 74 ++++++++++++++ lib/auth/register.go | 72 ++++++++----- lib/utils/ec2.go | 8 ++ 4 files changed, 322 insertions(+), 39 deletions(-) diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 862827ae0258b..8d22ede5dbfaf 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -23,6 +23,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "io" "net" "net/http" @@ -33,9 +34,12 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils" + apiutils "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/aws" + awssdk "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" "github.com/gravitational/trace" @@ -49,16 +53,103 @@ const ( // ever have a need to allow a newer API version. expectedStsIdentityRequestBody = "Action=GetCallerIdentity&Version=2011-06-15" - // Only allowing the global sts endpoint here, Teleport nodes will only send - // requests for this endpoint. If we want to start using regional endpoints - // we can update this check before updating the nodes. - stsHost = "sts.amazonaws.com" + // 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" ) +// validateSTSHost returns an error if the given stsHost is not a valid regional +// endpoint for the AWS STS service, or nil if it is valid. +// +// This is a security-critical check: we are allowing the client to tell us +// which URL we should use to validate their identity. If the client could pass +// off an attacker-controlled URL as the STS endpoint, the entire security +// mechanism of the IAM join method would be compromised. +// +// The simplest approach would be to make sure that the URL matches +// "sts.(.)?amazonaws.com(.cn)?" but this fails in a couple of ways: +// - it seems that, at least in the past, Amazon was selling subdomains of +// "amazonaws.com", so we don't know who might control that endpoint. +// - Second, it does not match endpoints in the AWS ISO partitions such as +// "sts.us-iso-east-1.c2s.ic.gov". We don't know if AWS will add more STS +// endpoints like this with different URL patterns. +// +// Probably the most secure approach would be to check the given STS URL against +// a static list of known STS endpoints. This would be hard to maintain over +// time as AWS adds new regions and partitions. +// +// Another option would be to query AWS at runtime to get the list of valid STS +// endpoints. I couldn't find a suitable API to support this. +// +// The approach I've chosen here basically offloads the task of maintaining the +// static list of STS endpoints to the AWS SDK. Since I can't get at the list +// directly, I attempt to infer the region from the given URL, and then check if +// the SDK will return an exact match for the given stsHost in that region. As +// new STS endpoints come online, we will need to update the version of +// aws-sdk-go used by Teleport. +// +// TestValidateSTSHost includes a list of all currently known valid STS +// endpoints, and asserts that all pass this check. +func validateSTSHost(stsHost string) error { + for _, p := range endpoints.DefaultPartitions() { + prefix := strings.TrimSuffix(stsHost, p.DNSSuffix()) + if prefix == stsHost { + // The given stsHost does not match this partition's DNS suffix. We + // can continue early in this case, but multiple partitions may have + // the same suffix, e.g. GovCloud and the default partition. + continue + } + + // It's important to include StrictMatchingOption here so that the SDK + // won't fill in a value for an unknown region, which could be used to + // make sts.attacker.amazonaws.com pass the check. + resolveOptions := []func(*endpoints.Options){ + endpoints.StrictMatchingOption, + endpoints.STSRegionalEndpointOption, + } + + // Known STS endpoints match "sts(-fips)?.(.)?" + parts := strings.Split(prefix, ".") + if len(parts) == 0 { + return trace.AccessDenied("invalid STS host %q", stsHost) + } + + switch parts[0] { + case "sts": + case "sts-fips": + resolveOptions = append(resolveOptions, endpoints.UseFIPSEndpointOption) + default: + return trace.AccessDenied("invalid prefix %q for STS host %q", parts[0], stsHost) + } + + region := "" + if len(parts) > 1 { + region = parts[1] + } + + endpoint, err := p.EndpointFor(sts.ServiceName, region, resolveOptions...) + if errors.As(err, &endpoints.UnknownServiceError{}) || errors.As(err, &endpoints.UnknownEndpointError{}) || errors.As(err, &endpoints.EndpointNotFoundError{}) { + // This region is probably not valid in this partition, or there is + // no STS in this partition. Keep iterating. + continue + } else if err != nil { + return trace.AccessDenied("unexpected error resolving STS endpoint: %v", err) + } + + if endpoint.URL == "https://"+stsHost { + // Found an exact match, this is a valid STS endpoint. + return nil + } + // Didn't find a matching endpoint in this partition, this can + // happen if checking the GovCloud partition for a region in the + // default partition or vice-versa. Continue iterating. + } + return trace.AccessDenied("unrecognized STS host %q", stsHost) +} + // validateStsIdentityRequest checks that a received sts:GetCallerIdentity // request is valid and includes the challenge as a signed header. An example // valid request looks like: @@ -77,8 +168,8 @@ const ( // Action=GetCallerIdentity&Version=2011-06-15 // ``` func validateStsIdentityRequest(req *http.Request, challenge string) error { - if req.Host != stsHost { - return trace.AccessDenied("sts identity request is for unknown host %q", req.Host) + if err := validateSTSHost(req.Host); err != nil { + return trace.Wrap(err) } if req.Method != http.MethodPost { @@ -95,7 +186,7 @@ func validateStsIdentityRequest(req *http.Request, challenge string) error { if err != nil { return trace.Wrap(err) } - if !utils.SliceContainsStr(sigV4.SignedHeaders, challengeHeaderKey) { + if !apiutils.SliceContainsStr(sigV4.SignedHeaders, challengeHeaderKey) { return trace.AccessDenied("sts identity request auth header %q does not include "+ challengeHeaderKey+" as a signed header", authHeader) } @@ -125,7 +216,7 @@ func parseSTSRequest(req []byte) (*http.Request, error) { httpReq.RequestURI = "" httpReq.URL = &url.URL{ Scheme: "https", - Host: stsHost, + Host: httpReq.Host, } return httpReq, nil } @@ -335,16 +426,13 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c // createSignedStsIdentityRequest is called on the client side and returns an // sts:GetCallerIdentity request signed with the local AWS credentials -func createSignedStsIdentityRequest(challenge string) ([]byte, error) { - // use the aws sdk to generate the request - sess, err := session.NewSessionWithOptions(session.Options{ - SharedConfigState: session.SharedConfigEnable, - }) +func createSignedStsIdentityRequest(ctx context.Context, endpointOption stsEndpointOption, challenge string) ([]byte, error) { + stsClient, err := endpointOption(ctx) if err != nil { return nil, trace.Wrap(err) } - stsService := sts.New(sess) - req, _ := stsService.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) + + req, _ := stsClient.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) // set challenge header req.HTTPRequest.Header.Set(challengeHeaderKey, challenge) // request json for simpler parsing @@ -360,3 +448,90 @@ func createSignedStsIdentityRequest(challenge string) ([]byte, error) { } 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) + } + 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), + }) + 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) + } else { + // Found the region, set it on the config. + extraConfigOptions.Region = ®ion + } + } + + return sts.New(sess, extraConfigOptions), nil +} + +// getEC2LocalRegion returns the AWS region this EC2 instance is running in, or +// a NotFound error if the EC2 IMDS is unavailable. +func getEC2LocalRegion(ctx context.Context) (string, error) { + imdsClient, err := utils.NewInstanceMetadataClient(ctx) + if err != nil { + return "", trace.Wrap(err) + } + + if !imdsClient.IsAvailable(ctx) { + return "", trace.NotFound("IMDS is unavailable") + } + + region, err := imdsClient.GetRegion(ctx) + return region, trace.Wrap(err) +} diff --git a/lib/auth/join_iam_test.go b/lib/auth/join_iam_test.go index f4e402f5b3801..5e3846397810f 100644 --- a/lib/auth/join_iam_test.go +++ b/lib/auth/join_iam_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/trace" + "github.com/stretchr/testify/require" ) @@ -424,3 +425,76 @@ func TestAuth_RegisterUsingIAMMethod(t *testing.T) { }) } } + +func TestValidateSTSHost(t *testing.T) { + t.Parallel() + for _, validHost := range []string{ + "sts.amazonaws.com", + "sts.us-east-2.amazonaws.com", + "sts-fips.us-east-2.amazonaws.com", + "sts.us-east-1.amazonaws.com", + "sts-fips.us-east-1.amazonaws.com", + "sts.us-west-1.amazonaws.com", + "sts-fips.us-west-1.amazonaws.com", + "sts.us-west-2.amazonaws.com", + "sts-fips.us-west-2.amazonaws.com", + "sts.af-south-1.amazonaws.com", + "sts.ap-east-1.amazonaws.com", + "sts.ap-southeast-3.amazonaws.com", + "sts.ap-south-1.amazonaws.com", + "sts.ap-northeast-3.amazonaws.com", + "sts.ap-northeast-2.amazonaws.com", + "sts.ap-southeast-1.amazonaws.com", + "sts.ap-southeast-2.amazonaws.com", + "sts.ap-northeast-1.amazonaws.com", + "sts.ca-central-1.amazonaws.com", + "sts.eu-central-1.amazonaws.com", + "sts.eu-west-1.amazonaws.com", + "sts.eu-west-2.amazonaws.com", + "sts.eu-south-1.amazonaws.com", + "sts.eu-west-3.amazonaws.com", + "sts.eu-north-1.amazonaws.com", + "sts.me-south-1.amazonaws.com", + "sts.sa-east-1.amazonaws.com", + "sts.us-gov-east-1.amazonaws.com", + "sts.us-gov-west-1.amazonaws.com", + "sts.cn-north-1.amazonaws.com.cn", + "sts.cn-northwest-1.amazonaws.com.cn", + "sts.us-iso-east-1.c2s.ic.gov", + "sts.us-iso-west-1.c2s.ic.gov", + "sts.us-isob-east-1.sc2s.sgov.gov", + } { + t.Run(validHost, func(t *testing.T) { + err := validateSTSHost(validHost) + require.NoError(t, err, "unexpected error validating hostname %s: %v", validHost, err) + }) + } + for _, invalidHost := range []string{ + "", + "sts.com", + "sts.example.com", + "sts.us-east-1.com", + "sts.us-attacker-1.amazonaws.com", + "sts.cn-attacker-1.amazonaws.com.cn", + "sts.us-east-1.amazonaws.com.cn", + "sts.us-attacker-1.c2s.ic.gov", + "sts.us-attacker-1.sc2s.sgov.gov", + "sts.us-east-1.attacker.amazonaws.com", + "sts.cn-north-1.attacker.amazonaws.com.cn", + "sts.us-iso-east-1.attacker.c2s.ic.gov", + "sts.us-isob-east-1.attacker.sc2s.sgov.gov", + "example.com", + "com", + "sts", + "sts..amazonaws.com", + ".", + "..", + "...", + "sts.💩.amazonaws.com", + } { + t.Run(invalidHost, func(t *testing.T) { + err := validateSTSHost(invalidHost) + require.True(t, trace.IsAccessDenied(err), "expected AccessDenied error while validating hostname %s, got: %T %v", invalidHost, err, err) + }) + } +} diff --git a/lib/auth/register.go b/lib/auth/register.go index a863f0b440aff..02b5b9352069c 100644 --- a/lib/auth/register.go +++ b/lib/auth/register.go @@ -463,32 +463,58 @@ type joinServiceClient interface { func registerUsingIAMMethod(joinServiceClient joinServiceClient, token string, params RegisterParams) (*proto.Certs, error) { ctx := context.Background() - // call RegisterUsingIAMMethod with a callback to respond to the challenge - // with the 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(challenge) + // Attempt to use the regional STS endpoint, fall back to using the global + // endpoint. The regional endpoint may fail if Auth is on an older version + // which does not support regional endpoints, the STS service is not + // enabled in the current region, or an unknown AWS region is configured. + var errs []error + for _, s := range []struct { + desc string + opt stsEndpointOption + }{ + { + desc: "regional", + opt: stsEndpointOptionRegional, + }, + { + desc: "global", + opt: stsEndpointOptionGlobal, + }, + } { + 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) + if err != nil { + return nil, trace.Wrap(err) + } + + // send the register request including the challenge response + return &proto.RegisterUsingIAMMethodRequest{ + RegisterUsingTokenRequest: &types.RegisterUsingTokenRequest{ + Token: token, + HostID: params.ID.HostUUID, + NodeName: params.ID.NodeName, + Role: params.ID.Role, + AdditionalPrincipals: params.AdditionalPrincipals, + DNSNames: params.DNSNames, + PublicTLSKey: params.PublicTLSKey, + PublicSSHKey: params.PublicSSHKey, + }, + StsIdentityRequest: signedRequest, + }, nil + }) if err != nil { - return nil, trace.Wrap(err) + log.WithError(err).Infof("Failed to register %s using %s STS endpoint", params.ID.Role, s.desc) + errs = append(errs, err) + } else { + log.Infof("Successfully registered %s with IAM method using %s STS endpoint", params.ID.Role, s.desc) + return certs, nil } + } - // send the register request including the challenge response - return &proto.RegisterUsingIAMMethodRequest{ - RegisterUsingTokenRequest: &types.RegisterUsingTokenRequest{ - Token: token, - HostID: params.ID.HostUUID, - NodeName: params.ID.NodeName, - Role: params.ID.Role, - AdditionalPrincipals: params.AdditionalPrincipals, - DNSNames: params.DNSNames, - PublicTLSKey: params.PublicTLSKey, - PublicSSHKey: params.PublicSSHKey, - }, - StsIdentityRequest: signedRequest, - }, nil - }) - - return certs, trace.Wrap(err) + return nil, trace.NewAggregate(errs...) } // ReRegisterParams specifies parameters for re-registering diff --git a/lib/utils/ec2.go b/lib/utils/ec2.go index c94ebc6d12eec..bdf84a0fae70b 100644 --- a/lib/utils/ec2.go +++ b/lib/utils/ec2.go @@ -178,3 +178,11 @@ func (client *InstanceMetadataClient) GetTagValue(ctx context.Context, key strin } return body, nil } + +func (client *InstanceMetadataClient) GetRegion(ctx context.Context) (string, error) { + getRegionOutput, err := client.c.GetRegion(ctx, nil) + if err != nil { + return "", trace.Wrap(err) + } + return getRegionOutput.Region, nil +} From 62db6bbfeff3ce3280cec2d5788932559bd095d6 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 8 Aug 2022 18:27:43 -0700 Subject: [PATCH 2/7] update docs --- docs/pages/setup/guides/joining-nodes-aws-iam.mdx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/pages/setup/guides/joining-nodes-aws-iam.mdx b/docs/pages/setup/guides/joining-nodes-aws-iam.mdx index 8a97280be5608..7b891945aefe9 100644 --- a/docs/pages/setup/guides/joining-nodes-aws-iam.mdx +++ b/docs/pages/setup/guides/joining-nodes-aws-iam.mdx @@ -58,15 +58,6 @@ The IAM join method will not work if TLS is terminated at a load balancer in front of your Teleport Proxy Service unless the Node using this method is connecting directly to the Auth Service. -The IAM join method is currently not supported in the AWS China or GovCloud -partitions. - - - - -The IAM join method is currently not supported in the AWS China or GovCloud -partitions. - ## Prerequisites @@ -169,4 +160,4 @@ proxy_service: ## Step 4/4. Launch your Teleport Node Start Teleport on the Node and confirm that it is able to connect to and join -your cluster. You're all set! \ No newline at end of file +your cluster. You're all set! From 31a27cda44f6bb07c614d401ac67d707deee73a8 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 12 Aug 2022 11:59:27 -0700 Subject: [PATCH 3/7] s/Sts/STS/g --- lib/auth/join_iam.go | 22 +++++++++++----------- lib/auth/register.go | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 8d22ede5dbfaf..30122e8639568 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -51,7 +51,7 @@ const ( // update our AWS SDK dependency. Since Auth should always be upgraded // before nodes, we will have a chance to update the check on Auth if we // ever have a need to allow a newer API version. - expectedStsIdentityRequestBody = "Action=GetCallerIdentity&Version=2011-06-15" + 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" @@ -150,7 +150,7 @@ func validateSTSHost(stsHost string) error { return trace.AccessDenied("unrecognized STS host %q", stsHost) } -// validateStsIdentityRequest checks that a received sts:GetCallerIdentity +// validateSTSIdentityRequest checks that a received sts:GetCallerIdentity // request is valid and includes the challenge as a signed header. An example // valid request looks like: // ``` @@ -167,7 +167,7 @@ func validateSTSHost(stsHost string) error { // // Action=GetCallerIdentity&Version=2011-06-15 // ``` -func validateStsIdentityRequest(req *http.Request, challenge string) error { +func validateSTSIdentityRequest(req *http.Request, challenge string) error { if err := validateSTSHost(req.Host); err != nil { return trace.Wrap(err) } @@ -195,8 +195,8 @@ func validateStsIdentityRequest(req *http.Request, challenge string) error { if err != nil { return trace.Wrap(err) } - if !bytes.Equal([]byte(expectedStsIdentityRequestBody), body) { - return trace.BadParameter("sts request body %q does not equal expected %q", string(body), expectedStsIdentityRequestBody) + if !bytes.Equal([]byte(expectedSTSIdentityRequestBody), body) { + return trace.BadParameter("sts request body %q does not equal expected %q", string(body), expectedSTSIdentityRequestBody) } return nil @@ -252,9 +252,9 @@ func stsClientFromContext(ctx context.Context) stsClient { return http.DefaultClient } -// executeStsIdentityRequest sends the sts:GetCallerIdentity HTTP request to the +// executeSTSIdentityRequest sends the sts:GetCallerIdentity HTTP request to the // AWS API, parses the response, and returns the awsIdentity -func executeStsIdentityRequest(ctx context.Context, req *http.Request) (*awsIdentity, error) { +func executeSTSIdentityRequest(ctx context.Context, req *http.Request) (*awsIdentity, error) { client := stsClientFromContext(ctx) // set the http request context so it can be canceled @@ -351,13 +351,13 @@ func (a *Server) checkIAMRequest(ctx context.Context, challenge string, req *pro // validate that the host, method, and headers are correct and the expected // challenge is included in the signed portion of the request - if err := validateStsIdentityRequest(identityRequest, challenge); err != nil { + if err := validateSTSIdentityRequest(identityRequest, challenge); err != nil { return trace.Wrap(err) } // send the signed request to the public AWS API and get the node identity // from the response - identity, err := executeStsIdentityRequest(ctx, identityRequest) + identity, err := executeSTSIdentityRequest(ctx, identityRequest) if err != nil { return trace.Wrap(err) } @@ -424,9 +424,9 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c return certs, trace.Wrap(err) } -// createSignedStsIdentityRequest is called on the client side and returns an +// 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) { +func createSignedSTSIdentityRequest(ctx context.Context, endpointOption stsEndpointOption, challenge string) ([]byte, error) { stsClient, err := endpointOption(ctx) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/auth/register.go b/lib/auth/register.go index 02b5b9352069c..c094ea8b9f051 100644 --- a/lib/auth/register.go +++ b/lib/auth/register.go @@ -485,7 +485,7 @@ func registerUsingIAMMethod(joinServiceClient joinServiceClient, token string, p // 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, s.opt, challenge) if err != nil { return nil, trace.Wrap(err) } From 2a5e238763494b7f3f0f4614467f7f3c7c6e2d4e Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 12 Aug 2022 14:08:43 -0700 Subject: [PATCH 4/7] validate against static list of known STS endpoints --- lib/auth/join_iam.go | 77 ++++++++++----------------------- lib/auth/join_iam_test.go | 73 ------------------------------- lib/auth/valid_sts_endpoints.go | 58 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 128 deletions(-) create mode 100644 lib/auth/valid_sts_endpoints.go diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 30122e8639568..5c08fcac3fd23 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -23,7 +23,6 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" - "errors" "io" "net" "net/http" @@ -94,60 +93,20 @@ const ( // TestValidateSTSHost includes a list of all currently known valid STS // endpoints, and asserts that all pass this check. func validateSTSHost(stsHost string) error { - for _, p := range endpoints.DefaultPartitions() { - prefix := strings.TrimSuffix(stsHost, p.DNSSuffix()) - if prefix == stsHost { - // The given stsHost does not match this partition's DNS suffix. We - // can continue early in this case, but multiple partitions may have - // the same suffix, e.g. GovCloud and the default partition. - continue - } - - // It's important to include StrictMatchingOption here so that the SDK - // won't fill in a value for an unknown region, which could be used to - // make sts.attacker.amazonaws.com pass the check. - resolveOptions := []func(*endpoints.Options){ - endpoints.StrictMatchingOption, - endpoints.STSRegionalEndpointOption, - } - - // Known STS endpoints match "sts(-fips)?.(.)?" - parts := strings.Split(prefix, ".") - if len(parts) == 0 { - return trace.AccessDenied("invalid STS host %q", stsHost) - } - - switch parts[0] { - case "sts": - case "sts-fips": - resolveOptions = append(resolveOptions, endpoints.UseFIPSEndpointOption) - default: - return trace.AccessDenied("invalid prefix %q for STS host %q", parts[0], stsHost) - } - - region := "" - if len(parts) > 1 { - region = parts[1] - } - - endpoint, err := p.EndpointFor(sts.ServiceName, region, resolveOptions...) - if errors.As(err, &endpoints.UnknownServiceError{}) || errors.As(err, &endpoints.UnknownEndpointError{}) || errors.As(err, &endpoints.EndpointNotFoundError{}) { - // This region is probably not valid in this partition, or there is - // no STS in this partition. Keep iterating. - continue - } else if err != nil { - return trace.AccessDenied("unexpected error resolving STS endpoint: %v", err) - } - - if endpoint.URL == "https://"+stsHost { - // Found an exact match, this is a valid STS endpoint. - return nil - } - // Didn't find a matching endpoint in this partition, this can - // happen if checking the GovCloud partition for a region in the - // default partition or vice-versa. Continue iterating. + valid := apiutils.SliceContainsStr(validSTSEndpoints, stsHost) + if valid { + return nil } - return trace.AccessDenied("unrecognized STS host %q", stsHost) + + return trace.AccessDenied("IAM join request uses unknown STS host %q. "+ + "This could mean that the Teleport Node attempting to join the cluster is "+ + "running in a new AWS region which is unknown to this Teleport auth server. "+ + "If this is the case, please check if the endpoint is included in the newest "+ + "list of known valid endpoints at https://github.com/gravitational/teleport/blob/master/lib/auth/valid_sts_endpoints.go "+ + "and consider upgrading your Teleport binary to the latest version. "+ + "If it is not included there, please submit a GitHub issue and we will add it. "+ + "Alternatively, if this URL looks suspicious, an attacker may be attempting to "+ + "join your Teleport cluster.", stsHost) } // validateSTSIdentityRequest checks that a received sts:GetCallerIdentity @@ -167,7 +126,15 @@ func validateSTSHost(stsHost string) error { // // Action=GetCallerIdentity&Version=2011-06-15 // ``` -func validateSTSIdentityRequest(req *http.Request, challenge string) error { +func validateSTSIdentityRequest(req *http.Request, challenge string) (err error) { + defer func() { + // Always log a warning on the Auth server if the function detects and + // invalid request. + if err != nil { + log.WithError(err).Warn("Invalid sts:GetCallerIdentity detected by client attempting to use the IAM join method.") + } + }() + if err := validateSTSHost(req.Host); err != nil { return trace.Wrap(err) } diff --git a/lib/auth/join_iam_test.go b/lib/auth/join_iam_test.go index 5e3846397810f..e4804a75f3b13 100644 --- a/lib/auth/join_iam_test.go +++ b/lib/auth/join_iam_test.go @@ -425,76 +425,3 @@ func TestAuth_RegisterUsingIAMMethod(t *testing.T) { }) } } - -func TestValidateSTSHost(t *testing.T) { - t.Parallel() - for _, validHost := range []string{ - "sts.amazonaws.com", - "sts.us-east-2.amazonaws.com", - "sts-fips.us-east-2.amazonaws.com", - "sts.us-east-1.amazonaws.com", - "sts-fips.us-east-1.amazonaws.com", - "sts.us-west-1.amazonaws.com", - "sts-fips.us-west-1.amazonaws.com", - "sts.us-west-2.amazonaws.com", - "sts-fips.us-west-2.amazonaws.com", - "sts.af-south-1.amazonaws.com", - "sts.ap-east-1.amazonaws.com", - "sts.ap-southeast-3.amazonaws.com", - "sts.ap-south-1.amazonaws.com", - "sts.ap-northeast-3.amazonaws.com", - "sts.ap-northeast-2.amazonaws.com", - "sts.ap-southeast-1.amazonaws.com", - "sts.ap-southeast-2.amazonaws.com", - "sts.ap-northeast-1.amazonaws.com", - "sts.ca-central-1.amazonaws.com", - "sts.eu-central-1.amazonaws.com", - "sts.eu-west-1.amazonaws.com", - "sts.eu-west-2.amazonaws.com", - "sts.eu-south-1.amazonaws.com", - "sts.eu-west-3.amazonaws.com", - "sts.eu-north-1.amazonaws.com", - "sts.me-south-1.amazonaws.com", - "sts.sa-east-1.amazonaws.com", - "sts.us-gov-east-1.amazonaws.com", - "sts.us-gov-west-1.amazonaws.com", - "sts.cn-north-1.amazonaws.com.cn", - "sts.cn-northwest-1.amazonaws.com.cn", - "sts.us-iso-east-1.c2s.ic.gov", - "sts.us-iso-west-1.c2s.ic.gov", - "sts.us-isob-east-1.sc2s.sgov.gov", - } { - t.Run(validHost, func(t *testing.T) { - err := validateSTSHost(validHost) - require.NoError(t, err, "unexpected error validating hostname %s: %v", validHost, err) - }) - } - for _, invalidHost := range []string{ - "", - "sts.com", - "sts.example.com", - "sts.us-east-1.com", - "sts.us-attacker-1.amazonaws.com", - "sts.cn-attacker-1.amazonaws.com.cn", - "sts.us-east-1.amazonaws.com.cn", - "sts.us-attacker-1.c2s.ic.gov", - "sts.us-attacker-1.sc2s.sgov.gov", - "sts.us-east-1.attacker.amazonaws.com", - "sts.cn-north-1.attacker.amazonaws.com.cn", - "sts.us-iso-east-1.attacker.c2s.ic.gov", - "sts.us-isob-east-1.attacker.sc2s.sgov.gov", - "example.com", - "com", - "sts", - "sts..amazonaws.com", - ".", - "..", - "...", - "sts.💩.amazonaws.com", - } { - t.Run(invalidHost, func(t *testing.T) { - err := validateSTSHost(invalidHost) - require.True(t, trace.IsAccessDenied(err), "expected AccessDenied error while validating hostname %s, got: %T %v", invalidHost, err, err) - }) - } -} diff --git a/lib/auth/valid_sts_endpoints.go b/lib/auth/valid_sts_endpoints.go new file mode 100644 index 0000000000000..48f89c2ca9e33 --- /dev/null +++ b/lib/auth/valid_sts_endpoints.go @@ -0,0 +1,58 @@ +/* +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", +} From 421cc5f19aa72378331da04ec9bacf9cf2231e22 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 12 Aug 2022 14:12:23 -0700 Subject: [PATCH 5/7] fix typo --- lib/auth/join_iam.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 5c08fcac3fd23..baafd1f15ebea 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -128,10 +128,11 @@ func validateSTSHost(stsHost string) error { // ``` func validateSTSIdentityRequest(req *http.Request, challenge string) (err error) { defer func() { - // Always log a warning on the Auth server if the function detects and - // invalid request. + // Always log a warning on the Auth server if the function detects an + // invalid sts:GetCallerIdentity request, it's either going to be caused + // by a node in a unknown region or an attacker. if err != nil { - log.WithError(err).Warn("Invalid sts:GetCallerIdentity detected by client attempting to use the IAM join method.") + log.WithError(err).Warn("Detected an invalid sts:GetCallerIdentity used by a client attempting to use the IAM join method.") } }() From 1c6e113a683a424d5804c280ababc216a9b8ed5a Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 12 Aug 2022 14:23:48 -0700 Subject: [PATCH 6/7] update comment --- lib/auth/join_iam.go | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index baafd1f15ebea..49195d4d8e8a0 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -68,30 +68,9 @@ const ( // off an attacker-controlled URL as the STS endpoint, the entire security // mechanism of the IAM join method would be compromised. // -// The simplest approach would be to make sure that the URL matches -// "sts.(.)?amazonaws.com(.cn)?" but this fails in a couple of ways: -// - it seems that, at least in the past, Amazon was selling subdomains of -// "amazonaws.com", so we don't know who might control that endpoint. -// - Second, it does not match endpoints in the AWS ISO partitions such as -// "sts.us-iso-east-1.c2s.ic.gov". We don't know if AWS will add more STS -// endpoints like this with different URL patterns. -// -// Probably the most secure approach would be to check the given STS URL against -// a static list of known STS endpoints. This would be hard to maintain over -// time as AWS adds new regions and partitions. -// -// Another option would be to query AWS at runtime to get the list of valid STS -// endpoints. I couldn't find a suitable API to support this. -// -// The approach I've chosen here basically offloads the task of maintaining the -// static list of STS endpoints to the AWS SDK. Since I can't get at the list -// directly, I attempt to infer the region from the given URL, and then check if -// the SDK will return an exact match for the given stsHost in that region. As -// new STS endpoints come online, we will need to update the version of -// aws-sdk-go used by Teleport. -// -// TestValidateSTSHost includes a list of all currently known valid STS -// endpoints, and asserts that all pass this check. +// To keep this validation simple and secure, we check the given endpoint +// against a static list of known valid endpoints. We will need to update this +// list as AWS adds new regions. func validateSTSHost(stsHost string) error { valid := apiutils.SliceContainsStr(validSTSEndpoints, stsHost) if valid { From c12ffba22d511480c870630afc21425d7871c405 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 26 Aug 2022 16:06:38 -0700 Subject: [PATCH 7/7] reword error message --- lib/auth/join_iam.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 49195d4d8e8a0..916a53d52b924 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -80,12 +80,12 @@ func validateSTSHost(stsHost string) error { return trace.AccessDenied("IAM join request uses unknown STS host %q. "+ "This could mean that the Teleport Node attempting to join the cluster is "+ "running in a new AWS region which is unknown to this Teleport auth server. "+ - "If this is the case, please check if the endpoint is included in the newest "+ - "list of known valid endpoints at https://github.com/gravitational/teleport/blob/master/lib/auth/valid_sts_endpoints.go "+ - "and consider upgrading your Teleport binary to the latest version. "+ - "If it is not included there, please submit a GitHub issue and we will add it. "+ "Alternatively, if this URL looks suspicious, an attacker may be attempting to "+ - "join your Teleport cluster.", stsHost) + "join your Teleport cluster. "+ + "Following is the list of valid STS endpoints known to this auth server. "+ + "If a legitimate STS endpoint is not included, please file an issue at "+ + "https://github.com/gravitational/teleport. %v", + stsHost, validSTSEndpoints) } // validateSTSIdentityRequest checks that a received sts:GetCallerIdentity