From f073bc10564288c77cd84094ae8598d075565181 Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Sat, 20 Feb 2021 14:03:29 -0800 Subject: [PATCH 1/8] Support GET requests for aws-iam This is required to support presigned requests from aws-sdk-go-v2 --- builtin/credential/aws/backend.go | 1 + builtin/credential/aws/path_config_client.go | 19 +++++ builtin/credential/aws/path_login.go | 75 ++++++++++++-------- builtin/credential/aws/path_login_test.go | 10 +-- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index e8424f2c4956..07de023814af 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -33,6 +33,7 @@ var defaultAllowedSTSRequestHeaders = []string{ "X-Amz-Security-Token", "X-Amz-Signature", "X-Amz-SignedHeaders", + "X-Amz-User-Agent", } func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 04f8f238d709..8cb60370e210 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -8,6 +8,7 @@ import ( "errors" "net/http" "net/textproto" + "net/url" "strings" "github.com/aws/aws-sdk-go/aws" @@ -388,6 +389,9 @@ type clientConfig struct { func (c *clientConfig) validateAllowedSTSHeaderValues(headers http.Header) error { for k := range headers { h := textproto.CanonicalMIMEHeaderKey(k) + if h == "X-Amz-Signedheaders" { + h = "X-Amz-SignedHeaders" + } if strings.HasPrefix(h, amzHeaderPrefix) && !strutil.StrListContains(defaultAllowedSTSRequestHeaders, h) && !strutil.StrListContains(c.AllowedSTSHeaderValues, h) { @@ -397,6 +401,21 @@ func (c *clientConfig) validateAllowedSTSHeaderValues(headers http.Header) error return nil } +func (c *clientConfig) validateAllowedSTSQueryValues(params url.Values) error { + for k := range params { + h := textproto.CanonicalMIMEHeaderKey(k) + if h == "X-Amz-Signedheaders" { + h = "X-Amz-SignedHeaders" + } + if strings.HasPrefix(h, amzHeaderPrefix) && + !strutil.StrListContains(defaultAllowedSTSRequestHeaders, k) && + !strutil.StrListContains(c.AllowedSTSHeaderValues, k) { + return errors.New("invalid request query param: " + k) + } + } + return nil +} + const pathConfigClientHelpSyn = ` Configure AWS IAM credentials that are used to query instance and role details from the AWS API. ` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index cd7c736766fa..c305d7624045 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -97,7 +97,7 @@ significance.`, Type: framework.TypeString, Description: `HTTP method to use for the AWS request when auth_type is iam. This must match what has been signed in the -presigned request. Currently, POST is the only supported value`, +presigned request.`, }, "iam_request_url": { @@ -253,9 +253,8 @@ func (b *backend) pathLoginIamGetRoleNameCallerIdAndEntity(ctx context.Context, return "", nil, nil, logical.ErrorResponse("missing iam_http_request_method"), nil } - // In the future, might consider supporting GET - if method != "POST" { - return "", nil, nil, logical.ErrorResponse("invalid iam_http_request_method; currently only 'POST' is supported"), nil + if method != http.MethodGet && method != http.MethodPost { + return "", nil, nil, logical.ErrorResponse("invalid iam_http_request_method; currently only 'GET' and 'POST' are supported"), nil } rawUrlB64 := data.Get("iam_request_url").(string) @@ -270,16 +269,13 @@ func (b *backend) pathLoginIamGetRoleNameCallerIdAndEntity(ctx context.Context, if err != nil { return "", nil, nil, logical.ErrorResponse("error parsing iam_request_url"), nil } - if parsedUrl.RawQuery != "" { + if parsedUrl.RawQuery != "" && method != http.MethodGet { // Should be no query parameters return "", nil, nil, logical.ErrorResponse(logical.ErrInvalidRequest.Error()), nil } - // TODO: There are two potentially valid cases we're not yet supporting that would - // necessitate this check being changed. First, if we support GET requests. - // Second if we support presigned POST requests bodyB64 := data.Get("iam_request_body").(string) - if bodyB64 == "" { - return "", nil, nil, logical.ErrorResponse("missing iam_request_body"), nil + if bodyB64 == "" && method != http.MethodGet { + return "", nil, nil, logical.ErrorResponse("missing iam_request_body which is required for POST requests"), nil } bodyRaw, err := base64.StdEncoding.DecodeString(bodyB64) if err != nil { @@ -305,7 +301,7 @@ func (b *backend) pathLoginIamGetRoleNameCallerIdAndEntity(ctx context.Context, maxRetries := awsClient.DefaultRetryerMaxNumRetries if config != nil { if config.IAMServerIdHeaderValue != "" { - err = validateVaultHeaderValue(headers, parsedUrl, config.IAMServerIdHeaderValue) + err = validateVaultHeaderValue(method, headers, parsedUrl, config.IAMServerIdHeaderValue) if err != nil { return "", nil, nil, logical.ErrorResponse(fmt.Sprintf("error validating %s header: %v", iamServerIdHeader, err)), nil } @@ -313,6 +309,11 @@ func (b *backend) pathLoginIamGetRoleNameCallerIdAndEntity(ctx context.Context, if err = config.validateAllowedSTSHeaderValues(headers); err != nil { return "", nil, nil, logical.ErrorResponse(err.Error()), nil } + if method == http.MethodGet { + if err = config.validateAllowedSTSQueryValues(parsedUrl.Query()); err != nil { + return "", nil, nil, logical.ErrorResponse(err.Error()), nil + } + } if config.STSEndpoint != "" { endpoint = config.STSEndpoint } @@ -1570,11 +1571,11 @@ func hasValuesForEc2Auth(data *framework.FieldData) (bool, bool) { } func hasValuesForIamAuth(data *framework.FieldData) (bool, bool) { - _, hasRequestMethod := data.GetOk("iam_http_request_method") + method, hasRequestMethod := data.GetOk("iam_http_request_method") _, hasRequestURL := data.GetOk("iam_request_url") _, hasRequestBody := data.GetOk("iam_request_body") _, hasRequestHeaders := data.GetOk("iam_request_headers") - return (hasRequestMethod && hasRequestURL && hasRequestBody && hasRequestHeaders), + return (hasRequestMethod && hasRequestURL && (method == http.MethodGet || hasRequestBody) && hasRequestHeaders), (hasRequestMethod || hasRequestURL || hasRequestBody || hasRequestHeaders) } @@ -1628,7 +1629,7 @@ func parseIamArn(iamArn string) (*iamEntity, error) { return &entity, nil } -func validateVaultHeaderValue(headers http.Header, _ *url.URL, requiredHeaderValue string) error { +func validateVaultHeaderValue(method string, headers http.Header, parsed *url.URL, requiredHeaderValue string) error { providedValue := "" for k, v := range headers { if strings.EqualFold(iamServerIdHeader, k) { @@ -1644,25 +1645,39 @@ func validateVaultHeaderValue(headers http.Header, _ *url.URL, requiredHeaderVal if providedValue != requiredHeaderValue { return fmt.Errorf("expected %q but got %q", requiredHeaderValue, providedValue) } - - if authzHeaders, ok := headers["Authorization"]; ok { - // authzHeader looks like AWS4-HMAC-SHA256 Credential=AKI..., SignedHeaders=host;x-amz-date;x-vault-awsiam-id, Signature=... - // We need to extract out the SignedHeaders - re := regexp.MustCompile(".*SignedHeaders=([^,]+)") - authzHeader := strings.Join(authzHeaders, ",") - matches := re.FindSubmatch([]byte(authzHeader)) - if len(matches) < 1 { - return fmt.Errorf("vault header wasn't signed") + switch method { + case http.MethodPost: + if authzHeaders, ok := headers["Authorization"]; ok { + // authzHeader looks like AWS4-HMAC-SHA256 Credential=AKI..., SignedHeaders=host;x-amz-date;x-vault-awsiam-id, Signature=... + // We need to extract out the SignedHeaders + re := regexp.MustCompile(".*SignedHeaders=([^,]+)") + authzHeader := strings.Join(authzHeaders, ",") + matches := re.FindSubmatch([]byte(authzHeader)) + if len(matches) < 1 { + return fmt.Errorf("vault header wasn't signed") + } + if len(matches) > 2 { + return fmt.Errorf("found multiple SignedHeaders components") + } + signedHeaders := string(matches[1]) + return ensureHeaderIsSigned(signedHeaders, iamServerIdHeader) + } + return fmt.Errorf("missing Authorization header") + case http.MethodGet: + actions := map[string][]string(parsed.Query())["Action"] + if len(actions) == 0 { + return fmt.Errorf("no action found in request") } - if len(matches) > 2 { - return fmt.Errorf("found multiple SignedHeaders components") + if len(actions) != 1 { + return fmt.Errorf("found multiple actions") } - signedHeaders := string(matches[1]) - return ensureHeaderIsSigned(signedHeaders, iamServerIdHeader) + if actions[0] != "GetCallerIdentity" { + return fmt.Errorf("unexpected action parameter, %s", actions[0]) + } + return ensureHeaderIsSigned(parsed.Query().Get("X-Amz-SignedHeaders"), iamServerIdHeader) + default: + return fmt.Errorf("unsupported method, %s", method) } - // TODO: If we support GET requests, then we need to parse the X-Amz-SignedHeaders - // argument out of the query string and search in there for the header value - return fmt.Errorf("missing Authorization header") } func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) *http.Request { diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 3fbc090f8477..7161be52213e 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -158,27 +158,27 @@ func TestBackend_validateVaultHeaderValue(t *testing.T) { "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request", "SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } - err = validateVaultHeaderValue(postHeadersMissing, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersMissing, requestURL, canaryHeaderValue) if err == nil { t.Error("validated POST request with missing Vault header") } - err = validateVaultHeaderValue(postHeadersInvalid, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersInvalid, requestURL, canaryHeaderValue) if err == nil { t.Error("validated POST request with invalid Vault header value") } - err = validateVaultHeaderValue(postHeadersUnsigned, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersUnsigned, requestURL, canaryHeaderValue) if err == nil { t.Error("validated POST request with unsigned Vault header") } - err = validateVaultHeaderValue(postHeadersValid, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersValid, requestURL, canaryHeaderValue) if err != nil { t.Errorf("did NOT validate valid POST request: %v", err) } - err = validateVaultHeaderValue(postHeadersSplit, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersSplit, requestURL, canaryHeaderValue) if err != nil { t.Errorf("did NOT validate valid POST request with split Authorization header: %v", err) } From 01bc255903ff6644f0cc24515bb01d6c190a81ef Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Sat, 20 Feb 2021 17:43:28 -0800 Subject: [PATCH 2/8] Add GET method tests for aws-iam auth login path --- builtin/credential/aws/path_login_test.go | 112 ++++++++++++++++++++-- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 7161be52213e..feb31c9b9d7a 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -126,9 +126,106 @@ func TestBackend_pathLogin_parseIamArn(t *testing.T) { } } -func TestBackend_validateVaultHeaderValue(t *testing.T) { +func TestBackend_validateVaultGetRequestValues(t *testing.T) { const canaryHeaderValue = "Vault-Server" - requestURL, err := url.Parse("https://sts.amazonaws.com/") + + getHeadersMissing := http.Header{ + "Host": []string{"Foo"}, + } + getHeadersInvalid := http.Header{ + "Host": []string{"Foo"}, + iamServerIdHeader: []string{"InvalidValue"}, + } + getHeadersValid := http.Header{ + "Host": []string{"Foo"}, + iamServerIdHeader: []string{canaryHeaderValue}, + } + getQueryValid := url.Values(map[string][]string{ + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + "X-Amz-SignedHeaders": {"host;x-vault-aws-iam-server-id"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetCallerIdentity"}, + "Version": {"2011-06-15"}, + }) + getQueryUnsigned := url.Values(map[string][]string{ + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + "X-Amz-SignedHeaders": {"host"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetCallerIdentity"}, + "Version": {"2011-06-15"}, + }) + getQueryNoAction := url.Values(map[string][]string{ + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + "X-Amz-SignedHeaders": {"host"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Version": {"2011-06-15"}, + }) + getQueryInvalidAction := url.Values(map[string][]string{ + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + "X-Amz-SignedHeaders": {"host"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetSessionToken"}, + "Version": {"2011-06-15"}, + }) + validGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryValid.Encode()) + if err != nil { + t.Fatalf("error parsing test URL: %v", err) + } + unsignedGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryUnsigned.Encode()) + if err != nil { + t.Fatalf("error parsing test URL: %v", err) + } + noActionGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryNoAction.Encode()) + if err != nil { + t.Fatalf("error parsing test URL: %v", err) + } + invalidActionGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryInvalidAction.Encode()) + if err != nil { + t.Fatalf("error parsing test URL: %v", err) + } + + err = validateVaultHeaderValue(http.MethodGet, getHeadersMissing, validGetRequestURL, canaryHeaderValue) + if err == nil { + t.Error("validated GET request with missing Vault header") + } + + err = validateVaultHeaderValue(http.MethodGet, getHeadersInvalid, validGetRequestURL, canaryHeaderValue) + if err == nil { + t.Error("validated GET request with invalid Vault header value") + } + + err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, unsignedGetRequestURL, canaryHeaderValue) + if err == nil { + t.Error("validated GET request with unsigned Vault header") + } + + err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, noActionGetRequestURL, canaryHeaderValue) + if err == nil { + t.Error("validated GET request with no Action parameter") + } + + err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, invalidActionGetRequestURL, canaryHeaderValue) + if err == nil { + t.Error("validated GET request with an invalid Action parameter") + } + + err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, validGetRequestURL, canaryHeaderValue) + if err != nil { + t.Errorf("did NOT validate valid GET request: %v", err) + } +} + +func TestBackend_validateVaultPostRequestValues(t *testing.T) { + const canaryHeaderValue = "Vault-Server" + postRequestURL, err := url.Parse("https://sts.amazonaws.com/") if err != nil { t.Fatalf("error parsing test URL: %v", err) } @@ -151,34 +248,33 @@ func TestBackend_validateVaultHeaderValue(t *testing.T) { iamServerIdHeader: []string{canaryHeaderValue}, "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } - postHeadersSplit := http.Header{ "Host": []string{"Foo"}, iamServerIdHeader: []string{canaryHeaderValue}, "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request", "SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } - err = validateVaultHeaderValue(http.MethodPost, postHeadersMissing, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersMissing, postRequestURL, canaryHeaderValue) if err == nil { t.Error("validated POST request with missing Vault header") } - err = validateVaultHeaderValue(http.MethodPost, postHeadersInvalid, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersInvalid, postRequestURL, canaryHeaderValue) if err == nil { t.Error("validated POST request with invalid Vault header value") } - err = validateVaultHeaderValue(http.MethodPost, postHeadersUnsigned, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersUnsigned, postRequestURL, canaryHeaderValue) if err == nil { t.Error("validated POST request with unsigned Vault header") } - err = validateVaultHeaderValue(http.MethodPost, postHeadersValid, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersValid, postRequestURL, canaryHeaderValue) if err != nil { t.Errorf("did NOT validate valid POST request: %v", err) } - err = validateVaultHeaderValue(http.MethodPost, postHeadersSplit, requestURL, canaryHeaderValue) + err = validateVaultHeaderValue(http.MethodPost, postHeadersSplit, postRequestURL, canaryHeaderValue) if err != nil { t.Errorf("did NOT validate valid POST request with split Authorization header: %v", err) } From beb47614de8308657c0020b49e7693e35ab52a80 Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Sat, 20 Feb 2021 18:30:28 -0800 Subject: [PATCH 3/8] Update Website Documenation --- website/content/api-docs/auth/aws.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/content/api-docs/auth/aws.mdx b/website/content/api-docs/auth/aws.mdx index 2beb84c18bc1..12bc598d3623 100644 --- a/website/content/api-docs/auth/aws.mdx +++ b/website/content/api-docs/auth/aws.mdx @@ -1078,18 +1078,18 @@ for more information on the signature types. enabled on either the role or the role tag, the `nonce` holds no significance. This is ignored unless using the ec2 auth method. - `iam_http_request_method` `(string: )` - HTTP method used in the - signed request. Currently only POST is supported, but other methods may be - supported in the future. This is required when using the iam auth method. + signed request. This is required when using the iam auth method. - `iam_request_url` `(string: )` - Base64-encoded HTTP URL used in the signed request. Most likely just `aHR0cHM6Ly9zdHMuYW1hem9uYXdzLmNvbS8=` (base64-encoding of `https://sts.amazonaws.com/`) as most requests will - probably use POST with an empty URI. This is required when using the iam auth - method. + probably use POST with an empty URI. If using GET method this will contain + the authentication headers that have been hoisted out of the message body. + This is required when using the iam auth method. - `iam_request_body` `(string: )` - Base64-encoded body of the signed request. Most likely `QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==`, which is the base64 encoding of `Action=GetCallerIdentity&Version=2011-06-15`. This is - required when using the iam auth method. + required when using the iam auth method with POST signed requests. - `iam_request_headers` `(string: )` - Key/value pairs of headers for use in the `sts:GetCallerIdentity` HTTP requests headers. Can be either a Base64-encoded, JSON-serialized string, or a JSON object of key/value pairs. The From 526f48b82957da2526a2e4ae25eb8dd087df99c0 Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Thu, 4 Mar 2021 13:34:30 -0800 Subject: [PATCH 4/8] Validate GET action even if iam-server header is not set --- builtin/credential/aws/path_login.go | 35 +++++++++++++++-------- builtin/credential/aws/path_login_test.go | 13 ++++++--- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index c305d7624045..bbf47862009c 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -269,6 +269,9 @@ func (b *backend) pathLoginIamGetRoleNameCallerIdAndEntity(ctx context.Context, if err != nil { return "", nil, nil, logical.ErrorResponse("error parsing iam_request_url"), nil } + if err = validateLoginIamRequestUrl(method, parsedUrl); err != nil { + return "", nil, nil, logical.ErrorResponse(err.Error()), nil + } if parsedUrl.RawQuery != "" && method != http.MethodGet { // Should be no query parameters return "", nil, nil, logical.ErrorResponse(logical.ErrInvalidRequest.Error()), nil @@ -1535,6 +1538,24 @@ func hasWildcardBind(boundIamPrincipalARNs []string) bool { return false } +// Validate that the iam_request_url passed is valid for the STS request +func validateLoginIamRequestUrl(method string, parsedUrl *url.URL) error { + switch method { + case http.MethodGet: + actions := map[string][]string(parsedUrl.Query())["Action"] + if len(actions) == 0 { + return fmt.Errorf("no action found in request") + } + if len(actions) != 1 { + return fmt.Errorf("found multiple actions") + } + if actions[0] != "GetCallerIdentity" { + return fmt.Errorf("unexpected action parameter, %s", actions[0]) + } + } + return nil +} + // Validate that the iam_request_body passed is valid for the STS request func validateLoginIamRequestBody(body string) error { qs, err := url.ParseQuery(body) @@ -1629,7 +1650,7 @@ func parseIamArn(iamArn string) (*iamEntity, error) { return &entity, nil } -func validateVaultHeaderValue(method string, headers http.Header, parsed *url.URL, requiredHeaderValue string) error { +func validateVaultHeaderValue(method string, headers http.Header, parsedUrl *url.URL, requiredHeaderValue string) error { providedValue := "" for k, v := range headers { if strings.EqualFold(iamServerIdHeader, k) { @@ -1664,17 +1685,7 @@ func validateVaultHeaderValue(method string, headers http.Header, parsed *url.UR } return fmt.Errorf("missing Authorization header") case http.MethodGet: - actions := map[string][]string(parsed.Query())["Action"] - if len(actions) == 0 { - return fmt.Errorf("no action found in request") - } - if len(actions) != 1 { - return fmt.Errorf("found multiple actions") - } - if actions[0] != "GetCallerIdentity" { - return fmt.Errorf("unexpected action parameter, %s", actions[0]) - } - return ensureHeaderIsSigned(parsed.Query().Get("X-Amz-SignedHeaders"), iamServerIdHeader) + return ensureHeaderIsSigned(parsedUrl.Query().Get("X-Amz-SignedHeaders"), iamServerIdHeader) default: return fmt.Errorf("unsupported method, %s", method) } diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index feb31c9b9d7a..989d604e914b 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -161,7 +161,7 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { getQueryNoAction := url.Values(map[string][]string{ "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, - "X-Amz-SignedHeaders": {"host"}, + "X-Amz-SignedHeaders": {"host;x-vault-aws-iam-server-id"}, "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, "Version": {"2011-06-15"}, @@ -169,7 +169,7 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { getQueryInvalidAction := url.Values(map[string][]string{ "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, - "X-Amz-SignedHeaders": {"host"}, + "X-Amz-SignedHeaders": {"host;x-vault-aws-iam-server-id"}, "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, "Action": {"GetSessionToken"}, @@ -207,16 +207,21 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { t.Error("validated GET request with unsigned Vault header") } - err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, noActionGetRequestURL, canaryHeaderValue) + err = validateLoginIamRequestUrl(http.MethodGet, noActionGetRequestURL) if err == nil { t.Error("validated GET request with no Action parameter") } - err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, invalidActionGetRequestURL, canaryHeaderValue) + err = validateLoginIamRequestUrl(http.MethodGet, invalidActionGetRequestURL) if err == nil { t.Error("validated GET request with an invalid Action parameter") } + err = validateLoginIamRequestUrl(http.MethodGet, validGetRequestURL) + if err != nil { + t.Errorf("did NOT validate valid GET request: %v", err) + } + err = validateVaultHeaderValue(http.MethodGet, getHeadersValid, validGetRequestURL, canaryHeaderValue) if err != nil { t.Errorf("did NOT validate valid GET request: %v", err) From a11f991a93f39815926510128f20e8c5623e569f Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Thu, 4 Mar 2021 14:32:26 -0800 Subject: [PATCH 5/8] Combine URL checks --- builtin/credential/aws/path_login.go | 13 ++++++++----- builtin/credential/aws/path_login_test.go | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index bbf47862009c..91913c84cd6f 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -272,10 +272,6 @@ func (b *backend) pathLoginIamGetRoleNameCallerIdAndEntity(ctx context.Context, if err = validateLoginIamRequestUrl(method, parsedUrl); err != nil { return "", nil, nil, logical.ErrorResponse(err.Error()), nil } - if parsedUrl.RawQuery != "" && method != http.MethodGet { - // Should be no query parameters - return "", nil, nil, logical.ErrorResponse(logical.ErrInvalidRequest.Error()), nil - } bodyB64 := data.Get("iam_request_body").(string) if bodyB64 == "" && method != http.MethodGet { return "", nil, nil, logical.ErrorResponse("missing iam_request_body which is required for POST requests"), nil @@ -1552,8 +1548,15 @@ func validateLoginIamRequestUrl(method string, parsedUrl *url.URL) error { if actions[0] != "GetCallerIdentity" { return fmt.Errorf("unexpected action parameter, %s", actions[0]) } + return nil + case http.MethodPost: + if parsedUrl.RawQuery != "" { + return logical.ErrInvalidRequest + } + return nil + default: + return fmt.Errorf("unsupported method, %s", method) } - return nil } // Validate that the iam_request_body passed is valid for the STS request diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 989d604e914b..a0f3e7991319 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -279,6 +279,11 @@ func TestBackend_validateVaultPostRequestValues(t *testing.T) { t.Errorf("did NOT validate valid POST request: %v", err) } + err = validateLoginIamRequestUrl(http.MethodPost, postRequestURL) + if err != nil { + t.Errorf("did NOT validate valid POST request: %v", err) + } + err = validateVaultHeaderValue(http.MethodPost, postHeadersSplit, postRequestURL, canaryHeaderValue) if err != nil { t.Errorf("did NOT validate valid POST request with split Authorization header: %v", err) From 02b7ab342bb54db399f86ab9774caa6f93ce922c Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Sun, 25 Sep 2022 08:57:24 -0700 Subject: [PATCH 6/8] Add const amzSignedHeaders to aws credential builtin --- builtin/credential/aws/backend.go | 3 +- builtin/credential/aws/path_config_client.go | 4 +- builtin/credential/aws/path_login.go | 2 +- builtin/credential/aws/path_login_test.go | 54 ++++++++++---------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 07de023814af..5b1ddc79f339 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -22,6 +22,7 @@ import ( const ( amzHeaderPrefix = "X-Amz-" + amzSignedHeaders = "X-Amz-SignedHeaders" operationPrefixAWS = "aws" ) @@ -32,7 +33,7 @@ var defaultAllowedSTSRequestHeaders = []string{ "X-Amz-Date", "X-Amz-Security-Token", "X-Amz-Signature", - "X-Amz-SignedHeaders", + amzSignedHeaders, "X-Amz-User-Agent", } diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 8cb60370e210..dec0acda8074 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -390,7 +390,7 @@ func (c *clientConfig) validateAllowedSTSHeaderValues(headers http.Header) error for k := range headers { h := textproto.CanonicalMIMEHeaderKey(k) if h == "X-Amz-Signedheaders" { - h = "X-Amz-SignedHeaders" + h = amzSignedHeaders } if strings.HasPrefix(h, amzHeaderPrefix) && !strutil.StrListContains(defaultAllowedSTSRequestHeaders, h) && @@ -405,7 +405,7 @@ func (c *clientConfig) validateAllowedSTSQueryValues(params url.Values) error { for k := range params { h := textproto.CanonicalMIMEHeaderKey(k) if h == "X-Amz-Signedheaders" { - h = "X-Amz-SignedHeaders" + h = amzSignedHeaders } if strings.HasPrefix(h, amzHeaderPrefix) && !strutil.StrListContains(defaultAllowedSTSRequestHeaders, k) && diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 91913c84cd6f..c3c9c691e63c 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -1688,7 +1688,7 @@ func validateVaultHeaderValue(method string, headers http.Header, parsedUrl *url } return fmt.Errorf("missing Authorization header") case http.MethodGet: - return ensureHeaderIsSigned(parsedUrl.Query().Get("X-Amz-SignedHeaders"), iamServerIdHeader) + return ensureHeaderIsSigned(parsedUrl.Query().Get(amzSignedHeaders), iamServerIdHeader) default: return fmt.Errorf("unsupported method, %s", method) } diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index a0f3e7991319..c6d1419bdc7f 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -141,39 +141,39 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { iamServerIdHeader: []string{canaryHeaderValue}, } getQueryValid := url.Values(map[string][]string{ - "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, - "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, - "X-Amz-SignedHeaders": {"host;x-vault-aws-iam-server-id"}, - "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, - "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, - "Action": {"GetCallerIdentity"}, - "Version": {"2011-06-15"}, + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + amzSignedHeaders: {"host;x-vault-aws-iam-server-id"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetCallerIdentity"}, + "Version": {"2011-06-15"}, }) getQueryUnsigned := url.Values(map[string][]string{ - "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, - "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, - "X-Amz-SignedHeaders": {"host"}, - "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, - "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, - "Action": {"GetCallerIdentity"}, - "Version": {"2011-06-15"}, + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + amzSignedHeaders: {"host"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetCallerIdentity"}, + "Version": {"2011-06-15"}, }) getQueryNoAction := url.Values(map[string][]string{ - "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, - "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, - "X-Amz-SignedHeaders": {"host;x-vault-aws-iam-server-id"}, - "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, - "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, - "Version": {"2011-06-15"}, + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + amzSignedHeaders: {"host;x-vault-aws-iam-server-id"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Version": {"2011-06-15"}, }) getQueryInvalidAction := url.Values(map[string][]string{ - "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, - "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, - "X-Amz-SignedHeaders": {"host;x-vault-aws-iam-server-id"}, - "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, - "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, - "Action": {"GetSessionToken"}, - "Version": {"2011-06-15"}, + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + amzSignedHeaders: {"host;x-vault-aws-iam-server-id"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetSessionToken"}, + "Version": {"2011-06-15"}, }) validGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryValid.Encode()) if err != nil { From 4bb7bbf3dd32dfac788d347d2a414134a499d4d0 Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Sun, 25 Sep 2022 08:58:41 -0700 Subject: [PATCH 7/8] Add test for multiple GET request actions --- builtin/credential/aws/path_login_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index c6d1419bdc7f..f90cc4c35424 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -175,6 +175,15 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { "Action": {"GetSessionToken"}, "Version": {"2011-06-15"}, }) + getQueryMultipleActions := url.Values(map[string][]string{ + "X-Amz-Algorithm": {"AWS4-HMAC-SHA256"}, + "X-Amz-Credential": {"AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request"}, + amzSignedHeaders: {"host;x-vault-aws-iam-server-id"}, + "X-Amz-Signature": {"5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "X-Amz-User-Agent": {"aws-sdk-go-v2/1.2.0 os/linux lang/go/1.16 md/GOOS/linux md/GOARCH/amd64"}, + "Action": {"GetCallerIdentity;GetSessionToken"}, + "Version": {"2011-06-15"}, + }) validGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryValid.Encode()) if err != nil { t.Fatalf("error parsing test URL: %v", err) @@ -191,6 +200,10 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { if err != nil { t.Fatalf("error parsing test URL: %v", err) } + multipleActionsGetRequestURL, err := url.Parse("https://sts.amazonaws.com/?" + getQueryMultipleActions.Encode()) + if err != nil { + t.Fatalf("error parsing test URL: %v", err) + } err = validateVaultHeaderValue(http.MethodGet, getHeadersMissing, validGetRequestURL, canaryHeaderValue) if err == nil { @@ -212,6 +225,11 @@ func TestBackend_validateVaultGetRequestValues(t *testing.T) { t.Error("validated GET request with no Action parameter") } + err = validateLoginIamRequestUrl(http.MethodGet, multipleActionsGetRequestURL) + if err == nil { + t.Error("validated GET request with multiple Action parameters") + } + err = validateLoginIamRequestUrl(http.MethodGet, invalidActionGetRequestURL) if err == nil { t.Error("validated GET request with an invalid Action parameter") From e7be37098f75d284ee9d86f47686332d9c486bbf Mon Sep 17 00:00:00 2001 From: Michael Ryan Dempsey Date: Sun, 25 Sep 2022 08:52:32 -0700 Subject: [PATCH 8/8] Add Changelog Entry --- changelog/10961.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/10961.txt diff --git a/changelog/10961.txt b/changelog/10961.txt new file mode 100644 index 000000000000..5387a53d38ae --- /dev/null +++ b/changelog/10961.txt @@ -0,0 +1,3 @@ +```release-note:improvement +auth/aws: Added support for signed GET requests for authenticating to vault using the aws iam method. +```