From 646baea5e3f5b63be3bb9458b0f0ac2c161a0105 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Mon, 23 Jan 2017 01:33:04 -0500 Subject: [PATCH 01/20] Rename builtin/credential/aws-ec2 to aws The aws-ec2 authentication backend is being expanded and will become the generic aws backend. This is a small rename commit to keep the commit history clean. --- builtin/credential/{aws-ec2 => aws}/backend.go | 0 builtin/credential/{aws-ec2 => aws}/backend_test.go | 0 builtin/credential/{aws-ec2 => aws}/client.go | 0 builtin/credential/{aws-ec2 => aws}/path_config_certificate.go | 0 builtin/credential/{aws-ec2 => aws}/path_config_client.go | 0 .../{aws-ec2 => aws}/path_config_tidy_identity_whitelist.go | 0 .../{aws-ec2 => aws}/path_config_tidy_roletag_blacklist.go | 0 builtin/credential/{aws-ec2 => aws}/path_identity_whitelist.go | 0 builtin/credential/{aws-ec2 => aws}/path_login.go | 0 builtin/credential/{aws-ec2 => aws}/path_role.go | 0 builtin/credential/{aws-ec2 => aws}/path_role_tag.go | 0 builtin/credential/{aws-ec2 => aws}/path_roletag_blacklist.go | 0 .../credential/{aws-ec2 => aws}/path_tidy_identity_whitelist.go | 0 .../credential/{aws-ec2 => aws}/path_tidy_roletag_blacklist.go | 0 cli/commands.go | 2 +- 15 files changed, 1 insertion(+), 1 deletion(-) rename builtin/credential/{aws-ec2 => aws}/backend.go (100%) rename builtin/credential/{aws-ec2 => aws}/backend_test.go (100%) rename builtin/credential/{aws-ec2 => aws}/client.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_config_certificate.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_config_client.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_config_tidy_identity_whitelist.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_config_tidy_roletag_blacklist.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_identity_whitelist.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_login.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_role.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_role_tag.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_roletag_blacklist.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_tidy_identity_whitelist.go (100%) rename builtin/credential/{aws-ec2 => aws}/path_tidy_roletag_blacklist.go (100%) diff --git a/builtin/credential/aws-ec2/backend.go b/builtin/credential/aws/backend.go similarity index 100% rename from builtin/credential/aws-ec2/backend.go rename to builtin/credential/aws/backend.go diff --git a/builtin/credential/aws-ec2/backend_test.go b/builtin/credential/aws/backend_test.go similarity index 100% rename from builtin/credential/aws-ec2/backend_test.go rename to builtin/credential/aws/backend_test.go diff --git a/builtin/credential/aws-ec2/client.go b/builtin/credential/aws/client.go similarity index 100% rename from builtin/credential/aws-ec2/client.go rename to builtin/credential/aws/client.go diff --git a/builtin/credential/aws-ec2/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go similarity index 100% rename from builtin/credential/aws-ec2/path_config_certificate.go rename to builtin/credential/aws/path_config_certificate.go diff --git a/builtin/credential/aws-ec2/path_config_client.go b/builtin/credential/aws/path_config_client.go similarity index 100% rename from builtin/credential/aws-ec2/path_config_client.go rename to builtin/credential/aws/path_config_client.go diff --git a/builtin/credential/aws-ec2/path_config_tidy_identity_whitelist.go b/builtin/credential/aws/path_config_tidy_identity_whitelist.go similarity index 100% rename from builtin/credential/aws-ec2/path_config_tidy_identity_whitelist.go rename to builtin/credential/aws/path_config_tidy_identity_whitelist.go diff --git a/builtin/credential/aws-ec2/path_config_tidy_roletag_blacklist.go b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go similarity index 100% rename from builtin/credential/aws-ec2/path_config_tidy_roletag_blacklist.go rename to builtin/credential/aws/path_config_tidy_roletag_blacklist.go diff --git a/builtin/credential/aws-ec2/path_identity_whitelist.go b/builtin/credential/aws/path_identity_whitelist.go similarity index 100% rename from builtin/credential/aws-ec2/path_identity_whitelist.go rename to builtin/credential/aws/path_identity_whitelist.go diff --git a/builtin/credential/aws-ec2/path_login.go b/builtin/credential/aws/path_login.go similarity index 100% rename from builtin/credential/aws-ec2/path_login.go rename to builtin/credential/aws/path_login.go diff --git a/builtin/credential/aws-ec2/path_role.go b/builtin/credential/aws/path_role.go similarity index 100% rename from builtin/credential/aws-ec2/path_role.go rename to builtin/credential/aws/path_role.go diff --git a/builtin/credential/aws-ec2/path_role_tag.go b/builtin/credential/aws/path_role_tag.go similarity index 100% rename from builtin/credential/aws-ec2/path_role_tag.go rename to builtin/credential/aws/path_role_tag.go diff --git a/builtin/credential/aws-ec2/path_roletag_blacklist.go b/builtin/credential/aws/path_roletag_blacklist.go similarity index 100% rename from builtin/credential/aws-ec2/path_roletag_blacklist.go rename to builtin/credential/aws/path_roletag_blacklist.go diff --git a/builtin/credential/aws-ec2/path_tidy_identity_whitelist.go b/builtin/credential/aws/path_tidy_identity_whitelist.go similarity index 100% rename from builtin/credential/aws-ec2/path_tidy_identity_whitelist.go rename to builtin/credential/aws/path_tidy_identity_whitelist.go diff --git a/builtin/credential/aws-ec2/path_tidy_roletag_blacklist.go b/builtin/credential/aws/path_tidy_roletag_blacklist.go similarity index 100% rename from builtin/credential/aws-ec2/path_tidy_roletag_blacklist.go rename to builtin/credential/aws/path_tidy_roletag_blacklist.go diff --git a/cli/commands.go b/cli/commands.go index 5297ea43bfbe..5b96b885f03e 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -9,7 +9,7 @@ import ( credAppId "github.com/hashicorp/vault/builtin/credential/app-id" credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - credAwsEc2 "github.com/hashicorp/vault/builtin/credential/aws-ec2" + credAwsEc2 "github.com/hashicorp/vault/builtin/credential/aws" credCert "github.com/hashicorp/vault/builtin/credential/cert" credGitHub "github.com/hashicorp/vault/builtin/credential/github" credLdap "github.com/hashicorp/vault/builtin/credential/ldap" From dc839e770761130b19303ffda71732e7ed7a9623 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Mon, 23 Jan 2017 02:01:27 -0500 Subject: [PATCH 02/20] Expand aws-ec2 backend to more generic aws This adds the ability to authenticate arbitrary AWS IAM principals using AWS's sts:GetCallerIdentity method. The AWS-EC2 auth backend is being to just AWS with the expansion. --- builtin/credential/aws/backend.go | 2 +- builtin/credential/aws/backend_test.go | 339 +++++---- builtin/credential/aws/cli.go | 124 +++ builtin/credential/aws/client.go | 14 +- .../credential/aws/path_config_certificate.go | 2 +- builtin/credential/aws/path_config_client.go | 59 +- .../credential/aws/path_config_client_test.go | 76 ++ .../path_config_tidy_identity_whitelist.go | 2 +- .../aws/path_config_tidy_roletag_blacklist.go | 2 +- .../credential/aws/path_identity_whitelist.go | 2 +- builtin/credential/aws/path_login.go | 712 +++++++++++++++--- builtin/credential/aws/path_login_test.go | 140 ++++ builtin/credential/aws/path_role.go | 152 +++- builtin/credential/aws/path_role_tag.go | 2 +- builtin/credential/aws/path_role_test.go | 374 +++++++++ .../credential/aws/path_roletag_blacklist.go | 2 +- .../aws/path_tidy_identity_whitelist.go | 2 +- .../aws/path_tidy_roletag_blacklist.go | 2 +- cli/commands.go | 5 +- 19 files changed, 1739 insertions(+), 274 deletions(-) create mode 100644 builtin/credential/aws/cli.go create mode 100644 builtin/credential/aws/path_config_client_test.go create mode 100644 builtin/credential/aws/path_login_test.go create mode 100644 builtin/credential/aws/path_role_test.go diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index ff8b71e2ff58..a910d0df3363 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "sync" diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index eccd52813ba2..37e2a6eb74a6 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1,12 +1,17 @@ -package awsec2 +package awsauth import ( "encoding/base64" + "encoding/json" "fmt" + "io/ioutil" + "net/http" "os" "strings" "testing" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" @@ -683,132 +688,6 @@ vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw } } -func TestBackend_pathRole(t *testing.T) { - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage - - b, err := Backend(config) - if err != nil { - t.Fatal(err) - } - _, err = b.Setup(config) - if err != nil { - t.Fatal(err) - } - - data := map[string]interface{}{ - "policies": "p,q,r,s", - "max_ttl": "2h", - "bound_ami_id": "ami-abcd123", - } - resp, err := b.HandleRequest(&logical.Request{ - Operation: logical.CreateOperation, - Path: "role/ami-abcd123", - Data: data, - Storage: storage, - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to create role") - } - if err != nil { - t.Fatal(err) - } - - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.ReadOperation, - Path: "role/ami-abcd123", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.IsError() { - t.Fatal("failed to read the role entry") - } - if !policyutil.EquivalentPolicies(strings.Split(data["policies"].(string), ","), resp.Data["policies"].([]string)) { - t.Fatalf("bad: policies: expected: %#v\ngot: %#v\n", data, resp.Data) - } - - data["allow_instance_migration"] = true - data["disallow_reauthentication"] = true - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/ami-abcd123", - Data: data, - Storage: storage, - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to create role") - } - if err != nil { - t.Fatal(err) - } - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.ReadOperation, - Path: "role/ami-abcd123", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - if !resp.Data["allow_instance_migration"].(bool) || !resp.Data["disallow_reauthentication"].(bool) { - t.Fatal("bad: expected:true got:false\n") - } - - // add another entry, to test listing of role entries - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, - Path: "role/ami-abcd456", - Data: data, - Storage: storage, - }) - if resp != nil && resp.IsError() { - t.Fatalf("failed to create role") - } - if err != nil { - t.Fatal(err) - } - - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.ListOperation, - Path: "roles", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Data == nil || resp.IsError() { - t.Fatalf("failed to list the role entries") - } - keys := resp.Data["keys"].([]string) - if len(keys) != 2 { - t.Fatalf("bad: keys: %#v\n", keys) - } - - _, err = b.HandleRequest(&logical.Request{ - Operation: logical.DeleteOperation, - Path: "role/ami-abcd123", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.ReadOperation, - Path: "role/ami-abcd123", - Storage: storage, - }) - if err != nil { - t.Fatal(err) - } - if resp != nil { - t.Fatalf("bad: response: expected:nil actual:%#v\n", resp) - } - -} - func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { // create a backend config := logical.TestBackendConfig() @@ -1068,7 +947,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { // needs to be set: // TEST_AWS_SECRET_KEY // TEST_AWS_ACCESS_KEY -func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { +func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing.T) { // This test case should be run only when certain env vars are set and // executed as an acceptance test. if os.Getenv(logicaltest.TestEnvVar) == "" { @@ -1275,3 +1154,207 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatalf("login attempt failed") } } + +func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[string]interface{}, error) { + headersJson, err := json.Marshal(request.Header) + if err != nil { + return nil, err + } + requestBody, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err + } + return map[string]interface{}{ + "auth_type": "signed_caller_identity_request", + "request_method": request.Method, + "request_url": request.URL.String(), + "request_headers": base64.StdEncoding.EncodeToString(headersJson), + "request_body": base64.StdEncoding.EncodeToString(requestBody), + "request_role": roleName, + }, nil +} + +// This is an acceptance test. +// If the test is NOT being run on an AWS EC2 instance in an instance profile, +// it requires the following environment variables to be set: +// TEST_AWS_ACCESS_KEY_ID +// TEST_AWS_SECRET_ACCESS_KEY +// TEST_AWS_SECURITY_TOKEN (optional, if you are using short-lived creds) +// These are intentionally NOT the "standard" variables to prevent accidentally +// using prod creds in acceptance tests +func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { + // This test case should be run only when certain env vars are set and + // executed as an acceptance test. + if os.Getenv(logicaltest.TestEnvVar) == "" { + t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) + return + } + + storage := &logical.InmemStorage{} + config := logical.TestBackendConfig() + config.StorageView = storage + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Setup(config) + if err != nil { + t.Fatal(err) + } + + // Override the default AWS env vars (if set) with our test creds + // so that the credential provider chain will pick them up + // NOTE that I'm not bothing to override the shared config file location, + // so if creds are specified there, they will be used before IAM + // instance profile creds + // This doesn't provide perfect leakage protection (e.g., it will still + // potentially pick up credentials from the ~/.config files), but probably + // good enough rather than having to muck around in the low-level details + for _, envvar := range []string{ + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SECURITY_TOKEN"} { + os.Setenv("TEST_"+envvar, os.Getenv(envvar)) + } + awsSession, err := session.NewSession() + if err != nil { + fmt.Println("failed to create session,", err) + return + } + + stsService := sts.New(awsSession) + var stsInputParams *sts.GetCallerIdentityInput + + testIdentity, err := stsService.GetCallerIdentity(stsInputParams) + if err != nil { + t.Fatalf("Received error retrieving identity: %s", err) + } + testIdentityArn, _, _, err := parseIamArn(*testIdentity.Arn) + if err != nil { + t.Fatal(err) + } + + // Test setup largely done + // At this point, we're going to: + // 1. Configure the client to require our test header value + // 2. Configure two different roles: + // a. One bound to our test user + // b. One bound to a garbage ARN + // 3. Pass in a request that doesn't have the signed header, ensure + // we're not allowed to login + // 4. Passin a request that has a validly signed header, but the wrong + // value, ensure it doesn't allow login + // 5. Pass in a request that has a validly signed request, ensure + // it allows us to login to our role + // 6. Pass in a request that has a validly signed request, asking for + // the other role, ensure it fails + const testVaultHeaderValue = "VaultAcceptanceTesting" + const testValidRoleName = "valid-role" + const testInvalidRoleName = "invalid-role" + + clientConfigData := map[string]interface{}{ + "caller_identity_header_value": testVaultHeaderValue, + } + clientRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/client", + Storage: storage, + Data: clientConfigData, + } + _, err = b.HandleRequest(clientRequest) + if err != nil { + t.Fatal(err) + } + + // configuring the valid role we'll be able to login to + roleData := map[string]interface{}{ + "bound_iam_principal_arn": testIdentityArn, + "policies": "root", + "allowed_auth_types": "signed_caller_identity_request", + } + roleRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/" + testValidRoleName, + Storage: storage, + Data: roleData, + } + resp, err := b.HandleRequest(roleRequest) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) + } + + // now we're creating the invalid role we won't be able to login to + roleData["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/FakeRole" + roleRequest.Path = "role/" + testInvalidRoleName + resp, err = b.HandleRequest(roleRequest) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) + } + + // now, create the request without the signed header + stsRequestNoHeader, _ := stsService.GetCallerIdentityRequest(stsInputParams) + stsRequestNoHeader.Sign() + loginData, err := buildCallerIdentityLoginData(stsRequestNoHeader.HTTPRequest, testValidRoleName) + if err != nil { + t.Fatal(err) + } + loginRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: loginData, + } + resp, err = b.HandleRequest(loginRequest) + if err != nil || resp == nil || !resp.IsError() { + t.Errorf("bad: expected failed login due to missing header: resp:%#v\nerr:%v", resp, err) + } + + // create the request with the invalid header value + + // Not reusing stsRequestNoHeader because the process of signing the request + // and reading the body modifies the underlying request, so it's just cleaner + // to get new requests. + stsRequestInvalidHeader, _ := stsService.GetCallerIdentityRequest(stsInputParams) + stsRequestInvalidHeader.HTTPRequest.Header.Add(magicVaultHeader, "InvalidValue") + stsRequestInvalidHeader.Sign() + loginData, err = buildCallerIdentityLoginData(stsRequestInvalidHeader.HTTPRequest, testValidRoleName) + if err != nil { + t.Fatal(err) + } + loginRequest = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: loginData, + } + resp, err = b.HandleRequest(loginRequest) + if err != nil || resp == nil || !resp.IsError() { + t.Errorf("bad: expected failed login due to invalid header: resp:%#v\nerr:%v", resp, err) + } + + // Now, valid request against invalid role + stsRequestValid, _ := stsService.GetCallerIdentityRequest(stsInputParams) + stsRequestValid.HTTPRequest.Header.Add(magicVaultHeader, testVaultHeaderValue) + stsRequestValid.Sign() + loginData, err = buildCallerIdentityLoginData(stsRequestValid.HTTPRequest, testInvalidRoleName) + if err != nil { + t.Fatal(err) + } + loginRequest = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: loginData, + } + resp, err = b.HandleRequest(loginRequest) + if err != nil || resp == nil || !resp.IsError() { + t.Errorf("bad: expected failed login due to invalid role: resp:%#v\nerr:%v", resp, err) + } + + loginData["role"] = testValidRoleName + resp, err = b.HandleRequest(loginRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Errorf("bad: expected valid login: resp:%#v", resp) + } +} diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go new file mode 100644 index 000000000000..ae61e400eba6 --- /dev/null +++ b/builtin/credential/aws/cli.go @@ -0,0 +1,124 @@ +package awsauth + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/awsutil" +) + +type CLIHandler struct{} + +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { + mount, ok := m["mount"] + if !ok { + mount = "aws" + } + + role, ok := m["role"] + if !ok { + role = "" + } + + headerValue, ok := m["header_value"] + if !ok { + headerValue = "" + } + + credConfig := &awsutil.CredentialsConfig{ + AccessKey: m["aws_access_key_id"], + SecretKey: m["aws_secret_access_key"], + SessionToken: m["aws_security_token"], + } + creds, err := credConfig.GenerateCredentialChain() + if err != nil { + return "", err + } + if creds == nil { + return "", fmt.Errorf("could not compile valid credential providers from static config, environemnt, shared, or instance metadata") + } + + stsSession, err := session.NewSessionWithOptions(session.Options{ + Config: aws.Config{Credentials: creds}, + }) + if err != nil { + return "", err + } + + var params *sts.GetCallerIdentityInput + svc := sts.New(stsSession) + stsRequest, _ := svc.GetCallerIdentityRequest(params) + if headerValue != "" { + stsRequest.HTTPRequest.Header.Add(magicVaultHeader, headerValue) + } + stsRequest.Sign() + headersJson, err := json.Marshal(stsRequest.HTTPRequest.Header) + if err != nil { + return "", err + } + requestBody, err := ioutil.ReadAll(stsRequest.HTTPRequest.Body) + if err != nil { + return "", err + } + method := stsRequest.HTTPRequest.Method + targetUrl := stsRequest.HTTPRequest.URL.String() + headers := base64.StdEncoding.EncodeToString(headersJson) + body := base64.StdEncoding.EncodeToString(requestBody) + + path := fmt.Sprintf("auth/%s/login", mount) + secret, err := c.Logical().Write(path, map[string]interface{}{ + "auth_type": "signed_caller_identity_request", + "request_method": method, + "request_url": targetUrl, + "request_headers": headers, + "request_body": body, + "role": role, + }) + + if err != nil { + return "", err + } + if secret == nil { + return "", fmt.Errorf("empty response from credential provider") + } + + return secret.Auth.ClientToken, nil + + return "", nil +} + +func (h *CLIHandler) Help() string { + help := ` +The AWS credentaial provider allows you to authenticate with +AWS IAM credentials. To use it, you specify valid AWS IAM credentials +in one of a number of ways. They can be specified explicitly on the +command line (which in general you should not do), via the standard AWS +environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and +AWS_SECURITY_TOKEN), via the ~/.aws/credentials file, or via an EC2 +instance profile (in that order). + + Example: vault auth -method=aws + +If you need to explicitly pass in credentials, you would do it like this: + Example: vault auth -method=aws aws_access_key_id= aws_secret_access_key= aws_security_token= + +Key/Value Pairs: + + mount=aws The mountpoint for the AWS credential provider. + Defaults to "aws-iam" + aws_access_key_id= Explicitly specified AWS access key + aws_secret_access_key= Explicitly specified AWS secret key + aws_security_token= Security token for temporary credentials + header_value The Value of the X-Vault-AWSIAM-Server-ID header. + role The name of the role you're requesting a token for + ` + + return strings.TrimSpace(help) +} diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 1434c9cebb71..dcc7ea587294 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" @@ -19,7 +19,7 @@ import ( // * Static credentials from 'config/client' // * Environment variables // * Instance metadata role -func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config, error) { +func (b *backend) getClientConfig(s logical.Storage, region, clientType string) (*aws.Config, error) { credsConfig := &awsutil.CredentialsConfig{ Region: region, } @@ -33,8 +33,12 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config endpoint := aws.String("") if config != nil { // Override the default endpoint with the configured endpoint. - if config.Endpoint != "" { + if clientType == "ec2" && config.Endpoint != "" { endpoint = aws.String(config.Endpoint) + } else if clientType == "iam" && config.IAMEndpoint != "" { + endpoint = aws.String(config.IAMEndpoint) + } else if clientType == "sts" && config.STSEndpoint != "" { + endpoint = aws.String(config.STSEndpoint) } credsConfig.AccessKey = config.AccessKey @@ -102,7 +106,7 @@ func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error) } // Create an AWS config object using a chain of providers - awsConfig, err := b.getClientConfig(s, region) + awsConfig, err := b.getClientConfig(s, region, "ec2") if err != nil { return nil, err } @@ -132,7 +136,7 @@ func (b *backend) clientIAM(s logical.Storage, region string) (*iam.IAM, error) } // Create an AWS config object using a chain of providers - awsConfig, err := b.getClientConfig(s, region) + awsConfig, err := b.getClientConfig(s, region, "iam") if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 4613b212112a..0c026ed45339 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "crypto/x509" diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 93f20219de97..67333aeae38e 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "github.com/fatih/structs" @@ -27,6 +27,24 @@ func pathConfigClient(b *backend) *framework.Path { Default: "", Description: "URL to override the default generated endpoint for making AWS EC2 API calls.", }, + + "iam_endpoint": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "URL to override the default generated endpoint for making AWS IAM API calls.", + }, + + "sts_endpoint": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "URL to override the default generated endpoint for making AWS STS API calls.", + }, + + "caller_identity_header_value": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "Value to require in the X-Vault-AWSIAM-Server-ID request header", + }, }, ExistenceCheck: b.pathConfigClientExistenceCheck, @@ -162,6 +180,36 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry.Endpoint = data.Get("endpoint").(string) } + iamEndpointStr, ok := data.GetOk("iam_endpoint") + if ok { + if configEntry.IAMEndpoint != iamEndpointStr.(string) { + changedCreds = true + configEntry.IAMEndpoint = iamEndpointStr.(string) + } + } else if req.Operation == logical.CreateOperation { + configEntry.IAMEndpoint = data.Get("iam_endpoint").(string) + } + + stsEndpointStr, ok := data.GetOk("sts_endpoint") + if ok { + if configEntry.STSEndpoint != stsEndpointStr.(string) { + // NOT setting changedCreds here, since this isn't really cached + configEntry.STSEndpoint = stsEndpointStr.(string) + } + } else if req.Operation == logical.CreateOperation { + configEntry.STSEndpoint = data.Get("sts_endpoint").(string) + } + + headerValStr, ok := data.GetOk("caller_identity_header_value") + if ok { + if configEntry.HeaderValue != headerValStr.(string) { + // NOT setting changedCreds here, since this isn't really cached + configEntry.HeaderValue = headerValStr.(string) + } + } else if req.Operation == logical.CreateOperation { + configEntry.HeaderValue = data.Get("caller_identity_header_value").(string) + } + // Since this endpoint supports both create operation and update operation, // the error checks for access_key and secret_key not being set are not present. // This allows calling this endpoint multiple times to provide the values. @@ -187,9 +235,12 @@ func (b *backend) pathConfigClientCreateUpdate( // Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to // interact with the AWS EC2 API. type clientConfig struct { - AccessKey string `json:"access_key" structs:"access_key" mapstructure:"access_key"` - SecretKey string `json:"secret_key" structs:"secret_key" mapstructure:"secret_key"` - Endpoint string `json:"endpoint" structs:"endpoint" mapstructure:"endpoint"` + AccessKey string `json:"access_key" structs:"access_key" mapstructure:"access_key"` + SecretKey string `json:"secret_key" structs:"secret_key" mapstructure:"secret_key"` + Endpoint string `json:"endpoint" structs:"endpoint" mapstructure:"endpoint"` + IAMEndpoint string `json:"iam_endpoint" structs:"iam_endpoint" mapstructure:"iam_endpoint"` + STSEndpoint string `json:"sts_endpoint" structs:"sts_endpoint" mapstructure:"sts_endpoint"` + HeaderValue string `json:"vault_header_value" structs:"vault_header_value" mapstructure:"vault_header_value"` } const pathConfigClientHelpSyn = ` diff --git a/builtin/credential/aws/path_config_client_test.go b/builtin/credential/aws/path_config_client_test.go new file mode 100644 index 000000000000..bf951ac5b624 --- /dev/null +++ b/builtin/credential/aws/path_config_client_test.go @@ -0,0 +1,76 @@ +package awsauth + +import ( + "testing" + + "github.com/hashicorp/vault/logical" +) + +func TestBackend_pathConfigClient(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Setup(config) + if err != nil { + t.Fatal(err) + } + + // make sure we start with empty roles, which gives us confidence that the read later + // actually is the two roles we created + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "config/client", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + // at this point, resp == nil is valid as no client config exists + // if resp != nil, then resp.Data must have EndPoint and HeaderValue as nil + if resp != nil { + if resp.IsError() { + t.Fatalf("failed to read client config entry") + } else if resp.Data["endpoint"] != nil || resp.Data["vault_header_value"] != nil { + t.Fatalf("returned endpoint or vault_header_value non-nil") + } + } + + data := map[string]interface{}{ + "sts_endpoint": "https://my-custom-sts-endpoint.example.com", + "caller_identity_header_value": "vault_server_identification_314159", + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "config/client", + Data: data, + Storage: storage, + }) + + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatal("failed to create the client config entry") + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "config/client", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatal("failed to read the client config entry") + } + if resp.Data["caller_identity_header_value"] != data["vault_header_value"] { + t.Fatalf("expected vault_header_value: '%#v'; returned vault_header_value: '%#v'", + data["caller_identity_header_value"], resp.Data["caller_identity_header_value"]) + } +} diff --git a/builtin/credential/aws/path_config_tidy_identity_whitelist.go b/builtin/credential/aws/path_config_tidy_identity_whitelist.go index 8fac923dc3b2..43aafaacbeb7 100644 --- a/builtin/credential/aws/path_config_tidy_identity_whitelist.go +++ b/builtin/credential/aws/path_config_tidy_identity_whitelist.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" diff --git a/builtin/credential/aws/path_config_tidy_roletag_blacklist.go b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go index 071ab9144687..c3059c68f1bb 100644 --- a/builtin/credential/aws/path_config_tidy_roletag_blacklist.go +++ b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" diff --git a/builtin/credential/aws/path_identity_whitelist.go b/builtin/credential/aws/path_identity_whitelist.go index 84a649499b46..600fc7dd59d7 100644 --- a/builtin/credential/aws/path_identity_whitelist.go +++ b/builtin/credential/aws/path_identity_whitelist.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "time" diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 8bf23d7b8d05..b59bf720f0f0 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -1,11 +1,16 @@ -package awsec2 +package awsauth import ( "crypto/subtle" "crypto/x509" "encoding/base64" "encoding/pem" + "encoding/xml" "fmt" + "io/ioutil" + "net/http" + "net/url" + "regexp" "strings" "time" @@ -13,6 +18,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/fullsailor/pkcs7" + "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/strutil" @@ -36,23 +42,58 @@ bearing the name of the AMI ID of the EC2 instance that is trying to login. If a matching role is not found, login fails.`, }, + "auth_type": { + Type: framework.TypeString, + Default: "instance_identity_document", + Description: `The login type to use upon logging in. The valid choices are +instance_identity_document (default) or signed_caller_identity_request.`, + }, + "pkcs7": { - Type: framework.TypeString, - Description: "PKCS7 signature of the identity document.", + Type: framework.TypeString, + Description: `PKCS7 signature of the identity document when using an auth_type +of instance_identity_document.`, }, "nonce": { Type: framework.TypeString, - Description: `The nonce to be used for subsequent login requests. -If this parameter is not specified at all and if reauthentication is allowed, -then the backend will generate a random nonce, attaches it to the instance's -identity-whitelist entry and returns the nonce back as part of auth metadata. -This value should be used with further login requests, to establish client -authenticity. Clients can choose to set a custom nonce if preferred, in which -case, it is recommended that clients provide a strong nonce. If a nonce is -provided but with an empty value, it indicates intent to disable -reauthentication. Note that, when 'disallow_reauthentication' option is enabled -on either the role or the role tag, the 'nonce' holds no significance.`, + Description: `The nonce to be used for subsequent login requests when +auth_type is instance_identity_document. If this parameter is not specified at +all and if reauthentication is allowed, then the backend will generate a random +nonce, attaches it to the instance's identity-whitelist entry and returns the +nonce back as part of auth metadata. This value should be used with further +login requests, to establish client authenticity. Clients can choose to set a +custom nonce if preferred, in which case, it is recommended that clients provide +a strong nonce. If a nonce is provided but with an empty value, it indicates +intent to disable reauthentication. Note that, when 'disallow_reauthentication' +option is enabled on either the role or the role tag, the 'nonce' holds no +significance.`, + }, + + "request_method": { + Type: framework.TypeString, + Description: `HTTP method to use for the AWS request when auth_type is +signed_caller_identity_request. This must match what has been signed in the +presigned request. Currently, POST is the only supported value`, + }, + + "request_url": { + Type: framework.TypeString, + Description: `Full URL against which to make the AWS request when auth_method is +signed_caller_identity_request. If using a POST request with the action +specified in the body, this should just be "/".`, + }, + + "request_body": { + Type: framework.TypeString, + Description: `Base64-encoded request body when auth_type is signed_caller_identity_request. +This must match the request body included in the signature.`, + }, + "request_headers": { + Type: framework.TypeString, + Description: `Base64-encoded JSON representation of the request headers when auth_type is +signed_caller_identity_request. This must at a minimum include the headers over +which AWS has included a signature.`, }, "identity": { Type: framework.TypeString, @@ -79,16 +120,11 @@ needs to be supplied along with 'identity' parameter.`, // instanceIamRoleARN fetches the IAM role ARN associated with the given // instance profile name -func (b *backend) instanceIamRoleARN(s logical.Storage, instanceProfileName, region string) (string, error) { +func (b *backend) instanceIamRoleARN(iamClient *iam.IAM, instanceProfileName string) (string, error) { if instanceProfileName == "" { return "", fmt.Errorf("missing instance profile name") } - iamClient, err := b.clientIAM(s, region) - if err != nil { - return "", err - } - profile, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{ InstanceProfileName: aws.String(instanceProfileName), }) @@ -116,7 +152,7 @@ func (b *backend) instanceIamRoleARN(s logical.Storage, instanceProfileName, reg // validateInstance queries the status of the EC2 instance using AWS EC2 API // and checks if the instance is running and is healthy -func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.DescribeInstancesOutput, error) { +func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.Instance, error) { // Create an EC2 client to pull the instance information ec2Client, err := b.clientEC2(s, region) if err != nil { @@ -155,7 +191,7 @@ func (b *backend) validateInstance(s logical.Storage, instanceID, region string) if *status.Reservations[0].Instances[0].State.Name != "running" { return nil, fmt.Errorf("instance is not in 'running' state") } - return status, nil + return status.Reservations[0].Instances[0], nil } // validateMetadata matches the given client nonce and pending time with the @@ -314,11 +350,114 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id return &identityDoc, nil } -// pathLoginUpdate is used to create a Vault token by the EC2 instances +func (b *backend) pathLoginUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + auth_type := data.Get("auth_type") + if auth_type == "instance_identity_document" { + return b.pathLoginUpdateInstanceIdentityDocument(req, data) + } else if auth_type == "signed_caller_identity_request" { + return b.pathLoginUpdateCallerIdentityRequest(req, data) + } else { + return logical.ErrorResponse("unrecognized auth_type, must be one of instance_identity_document or signed_caller_identity_request"), nil + } +} + +// Returns whether the EC2 instance meets the requirements of the particular +// AWS role entry. +func (b *backend) verifyInstanceMeetsRoleRequirements( + s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, iamClient *iam.IAM) (*roleTagLoginResponse, string, error) { + + // Verify that the AMI ID of the instance trying to login matches the + // AMI ID specified as a constraint on the role. + // + // Here, we're making a tradeoff and pulling the AMI ID out of the EC2 + // API rather than the signed instance identity doc. They *should* match. + // This means we require an EC2 API call to retrieve the AMI ID, but we're + // already calling the API to validate the Instance ID anyway, so it shouldn't + // matter. The benefit is that we have the exact same code whether auth_type + // is instance_identity_document or signed_caller_identity_request. + if roleEntry.BoundAmiID != "" && *instance.ImageId != roleEntry.BoundAmiID { + return nil, fmt.Sprintf("AMI ID '%s' does not belong to role '%s'", instance.ImageId, roleName), nil + } + + // Check if the IAM instance profile ARN of the instance trying to + // login, matches the IAM instance profile ARN specified as a constraint + // on the role + if roleEntry.BoundIamInstanceProfileARN != "" { + if instance.IamInstanceProfile == nil { + return nil, "", fmt.Errorf("IAM instance profile in the instance description is nil") + } + if instance.IamInstanceProfile.Arn == nil { + return nil, "", fmt.Errorf("IAM instance profile ARN in the instance description is nil") + } + iamInstanceProfileARN := *instance.IamInstanceProfile.Arn + if !strings.HasPrefix(iamInstanceProfileARN, roleEntry.BoundIamInstanceProfileARN) { + return nil, fmt.Sprintf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil + } + } + + // Check if the IAM role ARN of the instance trying to login, matches + // the IAM role ARN specified as a constraint on the role. + if roleEntry.BoundIamRoleARN != "" { + if instance.IamInstanceProfile == nil { + return nil, "", fmt.Errorf("IAM instance profile in the instance description is nil") + } + if instance.IamInstanceProfile.Arn == nil { + return nil, "", fmt.Errorf("IAM instance profile ARN in the instance description is nil") + } + + // Fetch the instance profile ARN from the instance description + iamInstanceProfileARN := *instance.IamInstanceProfile.Arn + + if iamInstanceProfileARN == "" { + return nil, "", fmt.Errorf("IAM instance profile ARN in the instance description is empty") + } + + // Extract out the instance profile name from the instance + // profile ARN + iamInstanceProfileARNSlice := strings.SplitAfter(iamInstanceProfileARN, ":instance-profile/") + iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1] + + if iamInstanceProfileName == "" { + return nil, "", fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") + } + + // Use instance profile ARN to fetch the associated role ARN + if iamClient == nil { + return nil, "", fmt.Errorf("Could not fetch IAM client") + } + iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName) + if err != nil { + return nil, "", fmt.Errorf("IAM role ARN could not be fetched: %v", err) + } + if iamRoleARN == "" { + return nil, "", fmt.Errorf("IAM role ARN could not be fetched") + } + + if !strings.HasPrefix(iamRoleARN, roleEntry.BoundIamRoleARN) { + return nil, fmt.Sprintf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil + } + } + + var roleTagResp *roleTagLoginResponse = nil + if roleEntry.RoleTag != "" { + roleTagResp, err := b.handleRoleTagLogin(s, roleName, roleEntry, instance) + if err != nil { + return nil, "", err + } + if roleTagResp == nil { + return nil, "failed to fetch and verify the role tag", nil + } + } + + return roleTagResp, "", nil +} + +// pathLoginUpdateInstanceIdentityDocument is used to create a Vault token by the EC2 instances // by providing the pkcs7 signature of the instance identity document // and a client created nonce. Client nonce is optional if 'disallow_reauthentication' // option is enabled on the registered role. -func (b *backend) pathLoginUpdate( +func (b *backend) pathLoginUpdateInstanceIdentityDocument( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { identityDocB64 := data.Get("identity").(string) var identityDocBytes []byte @@ -380,7 +519,7 @@ func (b *backend) pathLoginUpdate( // Validate the instance ID by making a call to AWS EC2 DescribeInstances API // and fetching the instance description. Validation succeeds only if the // instance is in 'running' state. - instanceDesc, err := b.validateInstance(req.Storage, identityDocParsed.InstanceID, identityDocParsed.Region) + instance, err := b.validateInstance(req.Storage, identityDocParsed.InstanceID, identityDocParsed.Region) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %v", err)), nil } @@ -394,72 +533,23 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil } - // Verify that the AMI ID of the instance trying to login matches the - // AMI ID specified as a constraint on the role - if roleEntry.BoundAmiID != "" && identityDocParsed.AmiID != roleEntry.BoundAmiID { - return logical.ErrorResponse(fmt.Sprintf("AMI ID %q does not belong to role %q", identityDocParsed.AmiID, roleName)), nil - } - // Verify that the AccountID of the instance trying to login matches the // AccountID specified as a constraint on the role if roleEntry.BoundAccountID != "" && identityDocParsed.AccountID != roleEntry.BoundAccountID { return logical.ErrorResponse(fmt.Sprintf("Account ID %q does not belong to role %q", identityDocParsed.AccountID, roleName)), nil } - // Check if the IAM instance profile ARN of the instance trying to - // login, matches the IAM instance profile ARN specified as a constraint - // on the role. - if roleEntry.BoundIamInstanceProfileARN != "" { - if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile == nil { - return nil, fmt.Errorf("IAM instance profile in the instance description is nil") - } - if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn == nil { - return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") - } - iamInstanceProfileARN := *instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn - if !strings.HasPrefix(iamInstanceProfileARN, roleEntry.BoundIamInstanceProfileARN) { - return logical.ErrorResponse(fmt.Sprintf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName)), nil - } + iamClient, err := b.clientIAM(req.Storage, identityDocParsed.Region) + if err != nil { + iamClient = nil } - // Check if the IAM role ARN of the instance trying to login, matches - // the IAM role ARN specified as a constraint on the role. - if roleEntry.BoundIamRoleARN != "" { - if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile == nil { - return nil, fmt.Errorf("IAM instance profile in the instance description is nil") - } - if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn == nil { - return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") - } - - // Fetch the instance profile ARN from the instance description - iamInstanceProfileARN := *instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn - - if iamInstanceProfileARN == "" { - return nil, fmt.Errorf("IAM instance profile ARN in the instance description is empty") - } - - // Extract out the instance profile name from the instance - // profile ARN - iamInstanceProfileARNSlice := strings.SplitAfter(iamInstanceProfileARN, ":instance-profile/") - iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1] - - if iamInstanceProfileName == "" { - return nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") - } - - // Use instance profile ARN to fetch the associated role ARN - iamRoleARN, err := b.instanceIamRoleARN(req.Storage, iamInstanceProfileName, identityDocParsed.Region) - if err != nil { - return nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err) - } - if iamRoleARN == "" { - return nil, fmt.Errorf("IAM role ARN could not be fetched") - } - - if !strings.HasPrefix(iamRoleARN, roleEntry.BoundIamRoleARN) { - return logical.ErrorResponse(fmt.Sprintf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName)), nil - } + roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, iamClient) + if err != nil { + return nil, err + } + if validationError != "" { + return logical.ErrorResponse(validationError), nil } // Get the entry from the identity whitelist, if there is one @@ -543,43 +633,34 @@ func (b *backend) pathLoginUpdate( policies := roleEntry.Policies rTagMaxTTL := time.Duration(0) - if roleEntry.RoleTag != "" { - // + if roleTagResp != nil { // Role tag is enabled on the role. // // Overwrite the policies with the ones returned from processing the role tag - resp, err := b.handleRoleTagLogin(req.Storage, identityDocParsed, roleName, roleEntry, instanceDesc) - if err != nil { - return nil, err - } - if resp == nil { - return logical.ErrorResponse("failed to fetch and verify the role tag"), nil - } - // If there are no policies on the role tag, policies on the role are inherited. // If policies on role tag are set, by this point, it is verified that it is a subset of the // policies on the role. So, apply only those. - if len(resp.Policies) != 0 { - policies = resp.Policies + if len(roleTagResp.Policies) != 0 { + policies = roleTagResp.Policies } // If roleEntry had disallowReauthentication set to 'true', do not reset it // to 'false' based on role tag having it not set. But, if role tag had it set, // be sure to override the value. if !disallowReauthentication { - disallowReauthentication = resp.DisallowReauthentication + disallowReauthentication = roleTagResp.DisallowReauthentication } // Cache the value of role tag's max_ttl value - rTagMaxTTL = resp.MaxTTL + rTagMaxTTL = roleTagResp.MaxTTL // Scope the shortestMaxTTL to the value set on the role tag - if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < shortestMaxTTL { - shortestMaxTTL = resp.MaxTTL + if roleTagResp.MaxTTL > time.Duration(0) && roleTagResp.MaxTTL < shortestMaxTTL { + shortestMaxTTL = roleTagResp.MaxTTL } - if resp.MaxTTL > longestMaxTTL { - longestMaxTTL = resp.MaxTTL + if roleTagResp.MaxTTL > longestMaxTTL { + longestMaxTTL = roleTagResp.MaxTTL } } @@ -625,6 +706,7 @@ func (b *backend) pathLoginUpdate( "role_tag_max_ttl": rTagMaxTTL.String(), "role": roleName, "ami_id": identityDocParsed.AmiID, + "auth_type": "instance_identity_document", }, LeaseOptions: logical.LeaseOptions{ Renewable: true, @@ -660,20 +742,17 @@ func (b *backend) pathLoginUpdate( // handleRoleTagLogin is used to fetch the role tag of the instance and // verifies it to be correct. Then the policies for the login request will be // set off of the role tag, if certain creteria satisfies. -func (b *backend) handleRoleTagLogin(s logical.Storage, identityDocParsed *identityDocument, roleName string, roleEntry *awsRoleEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) { - if identityDocParsed == nil { - return nil, fmt.Errorf("nil parsed identity document") - } +func (b *backend) handleRoleTagLogin(s logical.Storage, roleName string, roleEntry *awsRoleEntry, instance *ec2.Instance) (*roleTagLoginResponse, error) { if roleEntry == nil { return nil, fmt.Errorf("nil role entry") } - if instanceDesc == nil { - return nil, fmt.Errorf("nil instance description") + if instance == nil { + return nil, fmt.Errorf("nil instance") } - // Input validation on instanceDesc is not performed here considering + // Input validation on instance is not performed here considering // that it would have been done in validateInstance method. - tags := instanceDesc.Reservations[0].Instances[0].Tags + tags := instance.Tags if tags == nil || len(tags) == 0 { return nil, fmt.Errorf("missing tag with key %q on the instance", roleEntry.RoleTag) } @@ -707,7 +786,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDocParsed *ident } // If instance_id was set on the role tag, check if the same instance is attempting to login - if rTag.InstanceID != "" && rTag.InstanceID != identityDocParsed.InstanceID { + if rTag.InstanceID != "" && rTag.InstanceID != *instance.InstanceId { return nil, fmt.Errorf("role tag is being used by an unauthorized instance.") } @@ -734,6 +813,65 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDocParsed *ident // pathLoginRenew is used to renew an authenticated token func (b *backend) pathLoginRenew( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + authType, ok := req.Auth.Metadata["auth_type"] + if !ok { + // backwards compatibility for clients that have leases from before we added auth_type + authType = "instance_identity_document" + } + + if authType == "instance_identity_document" { + return b.pathLoginRenewInstanceIdentityDocument(req, data) + } else if authType == "signed_caller_identity_request" { + return b.pathLoginRenewSignedCallerIdentityRequest(req, data) + } else { + return nil, fmt.Errorf("unrecognized auth_type: '%s'", authType) + } +} + +func (b *backend) pathLoginRenewSignedCallerIdentityRequest( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + canonicalArn := req.Auth.Metadata["canonical_arn"] + if canonicalArn == "" { + return nil, fmt.Errorf("unable to retrieve canonical ARN from metadata during renewal") + } + + roleName := req.Auth.InternalData["role_name"].(string) + if roleName == "" { + return nil, fmt.Errorf("error retrieving role_name during renewal") + } + roleEntry, err := b.lockedAWSRole(req.Storage, roleName) + if err != nil { + return nil, err + } + if roleEntry == nil { + return nil, fmt.Errorf("role entry not found") + } + + if entityType, ok := req.Auth.Metadata["inferredEntityType"]; !ok { + if entityType == "ec2Instance" { + instanceID, ok := req.Auth.Metadata["inferredEntityId"] + if !ok { + return nil, fmt.Errorf("no inferred entity ID in auth metadata") + } + _, err := b.validateInstance(req.Storage, instanceID, roleEntry.InferredAWSRegion) + if err != nil { + return nil, fmt.Errorf("failed to verify instance ID '%s': %s", instanceID, err) + } + } else { + return nil, fmt.Errorf("unrecognized entity_type in metadata: '%s'", entityType) + } + } + + if roleEntry.BoundIamPrincipalARN != canonicalArn { + return nil, fmt.Errorf("role no longer bound to arn '%s'", canonicalArn) + } + + return framework.LeaseExtend(roleEntry.TTL, roleEntry.MaxTTL, b.System())(req, data) + +} + +func (b *backend) pathLoginRenewInstanceIdentityDocument( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { instanceID := req.Auth.Metadata["instance_id"] if instanceID == "" { @@ -815,6 +953,352 @@ func (b *backend) pathLoginRenew( // identityDocument represents the items of interest from the EC2 instance // identity document +func (b *backend) pathLoginUpdateCallerIdentityRequest( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + // BEGIN boring data parsing + method := data.Get("request_method").(string) + if method == "" { + return logical.ErrorResponse("missing method"), nil + } + + // In the future, might consider supporting GET + if method != "POST" { + return logical.ErrorResponse("invalid request_method; currently only 'POST' is supported"), nil + } + + rawUrl := data.Get("request_url").(string) + if rawUrl == "" { + return logical.ErrorResponse("missing request_url"), nil + } + parsedUrl, err := url.Parse(rawUrl) + if err != nil { + return logical.ErrorResponse("error parsing request_url"), 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("request_body").(string) + if bodyB64 == "" { + return logical.ErrorResponse("missing request_body"), nil + } + bodyRaw, err := base64.StdEncoding.DecodeString(bodyB64) + if err != nil { + return logical.ErrorResponse("request_body is invalid base64"), nil + } + body := string(bodyRaw) + + headersB64 := data.Get("request_headers").(string) + if headersB64 == "" { + return logical.ErrorResponse("missing request_headers"), nil + } + headersJson, err := base64.StdEncoding.DecodeString(headersB64) + if err != nil { + return logical.ErrorResponse("request_headers is invalid base64"), nil + } + var headers http.Header + err = jsonutil.DecodeJSON(headersJson, &headers) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("request_headers '%s' is invalid JSON: %s", headersJson, err)), nil + } + // END boring data parsing + + config, err := b.lockedClientConfigEntry(req.Storage) + if err != nil { + return logical.ErrorResponse("error getting configuration"), nil + } + + endpoint := "https://sts.amazonaws.com" + + if config != nil { + if config.HeaderValue != "" { + ok, msg := ensureVaultHeaderValue(headers, parsedUrl, config.HeaderValue) + if !ok { + return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %s", magicVaultHeader, msg)), nil + } + } + if config.STSEndpoint != "" { + endpoint = config.Endpoint + } + } + + clientArn, err := submitCallerIdentityRequest(method, endpoint, parsedUrl, body, headers) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %s", err)), nil + } + canonicalArn, principalName, sessionName, err := parseIamArn(clientArn) + if err != nil { + return logical.ErrorResponse("unrecognized IAM principal type"), nil + } + + roleName := data.Get("role").(string) + if roleName == "" { + roleName = principalName + } + + roleEntry, err := b.lockedAWSRole(req.Storage, roleName) + if err != nil { + return nil, err + } + if roleEntry == nil { + return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil + } + + // The role creation should ensure that either we're inferring this is an EC2 instance + // or that we're binding an ARN + if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn { + return logical.ErrorResponse(fmt.Sprintf("IAM Principal '%s' does not belong to the role '%s'", clientArn, roleName)), nil + } + + policies := roleEntry.Policies + rTagMaxTTL := time.Duration(0) + + inferredEntityType := "" + inferredEntityId := "" + if roleEntry.InferRoleType == "ec2Instance" { + instance, err := b.validateInstance(req.Storage, sessionName, roleEntry.InferredAWSRegion) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", sessionName, roleEntry.InferredAWSRegion)), nil + } + + // It's a bit of a hack to use the inferred "EC2" region to get a region for the IAM client + // IAM is a "global" service and so doesn't really have a region, so it wouldn't matter + // Except that the region is used to infer the partion (i.e., aws, aws-cn, or aws-us-gov), + // and we can safely assume that the EC2 client will be in the same partition as IAM + iamClient, err := b.clientIAM(req.Storage, roleEntry.InferredAWSRegion) + if err != nil { + iamClient = nil + } + + roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, iamClient) + if err != nil { + return nil, err + } + if validationError != "" { + return logical.ErrorResponse(validationError), nil + } + + if roleTagResp != nil { + if len(roleTagResp.Policies) != 0 { + policies = roleTagResp.Policies + } + + rTagMaxTTL = roleTagResp.MaxTTL + } + + inferredEntityType = "ec2Instance" + inferredEntityId = sessionName + } + + resp := &logical.Response{ + Auth: &logical.Auth{ + Policies: policies, + Metadata: map[string]string{ + "client_arn": clientArn, + "canonical_arn": canonicalArn, + "auth_type": "signed_caller_identity_request", + "role_tag_max_ttl": rTagMaxTTL.String(), + "inferredEntityType": inferredEntityType, + "inferredEntityId": inferredEntityId, + }, + InternalData: map[string]interface{}{ + "role_name": roleName, + }, + DisplayName: principalName, + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + TTL: roleEntry.TTL, + }, + }, + } + + shortestTTL := b.System().DefaultLeaseTTL() + if roleEntry.TTL > time.Duration(0) && roleEntry.TTL < shortestTTL { + shortestTTL = roleEntry.TTL + } + + maxTTL := b.System().MaxLeaseTTL() + if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < maxTTL { + maxTTL = roleEntry.MaxTTL + } + if rTagMaxTTL > time.Duration(0) && rTagMaxTTL < maxTTL { + maxTTL = rTagMaxTTL + } + + if shortestTTL > maxTTL { + resp.AddWarning(fmt.Sprintf("Effective TTL of %q exceeded the effective max_ttl of %q; TTL value is capped accordingly", (shortestTTL / time.Second).String(), (maxTTL / time.Second).String())) + shortestTTL = maxTTL + } + + resp.Auth.TTL = shortestTTL + + return resp, nil +} + +func parseIamArn(iamArn string) (string, string, string, error) { + fullParts := strings.Split(iamArn, ":") + principalFullName := fullParts[5] + parts := strings.Split(principalFullName, "/") + principalName := parts[1] + transformedArn := iamArn + sessionName := "" + if parts[0] == "assumed-role" { + transformedArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", fullParts[4], principalName) + sessionName = parts[2] + } else if parts[0] != "user" { + return "", "", "", fmt.Errorf("unrecognized principal type: '%s'", parts[0]) + } + return transformedArn, principalName, sessionName, nil +} + +func ensureVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) (bool, string) { + providedValue := "" + for k, v := range headers { + if strings.ToLower(magicVaultHeader) == strings.ToLower(k) { + providedValue = strings.Join(v, ",") + break + } + } + if providedValue == "" { + return false, fmt.Sprintf("didn't find %s", magicVaultHeader) + } + + // NOT doing a constant time compare here since the value is NOT intended to be secret + if providedValue != requiredHeaderValue { + return false, fmt.Sprintf("expected %s but got %s", 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 false, "vault header wasn't signed" + } + if len(matches) > 2 { + return false, "found multiple SignedHeaders components" + } + signedHeaders := string(matches[1]) + return ensureHeaderIsSigned(signedHeaders, magicVaultHeader) + } + // 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 false, "Missing Authorization header" +} + +func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) *http.Request { + // This is all a bit complicated because the AWS signature algorithm requires that + // the Host header be included in the signed headers. See + // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + // The use cases we want to support, in order of increasing complexity, are: + // 1. All defaults (client assumes sts.amazonaws.com and server has no override) + // 2. Alternate STS regions: client wants to go to a specific region, in which case + // Vault must be confiugred with that endpoint as well. The client's signed request + // will include a signature over what the client expects the Host header to be, + // so we cannot change that and must match. + // 3. Alternate STS regions with a proxy that is transparent to Vault's clients. + // In this case, Vault is aware of the proxy, as the proxy is configured as the + // endpoint, but the clients should NOT be aware of the proxy (because STS will + // not be aware of the proxy) + // It's also annoying because: + // 1. The AWS Sigv4 algorithm requires the Host header to be defined + // 2. Some of the official SDKs (at least botocore and aws-sdk-go) don't actually + // incude an explicit Host header in the HTTP requests they generate, relying on + // the underlying HTTP library to do that for them. + // 3. To get a validly signed request, the SDKs check if a Host header has been set + // and, if not, add an inferred host header (based on the URI) to the internal + // data structure used for calculating the signature, but never actually expose + // that to clients. So then they just "hope" that the underlying library actually + // adds the right Host header which was included in the signature calculation. + // We could either explicity require all Vault clients to explicitly add the Host header + // in the encoded request, or we could also implicitly infer it from the URI. + // We choose to support both -- allow you to explicitly set a Host header, but if not, + // infer one from the URI. + // HOWEVER, we have to preserve the request URI portion of the client's + // URL because the GetCallerIdentity Action can be encoded in either the body + // or the URL. So, we need to rebuild the URL sent to the http library to have the + // custom, Vault-specified endpoint with the client-side request parameters. + targetUrl := fmt.Sprintf("%s/%s", endpoint, parsedUrl.RequestURI()) + request, err := http.NewRequest(method, targetUrl, strings.NewReader(body)) + if err != nil { + return nil + } + request.Host = parsedUrl.Host + for k, vals := range headers { + for _, val := range vals { + request.Header.Add(k, val) + } + } + return request +} + +func ensureHeaderIsSigned(signedHeaders, headerToSign string) (bool, string) { + // Not doing a constant time compare here, the values aren't secret + for _, header := range strings.Split(signedHeaders, ";") { + if header == strings.ToLower(headerToSign) { + return true, "" + } + } + return false, fmt.Sprintf("Vault header wasn't signed") +} + +func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, error) { + decoder := xml.NewDecoder(strings.NewReader(response)) + result := GetCallerIdentityResponse{} + err := decoder.Decode(&result) + return result, err +} + +func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) (string, error) { + // NOTE: We need to ensure we're calling STS, instead of acting as an unintended network proxy + // The protection against this is that this method will only call the endpoint specified in the + // client config (defaulting to sts.amazonaws.com), so it would require a Vault admin to override + // the endpoint to talk to alternate web addresses + request := buildHttpRequest(method, endpoint, parsedUrl, body, headers) + client := cleanhttp.DefaultClient() + response, err := client.Do(request) + if err != nil { + return "", fmt.Errorf("error making request: %s", err) + } + if response != nil { + defer response.Body.Close() + } + // we check for status code afterwards to also print out response body + responseBody, err := ioutil.ReadAll(response.Body) + if response.StatusCode != 200 { + return "", fmt.Errorf("received error code %s from STS: %s", response.StatusCode, string(responseBody)) + } + callerIdentityResponse, err := parseGetCallerIdentityResponse(string(responseBody)) + if err != nil { + return "", fmt.Errorf("error parsing STS response") + } + clientArn := callerIdentityResponse.GetCallerIdentityResult[0].Arn + if clientArn == "" { + return "", fmt.Errorf("no ARN validated") + } + return clientArn, nil +} + +type GetCallerIdentityResponse struct { + XMLName xml.Name `xml:"GetCallerIdentityResponse"` + GetCallerIdentityResult []GetCallerIdentityResult `xml:"GetCallerIdentityResult"` + ResponseMetadata []ResponseMetadata `xml:"ResponseMetadata"` +} + +type GetCallerIdentityResult struct { + Arn string `xml:"Arn"` + UserId string `xml:"UserId"` + Account string `xml:"Account"` +} + +type ResponseMetadata struct { + RequestId string `xml:"RequestId"` +} + +// Struct to represent items of interest from the EC2 instance identity document. type identityDocument struct { Tags map[string]interface{} `json:"tags,omitempty" structs:"tags" mapstructure:"tags"` InstanceID string `json:"instanceId,omitempty" structs:"instanceId" mapstructure:"instanceId"` @@ -832,11 +1316,21 @@ type roleTagLoginResponse struct { DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } +const magicVaultHeader = "X-Vault-AWSIAM-Server-Id" + const pathLoginSyn = ` Authenticates an EC2 instance with Vault. ` const pathLoginDesc = ` +Authenticate AWS entities, either an arbitrary IAM principal or EC2 instances. + +IAM principals are authenticated by processing a signed sts:GetCallerIdentity +request and then parsing the response to see who signed the request. Optionally, +the caller can be inferred to be another AWS entity type, with EC2 instances +the only currently supported entity type, and additional filtering can be +implemented based on that inferred type. + An EC2 instance is authenticated using the PKCS#7 signature of the instance identity document and a client created nonce. This nonce should be unique and should be used by the instance for all future logins, unless 'disallow_reauthenitcation' option on the diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go new file mode 100644 index 000000000000..60dde74c6228 --- /dev/null +++ b/builtin/credential/aws/path_login_test.go @@ -0,0 +1,140 @@ +package awsauth + +import ( + "net/http" + "net/url" + "testing" +) + +func TestBackend_pathLogin_getCallerIdentityResponse(t *testing.T) { + responseFromUser := ` + + arn:aws:iam::123456789012:user/MyUserName + ASOMETHINGSOMETHINGSOMETHING + 123456789012 + + + 7f4fc40c-853a-11e6-8848-8d035d01eb87 + +` + expectedUserArn := "arn:aws:iam::123456789012:user/MyUserName" + + responseFromAssumedRole := ` + + arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName + ASOMETHINGSOMETHINGELSE:RoleSessionName + 123456789012 + + + 7f4fc40c-853a-11e6-8848-8d035d01eb87 + +` + expectedRoleArn := "arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName" + + parsedUserResponse, err := parseGetCallerIdentityResponse(responseFromUser) + if parsed_arn := parsedUserResponse.GetCallerIdentityResult[0].Arn; parsed_arn != expectedUserArn { + t.Errorf("expected to parse arn %#v, got %#v", expectedUserArn, parsed_arn) + } + + parsedRoleResponse, err := parseGetCallerIdentityResponse(responseFromAssumedRole) + if parsed_arn := parsedRoleResponse.GetCallerIdentityResult[0].Arn; parsed_arn != expectedRoleArn { + t.Errorf("expected to parn arn %#v; got %#v", expectedRoleArn, parsed_arn) + } + + _, err = parseGetCallerIdentityResponse("SomeRandomGibberish") + if err == nil { + t.Errorf("expected to NOT parse random giberish, but didn't get an error") + } +} + +func TestBackend_pathLogin_parseIamArn(t *testing.T) { + userArn := "arn:aws:iam::123456789012:user/MyUserName" + assumedRoleArn := "arn:aws:sts::123456789012:assumed-role/RoleName/RoleSessionName" + baseRoleArn := "arn:aws:iam::123456789012:role/RoleName" + + xformedUser, principalFriendlyName, sessionName, err := parseIamArn(userArn) + if err != nil { + t.Fatal(err) + } + if xformedUser != userArn { + t.Fatalf("expected to transform ARN %#v into %#v but got %#v instead", userArn, userArn, xformedUser) + } + if principalFriendlyName != "MyUserName" { + t.Fatalf("expected to extract MyUserName from ARN %#v but got %#v instead", userArn, principalFriendlyName) + } + if sessionName != "" { + t.Fatalf("expected to extract no session name from ARN %#v but got %#v instead", userArn, sessionName) + } + + xformedRole, principalFriendlyName, sessionName, err := parseIamArn(assumedRoleArn) + if err != nil { + t.Fatal(err) + } + if xformedRole != baseRoleArn { + t.Fatalf("expected to transform ARN %#v into %#v but got %#v instead", assumedRoleArn, baseRoleArn, xformedRole) + } + if principalFriendlyName != "RoleName" { + t.Fatalf("expected to extract principal name of RoleName from ARN %#v but got %#v instead", assumedRoleArn, sessionName) + } + if sessionName != "RoleSessionName" { + t.Fatalf("expected to extract role session name of RoleSessionName from ARN %#v but got %#v instead", assumedRoleArn, sessionName) + } +} + +func TestBackend_ensureVaultHeaderValue(t *testing.T) { + const canaryHeaderValue = "Vault-Server" + requestUrl, err := url.Parse("https://sts.amazonaws.com/") + if err != nil { + t.Fatalf("error parsing test URL: %s", err) + } + postHeadersMissing := http.Header{ + "Host": []string{"Foo"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + postHeadersInvalid := http.Header{ + "Host": []string{"Foo"}, + magicVaultHeader: []string{"InvalidValue"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + postHeadersUnsigned := http.Header{ + "Host": []string{"Foo"}, + magicVaultHeader: []string{canaryHeaderValue}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + postHeadersValid := http.Header{ + "Host": []string{"Foo"}, + magicVaultHeader: []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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + + postHeadersSplit := http.Header{ + "Host": []string{"Foo"}, + magicVaultHeader: []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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + + found, errMsg := ensureVaultHeaderValue(postHeadersMissing, requestUrl, canaryHeaderValue) + if found { + t.Error("validated POST request with missing Vault header") + } + + found, errMsg = ensureVaultHeaderValue(postHeadersInvalid, requestUrl, canaryHeaderValue) + if found { + t.Error("validated POST request with invalid Vault header value") + } + + found, errMsg = ensureVaultHeaderValue(postHeadersUnsigned, requestUrl, canaryHeaderValue) + if found { + t.Error("validated POST request with unsigned Vault header") + } + + found, errMsg = ensureVaultHeaderValue(postHeadersValid, requestUrl, canaryHeaderValue) + if !found { + t.Errorf("did NOT validate valid POST request: %s", errMsg) + } + + found, errMsg = ensureVaultHeaderValue(postHeadersSplit, requestUrl, canaryHeaderValue) + if !found { + t.Errorf("did NOT validate valid POST request with split Authorization header: %s", errMsg) + } +} diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 10be3224986d..8be13fccb016 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" @@ -20,6 +20,12 @@ func pathRole(b *backend) *framework.Path { Type: framework.TypeString, Description: "Name of the role.", }, + "allowed_auth_types": { + Type: framework.TypeString, + Default: "instance_identity_document", + Description: `The comma-separated list of allowed auth_type values that +are allowed to authenticate to this role.`, + }, "bound_ami_id": { Type: framework.TypeString, Description: `If set, defines a constraint on the EC2 instances that they should be @@ -29,6 +35,11 @@ using the AMI ID specified by this parameter.`, Type: framework.TypeString, Description: `If set, defines a constraint on the EC2 instances that the account ID in its identity document to match the one specified by this parameter.`, + }, + "bound_iam_principal_arn": { + Type: framework.TypeString, + Description: `ARN of the IAM principal to bind to this role. Only applicable when +auth_type is signed_caller_identity_request.`, }, "bound_iam_role_arn": { Type: framework.TypeString, @@ -37,19 +48,43 @@ that it must match the IAM role ARN specified by this parameter. The value is prefix-matched (as though it were a glob ending in '*'). The configured IAM user or EC2 instance role must be allowed to execute the 'iam:GetInstanceProfile' action if this is -specified.`, +specified. This is only checked when auth_type is +instance_identity_document.`, }, "bound_iam_instance_profile_arn": { Type: framework.TypeString, Description: `If set, defines a constraint on the EC2 instances to be associated with an IAM instance profile ARN which has a prefix that matches the value specified by this parameter. The value is prefix-matched -(as though it were a glob ending in '*').`, +(as though it were a glob ending in '*'). This is only checked when +auth_type is instance_identity_document.`, + }, + "infer_role_as_type": { + Type: framework.TypeString, + Default: false, + Description: `When auth_type is signed_caller_identity_request, the +AWS entity type to infer from the authenticated principal. The only supported +value is ec2Instance, which will extract the EC2 instance ID from the +authenticated role and apply restrictions specific to EC2 instances (such as +role_tag). The configured EC2 client must be able to find the inferred instance +ID in the results, and the instance must be running. If unable to +determine the EC2 instance ID or unable to find the EC2 instance ID +among running instances, then authentication will fail.`, + }, + "inferred_aws_region": { + Type: framework.TypeString, + Description: `When auth_type is signed_caller_identity_request and +infer_role_as_type is set, the region to assume the inferred entity exists in.`, }, "role_tag": { - Type: framework.TypeString, - Default: "", - Description: "If set, enables the role tags for this role. The value set for this field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using 'role//tag' endpoint. Defaults to an empty string, meaning that role tags are disabled.", + Type: framework.TypeString, + Default: "", + Description: `If set, enables the role tags for this role. The value set for this +field should be the 'key' of the tag on the EC2 instance. The 'value' +of the tag should be generated using 'role//tag' endpoint. +Defaults to an empty string, meaning that role tags are disabled. This +is only checked if auth_type is instance_identity_document or +infer_role_as_ec2_instance is true`, }, "ttl": { Type: framework.TypeDurationSecond, @@ -68,9 +103,14 @@ to 0, in which case the value will fallback to the system/mount defaults.`, Description: "Policies to be set on tokens issued using this role.", }, "allow_instance_migration": { - Type: framework.TypeBool, - Default: false, - Description: "If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution.", + Type: framework.TypeBool, + Default: false, + Description: `If set, allows migration of the underlying instance where the client +resides. This keys off of pendingTime in the metadata document, so +essentially, this disables the client nonce check whenever the +instance is migrated to a new host and pendingTime is newer than the +previously-remembered time. Use with caution. This is only checked when +auth_type is instance_identity_document.`, }, "disallow_reauthentication": { Type: framework.TypeBool, @@ -314,15 +354,88 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.BoundIamInstanceProfileARN = boundIamInstanceProfileARNRaw.(string) } - // Ensure that at least one bound is set on the role - switch { - case roleEntry.BoundAccountID != "": - case roleEntry.BoundAmiID != "": - case roleEntry.BoundIamInstanceProfileARN != "": - case roleEntry.BoundIamRoleARN != "": - default: + if boundIamPrincipalARNRaw, ok := data.GetOk("bound_iam_principal_arn"); ok { + roleEntry.BoundIamPrincipalARN = boundIamPrincipalARNRaw.(string) + } + + if inferRoleTypeRaw, ok := data.GetOk("infer_role_as_type"); ok { + roleEntry.InferRoleType = inferRoleTypeRaw.(string) + } + + if inferredAWSRegionRaw, ok := data.GetOk("inferred_aws_region"); ok { + roleEntry.InferredAWSRegion = inferredAWSRegionRaw.(string) + } + + allowInstanceIdentityDocument, allowCallerIdentity := false, false - return logical.ErrorResponse("at least be one bound parameter should be specified on the role"), nil + parseAllowedAuthTypes := func(input string) string { + allowedAuthTypes := []string{} + for _, t := range strings.Split(input, ",") { + if t == "instance_identity_document" { + allowInstanceIdentityDocument = true + allowedAuthTypes = append(allowedAuthTypes, t) + } else if t == "signed_caller_identity_request" { + allowCallerIdentity = true + allowedAuthTypes = append(allowedAuthTypes, t) + } else if t != "" { + return fmt.Sprintf("unrecognized auth type: '%s'", t) + } + } + roleEntry.AllowedAuthTypes = allowedAuthTypes + return "" + } + + if allowedAuthTypesRaw, ok := data.GetOk("allowed_auth_types"); ok { + err := parseAllowedAuthTypes(allowedAuthTypesRaw.(string)) + if err != "" { + return logical.ErrorResponse(err), nil + } + } else if req.Operation == logical.CreateOperation { + err := parseAllowedAuthTypes(data.Get("allowed_auth_types").(string)) + if err != "" { + return logical.ErrorResponse(err), nil + } + } + + if allowInstanceIdentityDocument || (allowCallerIdentity && roleEntry.BoundIamPrincipalARN == "") { + // Ensure that at least one bound is set on the role + switch { + case roleEntry.BoundAccountID != "": + case roleEntry.BoundAmiID != "": + case roleEntry.BoundIamInstanceProfileARN != "": + case roleEntry.BoundIamRoleARN != "": + default: + + return logical.ErrorResponse("at least be one bound parameter should be specified on the role with auth_type instance_identity_document or auth_type signed_caller_identity_request"), nil + } + } + + if allowCallerIdentity { + if roleEntry.InferRoleType != "" { + if roleEntry.InferRoleType != "ec2Instance" { + return logical.ErrorResponse(fmt.Sprintf("invalid infer_role_as_type value: '%s'", roleEntry.InferRoleType)), nil + } + if roleEntry.InferredAWSRegion == "" { + return logical.ErrorResponse("must set inferred_aws_region when setting infer_role_as_type"), nil + } + } else { + if roleEntry.BoundIamPrincipalARN == "" { + return logical.ErrorResponse("must set bound_iam_principal_arn if not setting infer_role_as_type"), nil + } + if roleEntry.InferredAWSRegion != "" { + return logical.ErrorResponse("must not set inferred_aws_region without infer_role_as_type"), nil + } + } + } else { + if roleEntry.InferRoleType != "" { + return logical.ErrorResponse("must only set infer_role_as_type when allowing signed_caller_identity_request auth_type"), nil + } + if roleEntry.BoundIamPrincipalARN != "" { + return logical.ErrorResponse("must only set bound_iam_principal_arn when allowing signed_caller_identity_request auth_type"), nil + } + if roleEntry.InferredAWSRegion != "" { + return logical.ErrorResponse("must not set inferred_aws_region without allowing signed_caller_identity_request auth_type"), nil + } } policiesStr, ok := data.GetOk("policies") @@ -413,10 +526,14 @@ func (b *backend) pathRoleCreateUpdate( // Struct to hold the information associated with an AMI ID in Vault. type awsRoleEntry struct { + AllowedAuthTypes []string `json:"allowed_auth_types" structs:"allowed_auth_types" mapstructure:"allowed_auth_types"` BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"` BoundAccountID string `json:"bound_account_id" structs:"bound_account_id" mapstructure:"bound_account_id"` + BoundIamPrincipalARN string `json:"bound_iam_principal_arn" structs:"bound_iam_principal_arn" mapstructure:"bound_iam_principal_arn"` BoundIamRoleARN string `json:"bound_iam_role_arn" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"` BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"` + InferRoleType string `json:"infer_role_type" structs:"infer_role_type" mapstructure:"infer_role_type"` + InferredAWSRegion string `json:"inferred_aws_region" structs:"inferred_aws_region" mapstructure:"inferred_aws_region"` RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` @@ -443,6 +560,7 @@ endpoint 'role//tag'. This tag then needs to be applied on the instance before it attempts a login. The policies on the tag should be a subset of policies that are associated to the role. In order to enable login using tags, 'role_tag' option should be set while creating a role. +This only applies when authenticating EC2 instances. Also, a 'max_ttl' can be configured in this endpoint that determines the maximum duration for which a login can be renewed. Note that the 'max_ttl' has an upper diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index 5544fd30177f..5c8a119b14b3 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "crypto/hmac" diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go new file mode 100644 index 000000000000..721413d7688a --- /dev/null +++ b/builtin/credential/aws/path_role_test.go @@ -0,0 +1,374 @@ +package awsauth + +import ( + "strings" + "testing" + + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/logical" +) + +func TestBackend_pathRoleInstanceIdentityDocument(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Setup(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "p,q,r,s", + "max_ttl": "2h", + "bound_ami_id": "ami-abcd123", + } + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/ami-abcd123", + Data: data, + Storage: storage, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "role/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatal("failed to read the role entry") + } + if !policyutil.EquivalentPolicies(strings.Split(data["policies"].(string), ","), resp.Data["policies"].([]string)) { + t.Fatalf("bad: policies: expected: %#v\ngot: %#v\n", data, resp.Data) + } + + data["allow_instance_migration"] = true + data["disallow_reauthentication"] = true + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/ami-abcd123", + Data: data, + Storage: storage, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "role/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if !resp.Data["allow_instance_migration"].(bool) || !resp.Data["disallow_reauthentication"].(bool) { + t.Fatal("bad: expected:true got:false\n") + } + + // add another entry, to test listing of role entries + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/ami-abcd456", + Data: data, + Storage: storage, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ListOperation, + Path: "roles", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil || resp.IsError() { + t.Fatalf("failed to list the role entries") + } + keys := resp.Data["keys"].([]string) + if len(keys) != 2 { + t.Fatalf("bad: keys: %#v\n", keys) + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.DeleteOperation, + Path: "role/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "role/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatalf("bad: response: expected:nil actual:%#v\n", resp) + } + +} + +func TestBackend_pathRoleSignedCallerIdentity(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Setup(config) + if err != nil { + t.Fatal(err) + } + + // make sure we start with empty roles, which gives us confidence that the read later + // actually is the two roles we created + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ListOperation, + Path: "roles", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil || resp.IsError() { + t.Fatalf("failed to list role entries") + } + if resp.Data["keys"] != nil { + t.Fatalf("Received roles when expected none") + } + + data := map[string]interface{}{ + "allowed_auth_types": "signed_caller_identity_request", + "policies": "p,q,r,s", + "max_ttl": "2h", + "bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName", + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/MyRoleName", + Data: data, + Storage: storage, + }) + + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatal("failed to create the role entry") + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "role/MyRoleName", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatal("failed to read the role entry") + } + if !policyutil.EquivalentPolicies(strings.Split(data["policies"].(string), ","), resp.Data["policies"].([]string)) { + t.Fatalf("bad: policies: expected %#v\ngot: %#v\n", data, resp.Data) + } + + data["infer_role_as_type"] = "invalid" + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/ShouldNeverExist", + Data: data, + Storage: storage, + }) + if resp == nil || !resp.IsError() { + t.Fatalf("Created role with invalid infer_role_as_type") + } + if err != nil { + t.Fatal(err) + } + + data["infer_role_as_type"] = "ec2Instance" + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/ShouldNeverExist", + Data: data, + Storage: storage, + }) + if resp == nil || !resp.IsError() { + t.Fatalf("Created role without necessary inferred_aws_region") + } + if err != nil { + t.Fatal(err) + } + + delete(data, "bound_iam_principal_arn") + data["inferred_aws_region"] = "us-east-1" + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/ShouldNeverExist", + Data: data, + Storage: storage, + }) + if resp == nil || !resp.IsError() { + t.Fatalf("Created role without anything bound") + } + if err != nil { + t.Fatal(err) + } + + // generate a second role, ensure we're able to list both + data["bound_ami_id"] = "ami-abcd123" + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/MyOtherRoleName", + Data: data, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf("failed to create additional role: %s") + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ListOperation, + Path: "roles", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil || resp.IsError() { + t.Fatalf("failed to list role entries") + } + keys := resp.Data["keys"].([]string) + if len(keys) != 2 { + t.Fatalf("bad: keys %#v\n", keys) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.DeleteOperation, + Path: "role/MyOtherRoleName", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "role/MyOtherRoleName", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatalf("bad: response: expected: nil actual:%3v\n", resp) + } +} + +func TestBackend_pathRoleMixedTypes(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Setup(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "p,q,r,s", + "bound_ami_id": "ami-abc1234", + "allowed_auth_types": "instance_identity_document,invalid", + } + + submitCreateRequest := func(roleName string) (*logical.Response, error) { + return b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/" + roleName, + Data: data, + Storage: storage, + }) + } + + resp, err := submitCreateRequest("shouldNeverExist") + if resp != nil && !resp.IsError() { + t.Fatalf("created role with invalid allowed_auth_type") + } + if err != nil { + t.Fatal(err) + } + + data["allowed_auth_types"] = "instance_identity_document,,signed_caller_identity_request" + resp, err = submitCreateRequest("shouldNeverExist") + if resp != nil && !resp.IsError() { + t.Fatalf("created role without required bound_iam_principal_arn") + } + if err != nil { + t.Fatal(err) + } + + data["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/MyRole" + delete(data, "bound_ami_id") + resp, err = submitCreateRequest("shouldNeverExist") + if resp != nil && !resp.IsError() { + t.Fatalf("created role without required instance_identity_document binding") + } + if err != nil { + t.Fatal(err) + } + + data["bound_ami_id"] = "ami-1234567" + resp, err = submitCreateRequest("multipleTypes") + if err != nil { + t.Fatal(err) + } + if resp.IsError() { + t.Fatalf("didn't allow creation of valid role with multiple bindings of different types") + } + + delete(data, "bound_iam_principal_arn") + data["infer_role_as_type"] = "ec2Instance" + data["inferred_aws_region"] = "us-east-1" + resp, err = submitCreateRequest("multipleTypesInferred") + if err != nil { + t.Fatal(err) + } + if resp.IsError() { + t.Fatalf("didn't allow creation of roles with only inferred bindings") + } +} diff --git a/builtin/credential/aws/path_roletag_blacklist.go b/builtin/credential/aws/path_roletag_blacklist.go index c3faa4cba946..32fded87852e 100644 --- a/builtin/credential/aws/path_roletag_blacklist.go +++ b/builtin/credential/aws/path_roletag_blacklist.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "encoding/base64" diff --git a/builtin/credential/aws/path_tidy_identity_whitelist.go b/builtin/credential/aws/path_tidy_identity_whitelist.go index 266d4596f28f..c77687f6322d 100644 --- a/builtin/credential/aws/path_tidy_identity_whitelist.go +++ b/builtin/credential/aws/path_tidy_identity_whitelist.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" diff --git a/builtin/credential/aws/path_tidy_roletag_blacklist.go b/builtin/credential/aws/path_tidy_roletag_blacklist.go index d163968ddb69..3970a1815d9a 100644 --- a/builtin/credential/aws/path_tidy_roletag_blacklist.go +++ b/builtin/credential/aws/path_tidy_roletag_blacklist.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" diff --git a/cli/commands.go b/cli/commands.go index 5b96b885f03e..bc57d7bddcbd 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -9,7 +9,7 @@ import ( credAppId "github.com/hashicorp/vault/builtin/credential/app-id" credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - credAwsEc2 "github.com/hashicorp/vault/builtin/credential/aws" + credAws "github.com/hashicorp/vault/builtin/credential/aws" credCert "github.com/hashicorp/vault/builtin/credential/cert" credGitHub "github.com/hashicorp/vault/builtin/credential/github" credLdap "github.com/hashicorp/vault/builtin/credential/ldap" @@ -67,7 +67,8 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { CredentialBackends: map[string]logical.Factory{ "approle": credAppRole.Factory, "cert": credCert.Factory, - "aws-ec2": credAwsEc2.Factory, + "aws": credAws.Factory, + "aws-ec2": credAws.Factory, "app-id": credAppId.Factory, "github": credGitHub.Factory, "userpass": credUserpass.Factory, From 1826aca9dee8415b3842d30551c34bc0614157a2 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Mon, 23 Jan 2017 22:57:53 -0500 Subject: [PATCH 03/20] Add missing aws auth handler to CLI This was omitted from the previous commit --- cli/commands.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/commands.go b/cli/commands.go index bc57d7bddcbd..b87765cd088b 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -112,6 +112,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { "userpass": &credUserpass.CLIHandler{}, "ldap": &credLdap.CLIHandler{}, "cert": &credCert.CLIHandler{}, + "aws": &credAws.CLIHandler{}, }, }, nil }, From 32a3fa963f6ed85d61bb73cee9ea170aefe60817 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Wed, 25 Jan 2017 01:34:52 -0500 Subject: [PATCH 04/20] aws auth backend general variable name cleanup Also fixed a bug where allowed auth types weren't being checked upon login, and added tests for it. --- builtin/credential/aws/backend_test.go | 65 ++++++++++++++- builtin/credential/aws/cli.go | 2 +- builtin/credential/aws/path_config_client.go | 6 +- .../credential/aws/path_config_client_test.go | 8 +- builtin/credential/aws/path_login.go | 82 +++++++++++-------- builtin/credential/aws/path_role.go | 56 ++++++------- builtin/credential/aws/path_role_test.go | 16 ++-- 7 files changed, 155 insertions(+), 80 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 37e2a6eb74a6..238baf3f481c 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1089,8 +1089,24 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) } - // Place the correct IAM Role ARN + // place the correct IAM role ARN, but make the auth type wrong data["bound_iam_role_arn"] = iamARN + data["bound_iam_principal_arn"] = iamARN + data["allowed_auth_types"] = "iam" + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) + } + + // Attempt to login and expect a fail because auth_type is wrong + resp, err = b.HandleRequest(loginRequest) + if err != nil || resp == nil || (resp != nil && !resp.IsError()) { + t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) + } + + // Place the correct auth type + delete(data, "bound_iam_principal_arn") + data["allowed_auth_types"] = "ec2" resp, err = b.HandleRequest(roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1165,7 +1181,7 @@ func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[s return nil, err } return map[string]interface{}{ - "auth_type": "signed_caller_identity_request", + "auth_type": "iam", "request_method": request.Method, "request_url": request.URL.String(), "request_headers": base64.StdEncoding.EncodeToString(headersJson), @@ -1251,7 +1267,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { const testInvalidRoleName = "invalid-role" clientConfigData := map[string]interface{}{ - "caller_identity_header_value": testVaultHeaderValue, + "iam_auth_header_value": testVaultHeaderValue, } clientRequest := &logical.Request{ Operation: logical.UpdateOperation, @@ -1268,7 +1284,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { roleData := map[string]interface{}{ "bound_iam_principal_arn": testIdentityArn, "policies": "root", - "allowed_auth_types": "signed_caller_identity_request", + "allowed_auth_types": "iam", } roleRequest := &logical.Request{ Operation: logical.UpdateOperation, @@ -1281,6 +1297,30 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) } + // configuring a valid role we won't be able to login to + roleDataEc2 := map[string]interface{}{ + "policies": "root", + "bound_ami_id": "ami-1234567", + } + roleRequestEc2 := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/ec2only", + Storage: storage, + Data: roleDataEc2, + } + resp, err = b.HandleRequest(roleRequestEc2) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: failed to create role; resp:%#v\nerr:%v", resp, err) + } + + roleDataEc2["allowed_auth_types"] = "ec2,iam" + roleDataEc2["bound_iam_principal_arn"] = testIdentityArn + roleRequestEc2.Path = "role/ec2Iam" + resp, err = b.HandleRequest(roleRequestEc2) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: failed to create role; resp:%#v\nerr:%v", resp, err) + } + // now we're creating the invalid role we won't be able to login to roleData["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/FakeRole" roleRequest.Path = "role/" + testInvalidRoleName @@ -1349,6 +1389,14 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { t.Errorf("bad: expected failed login due to invalid role: resp:%#v\nerr:%v", resp, err) } + loginData["role"] = "ec2only" + resp, err = b.HandleRequest(loginRequest) + if err != nil || resp == nil || !resp.IsError() { + t.Errorf("bad: expected failed login due to bad auth type: resp:%#v\nerr:%v", resp, err) + } + + // finally, the happy path tests :) + loginData["role"] = testValidRoleName resp, err = b.HandleRequest(loginRequest) if err != nil { @@ -1357,4 +1405,13 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { if resp == nil || resp.Auth == nil || resp.IsError() { t.Errorf("bad: expected valid login: resp:%#v", resp) } + + loginData["role"] = "ec2Iam" + resp, err = b.HandleRequest(loginRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Errorf("bad: expected valid login: resp:%#v", resp) + } } diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index ae61e400eba6..2f7f040c7e01 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -74,7 +74,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login", mount) secret, err := c.Logical().Write(path, map[string]interface{}{ - "auth_type": "signed_caller_identity_request", + "auth_type": "iam", "request_method": method, "request_url": targetUrl, "request_headers": headers, diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 67333aeae38e..ab382a0ea002 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -40,7 +40,7 @@ func pathConfigClient(b *backend) *framework.Path { Description: "URL to override the default generated endpoint for making AWS STS API calls.", }, - "caller_identity_header_value": &framework.FieldSchema{ + "iam_auth_header_value": &framework.FieldSchema{ Type: framework.TypeString, Default: "", Description: "Value to require in the X-Vault-AWSIAM-Server-ID request header", @@ -200,14 +200,14 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry.STSEndpoint = data.Get("sts_endpoint").(string) } - headerValStr, ok := data.GetOk("caller_identity_header_value") + headerValStr, ok := data.GetOk("iam_auth_header_value") if ok { if configEntry.HeaderValue != headerValStr.(string) { // NOT setting changedCreds here, since this isn't really cached configEntry.HeaderValue = headerValStr.(string) } } else if req.Operation == logical.CreateOperation { - configEntry.HeaderValue = data.Get("caller_identity_header_value").(string) + configEntry.HeaderValue = data.Get("iam_auth_header_value").(string) } // Since this endpoint supports both create operation and update operation, diff --git a/builtin/credential/aws/path_config_client_test.go b/builtin/credential/aws/path_config_client_test.go index bf951ac5b624..94590eb46f6b 100644 --- a/builtin/credential/aws/path_config_client_test.go +++ b/builtin/credential/aws/path_config_client_test.go @@ -41,8 +41,8 @@ func TestBackend_pathConfigClient(t *testing.T) { } data := map[string]interface{}{ - "sts_endpoint": "https://my-custom-sts-endpoint.example.com", - "caller_identity_header_value": "vault_server_identification_314159", + "sts_endpoint": "https://my-custom-sts-endpoint.example.com", + "iam_auth_header_value": "vault_server_identification_314159", } resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, @@ -69,8 +69,8 @@ func TestBackend_pathConfigClient(t *testing.T) { if resp == nil || resp.IsError() { t.Fatal("failed to read the client config entry") } - if resp.Data["caller_identity_header_value"] != data["vault_header_value"] { + if resp.Data["iam_auth_header_value"] != data["vault_header_value"] { t.Fatalf("expected vault_header_value: '%#v'; returned vault_header_value: '%#v'", - data["caller_identity_header_value"], resp.Data["caller_identity_header_value"]) + data["iam_auth_header_value"], resp.Data["iam_auth_header_value"]) } } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index b59bf720f0f0..28a80b24e34b 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -44,21 +44,21 @@ If a matching role is not found, login fails.`, "auth_type": { Type: framework.TypeString, - Default: "instance_identity_document", + Default: "ec2", Description: `The login type to use upon logging in. The valid choices are -instance_identity_document (default) or signed_caller_identity_request.`, +ec2 (default) or iam.`, }, "pkcs7": { Type: framework.TypeString, Description: `PKCS7 signature of the identity document when using an auth_type -of instance_identity_document.`, +of ec2.`, }, "nonce": { Type: framework.TypeString, Description: `The nonce to be used for subsequent login requests when -auth_type is instance_identity_document. If this parameter is not specified at +auth_type is ec2. If this parameter is not specified at all and if reauthentication is allowed, then the backend will generate a random nonce, attaches it to the instance's identity-whitelist entry and returns the nonce back as part of auth metadata. This value should be used with further @@ -73,26 +73,26 @@ significance.`, "request_method": { Type: framework.TypeString, Description: `HTTP method to use for the AWS request when auth_type is -signed_caller_identity_request. This must match what has been signed in the +iam. This must match what has been signed in the presigned request. Currently, POST is the only supported value`, }, "request_url": { Type: framework.TypeString, Description: `Full URL against which to make the AWS request when auth_method is -signed_caller_identity_request. If using a POST request with the action +iam. If using a POST request with the action specified in the body, this should just be "/".`, }, "request_body": { Type: framework.TypeString, - Description: `Base64-encoded request body when auth_type is signed_caller_identity_request. + Description: `Base64-encoded request body when auth_type is iam. This must match the request body included in the signature.`, }, "request_headers": { Type: framework.TypeString, Description: `Base64-encoded JSON representation of the request headers when auth_type is -signed_caller_identity_request. This must at a minimum include the headers over +iam. This must at a minimum include the headers over which AWS has included a signature.`, }, "identity": { @@ -353,12 +353,12 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id func (b *backend) pathLoginUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { auth_type := data.Get("auth_type") - if auth_type == "instance_identity_document" { - return b.pathLoginUpdateInstanceIdentityDocument(req, data) - } else if auth_type == "signed_caller_identity_request" { - return b.pathLoginUpdateCallerIdentityRequest(req, data) + if auth_type == "ec2" { + return b.pathLoginUpdateEc2(req, data) + } else if auth_type == "iam" { + return b.pathLoginUpdateIam(req, data) } else { - return logical.ErrorResponse("unrecognized auth_type, must be one of instance_identity_document or signed_caller_identity_request"), nil + return logical.ErrorResponse("unrecognized auth_type, must be one of ec2 or iam"), nil } } @@ -375,7 +375,7 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // This means we require an EC2 API call to retrieve the AMI ID, but we're // already calling the API to validate the Instance ID anyway, so it shouldn't // matter. The benefit is that we have the exact same code whether auth_type - // is instance_identity_document or signed_caller_identity_request. + // is ec2 or iam. if roleEntry.BoundAmiID != "" && *instance.ImageId != roleEntry.BoundAmiID { return nil, fmt.Sprintf("AMI ID '%s' does not belong to role '%s'", instance.ImageId, roleName), nil } @@ -453,11 +453,11 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( return roleTagResp, "", nil } -// pathLoginUpdateInstanceIdentityDocument is used to create a Vault token by the EC2 instances +// pathLoginUpdateEc2 is used to create a Vault token by the EC2 instances // by providing the pkcs7 signature of the instance identity document // and a client created nonce. Client nonce is optional if 'disallow_reauthentication' // option is enabled on the registered role. -func (b *backend) pathLoginUpdateInstanceIdentityDocument( +func (b *backend) pathLoginUpdateEc2( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { identityDocB64 := data.Get("identity").(string) var identityDocBytes []byte @@ -533,6 +533,10 @@ func (b *backend) pathLoginUpdateInstanceIdentityDocument( return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil } + if !roleAllowsAuthMethod("ec2", roleEntry) { + return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil + } + // Verify that the AccountID of the instance trying to login matches the // AccountID specified as a constraint on the role if roleEntry.BoundAccountID != "" && identityDocParsed.AccountID != roleEntry.BoundAccountID { @@ -706,7 +710,7 @@ func (b *backend) pathLoginUpdateInstanceIdentityDocument( "role_tag_max_ttl": rTagMaxTTL.String(), "role": roleName, "ami_id": identityDocParsed.AmiID, - "auth_type": "instance_identity_document", + "auth_type": "ec2", }, LeaseOptions: logical.LeaseOptions{ Renewable: true, @@ -817,19 +821,19 @@ func (b *backend) pathLoginRenew( authType, ok := req.Auth.Metadata["auth_type"] if !ok { // backwards compatibility for clients that have leases from before we added auth_type - authType = "instance_identity_document" + authType = "ec2" } - if authType == "instance_identity_document" { - return b.pathLoginRenewInstanceIdentityDocument(req, data) - } else if authType == "signed_caller_identity_request" { - return b.pathLoginRenewSignedCallerIdentityRequest(req, data) + if authType == "ec2" { + return b.pathLoginRenewEc2(req, data) + } else if authType == "iam" { + return b.pathLoginRenewIam(req, data) } else { return nil, fmt.Errorf("unrecognized auth_type: '%s'", authType) } } -func (b *backend) pathLoginRenewSignedCallerIdentityRequest( +func (b *backend) pathLoginRenewIam( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { canonicalArn := req.Auth.Metadata["canonical_arn"] if canonicalArn == "" { @@ -849,7 +853,7 @@ func (b *backend) pathLoginRenewSignedCallerIdentityRequest( } if entityType, ok := req.Auth.Metadata["inferredEntityType"]; !ok { - if entityType == "ec2Instance" { + if entityType == "ec2_instance" { instanceID, ok := req.Auth.Metadata["inferredEntityId"] if !ok { return nil, fmt.Errorf("no inferred entity ID in auth metadata") @@ -871,7 +875,7 @@ func (b *backend) pathLoginRenewSignedCallerIdentityRequest( } -func (b *backend) pathLoginRenewInstanceIdentityDocument( +func (b *backend) pathLoginRenewEc2( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { instanceID := req.Auth.Metadata["instance_id"] if instanceID == "" { @@ -951,9 +955,7 @@ func (b *backend) pathLoginRenewInstanceIdentityDocument( return framework.LeaseExtend(shortestTTL, shortestMaxTTL, b.System())(req, data) } -// identityDocument represents the items of interest from the EC2 instance -// identity document -func (b *backend) pathLoginUpdateCallerIdentityRequest( +func (b *backend) pathLoginUpdateIam( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // BEGIN boring data parsing @@ -1045,6 +1047,10 @@ func (b *backend) pathLoginUpdateCallerIdentityRequest( return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil } + if !roleAllowsAuthMethod("iam", roleEntry) { + return logical.ErrorResponse(fmt.Sprintf("auth method iam not allowed for role %s", roleName)), nil + } + // The role creation should ensure that either we're inferring this is an EC2 instance // or that we're binding an ARN if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn { @@ -1056,7 +1062,7 @@ func (b *backend) pathLoginUpdateCallerIdentityRequest( inferredEntityType := "" inferredEntityId := "" - if roleEntry.InferRoleType == "ec2Instance" { + if roleEntry.RoleInferredType == "ec2_instance" { instance, err := b.validateInstance(req.Storage, sessionName, roleEntry.InferredAWSRegion) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", sessionName, roleEntry.InferredAWSRegion)), nil @@ -1087,7 +1093,7 @@ func (b *backend) pathLoginUpdateCallerIdentityRequest( rTagMaxTTL = roleTagResp.MaxTTL } - inferredEntityType = "ec2Instance" + inferredEntityType = "ec2_instance" inferredEntityId = sessionName } @@ -1097,7 +1103,7 @@ func (b *backend) pathLoginUpdateCallerIdentityRequest( Metadata: map[string]string{ "client_arn": clientArn, "canonical_arn": canonicalArn, - "auth_type": "signed_caller_identity_request", + "auth_type": "iam", "role_tag_max_ttl": rTagMaxTTL.String(), "inferredEntityType": inferredEntityType, "inferredEntityId": inferredEntityId, @@ -1252,6 +1258,17 @@ func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, return result, err } +func roleAllowsAuthMethod(authMethod string, roleEntry *awsRoleEntry) bool { + allowedAuthMethod := false + for _, allowedAuthType := range roleEntry.AllowedAuthTypes { + if allowedAuthType == "iam" { + allowedAuthMethod = true + break + } + } + return allowedAuthMethod +} + func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) (string, error) { // NOTE: We need to ensure we're calling STS, instead of acting as an unintended network proxy // The protection against this is that this method will only call the endpoint specified in the @@ -1298,7 +1315,8 @@ type ResponseMetadata struct { RequestId string `xml:"RequestId"` } -// Struct to represent items of interest from the EC2 instance identity document. +// identityDocument represents the items of interest from the EC2 instance +// identity document type identityDocument struct { Tags map[string]interface{} `json:"tags,omitempty" structs:"tags" mapstructure:"tags"` InstanceID string `json:"instanceId,omitempty" structs:"instanceId" mapstructure:"instanceId"` diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 8be13fccb016..fde66c0fd33d 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -22,7 +22,7 @@ func pathRole(b *backend) *framework.Path { }, "allowed_auth_types": { Type: framework.TypeString, - Default: "instance_identity_document", + Default: "ec2", Description: `The comma-separated list of allowed auth_type values that are allowed to authenticate to this role.`, }, @@ -39,7 +39,7 @@ in its identity document to match the one specified by this parameter.`, "bound_iam_principal_arn": { Type: framework.TypeString, Description: `ARN of the IAM principal to bind to this role. Only applicable when -auth_type is signed_caller_identity_request.`, +auth_type is iam.`, }, "bound_iam_role_arn": { Type: framework.TypeString, @@ -49,7 +49,7 @@ The value is prefix-matched (as though it were a glob ending in '*'). The configured IAM user or EC2 instance role must be allowed to execute the 'iam:GetInstanceProfile' action if this is specified. This is only checked when auth_type is -instance_identity_document.`, +ec2.`, }, "bound_iam_instance_profile_arn": { Type: framework.TypeString, @@ -57,14 +57,14 @@ instance_identity_document.`, with an IAM instance profile ARN which has a prefix that matches the value specified by this parameter. The value is prefix-matched (as though it were a glob ending in '*'). This is only checked when -auth_type is instance_identity_document.`, +auth_type is ec2.`, }, - "infer_role_as_type": { + "role_inferred_type": { Type: framework.TypeString, Default: false, - Description: `When auth_type is signed_caller_identity_request, the + Description: `When auth_type is iam, the AWS entity type to infer from the authenticated principal. The only supported -value is ec2Instance, which will extract the EC2 instance ID from the +value is ec2_instance, which will extract the EC2 instance ID from the authenticated role and apply restrictions specific to EC2 instances (such as role_tag). The configured EC2 client must be able to find the inferred instance ID in the results, and the instance must be running. If unable to @@ -73,8 +73,8 @@ among running instances, then authentication will fail.`, }, "inferred_aws_region": { Type: framework.TypeString, - Description: `When auth_type is signed_caller_identity_request and -infer_role_as_type is set, the region to assume the inferred entity exists in.`, + Description: `When auth_type is iam and +role_inferred_type is set, the region to assume the inferred entity exists in.`, }, "role_tag": { Type: framework.TypeString, @@ -83,8 +83,8 @@ infer_role_as_type is set, the region to assume the inferred entity exists in.`, field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using 'role//tag' endpoint. Defaults to an empty string, meaning that role tags are disabled. This -is only checked if auth_type is instance_identity_document or -infer_role_as_ec2_instance is true`, +is only checked if auth_type is ec2 or +role_inferred_type is ec2_instance`, }, "ttl": { Type: framework.TypeDurationSecond, @@ -110,7 +110,7 @@ resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution. This is only checked when -auth_type is instance_identity_document.`, +auth_type is ec2.`, }, "disallow_reauthentication": { Type: framework.TypeBool, @@ -358,8 +358,8 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.BoundIamPrincipalARN = boundIamPrincipalARNRaw.(string) } - if inferRoleTypeRaw, ok := data.GetOk("infer_role_as_type"); ok { - roleEntry.InferRoleType = inferRoleTypeRaw.(string) + if inferRoleTypeRaw, ok := data.GetOk("role_inferred_type"); ok { + roleEntry.RoleInferredType = inferRoleTypeRaw.(string) } if inferredAWSRegionRaw, ok := data.GetOk("inferred_aws_region"); ok { @@ -371,10 +371,10 @@ func (b *backend) pathRoleCreateUpdate( parseAllowedAuthTypes := func(input string) string { allowedAuthTypes := []string{} for _, t := range strings.Split(input, ",") { - if t == "instance_identity_document" { + if t == "ec2" { allowInstanceIdentityDocument = true allowedAuthTypes = append(allowedAuthTypes, t) - } else if t == "signed_caller_identity_request" { + } else if t == "iam" { allowCallerIdentity = true allowedAuthTypes = append(allowedAuthTypes, t) } else if t != "" { @@ -406,35 +406,35 @@ func (b *backend) pathRoleCreateUpdate( case roleEntry.BoundIamRoleARN != "": default: - return logical.ErrorResponse("at least be one bound parameter should be specified on the role with auth_type instance_identity_document or auth_type signed_caller_identity_request"), nil + return logical.ErrorResponse("at least be one bound parameter should be specified on the role with auth_type ec2 or auth_type iam"), nil } } if allowCallerIdentity { - if roleEntry.InferRoleType != "" { - if roleEntry.InferRoleType != "ec2Instance" { - return logical.ErrorResponse(fmt.Sprintf("invalid infer_role_as_type value: '%s'", roleEntry.InferRoleType)), nil + if roleEntry.RoleInferredType != "" { + if roleEntry.RoleInferredType != "ec2_instance" { + return logical.ErrorResponse(fmt.Sprintf("invalid role_inferred_type value: '%s'", roleEntry.RoleInferredType)), nil } if roleEntry.InferredAWSRegion == "" { - return logical.ErrorResponse("must set inferred_aws_region when setting infer_role_as_type"), nil + return logical.ErrorResponse("must set inferred_aws_region when setting role_inferred_type"), nil } } else { if roleEntry.BoundIamPrincipalARN == "" { - return logical.ErrorResponse("must set bound_iam_principal_arn if not setting infer_role_as_type"), nil + return logical.ErrorResponse("must set bound_iam_principal_arn if not setting role_inferred_type"), nil } if roleEntry.InferredAWSRegion != "" { - return logical.ErrorResponse("must not set inferred_aws_region without infer_role_as_type"), nil + return logical.ErrorResponse("must not set inferred_aws_region without role_inferred_type"), nil } } } else { - if roleEntry.InferRoleType != "" { - return logical.ErrorResponse("must only set infer_role_as_type when allowing signed_caller_identity_request auth_type"), nil + if roleEntry.RoleInferredType != "" { + return logical.ErrorResponse("must only set role_inferred_type when allowing iam auth_type"), nil } if roleEntry.BoundIamPrincipalARN != "" { - return logical.ErrorResponse("must only set bound_iam_principal_arn when allowing signed_caller_identity_request auth_type"), nil + return logical.ErrorResponse("must only set bound_iam_principal_arn when allowing iam auth_type"), nil } if roleEntry.InferredAWSRegion != "" { - return logical.ErrorResponse("must not set inferred_aws_region without allowing signed_caller_identity_request auth_type"), nil + return logical.ErrorResponse("must not set inferred_aws_region without allowing iam auth_type"), nil } } @@ -532,7 +532,7 @@ type awsRoleEntry struct { BoundIamPrincipalARN string `json:"bound_iam_principal_arn" structs:"bound_iam_principal_arn" mapstructure:"bound_iam_principal_arn"` BoundIamRoleARN string `json:"bound_iam_role_arn" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"` BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"` - InferRoleType string `json:"infer_role_type" structs:"infer_role_type" mapstructure:"infer_role_type"` + RoleInferredType string `json:"infer_role_type" structs:"infer_role_type" mapstructure:"infer_role_type"` InferredAWSRegion string `json:"inferred_aws_region" structs:"inferred_aws_region" mapstructure:"inferred_aws_region"` RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index 721413d7688a..0b64b1c6564a 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -166,7 +166,7 @@ func TestBackend_pathRoleSignedCallerIdentity(t *testing.T) { } data := map[string]interface{}{ - "allowed_auth_types": "signed_caller_identity_request", + "allowed_auth_types": "iam", "policies": "p,q,r,s", "max_ttl": "2h", "bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName", @@ -200,7 +200,7 @@ func TestBackend_pathRoleSignedCallerIdentity(t *testing.T) { t.Fatalf("bad: policies: expected %#v\ngot: %#v\n", data, resp.Data) } - data["infer_role_as_type"] = "invalid" + data["role_inferred_type"] = "invalid" resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, Path: "role/ShouldNeverExist", @@ -208,13 +208,13 @@ func TestBackend_pathRoleSignedCallerIdentity(t *testing.T) { Storage: storage, }) if resp == nil || !resp.IsError() { - t.Fatalf("Created role with invalid infer_role_as_type") + t.Fatalf("Created role with invalid role_inferred_type") } if err != nil { t.Fatal(err) } - data["infer_role_as_type"] = "ec2Instance" + data["role_inferred_type"] = "ec2_instance" resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, Path: "role/ShouldNeverExist", @@ -313,7 +313,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { data := map[string]interface{}{ "policies": "p,q,r,s", "bound_ami_id": "ami-abc1234", - "allowed_auth_types": "instance_identity_document,invalid", + "allowed_auth_types": "ec2,invalid", } submitCreateRequest := func(roleName string) (*logical.Response, error) { @@ -333,7 +333,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { t.Fatal(err) } - data["allowed_auth_types"] = "instance_identity_document,,signed_caller_identity_request" + data["allowed_auth_types"] = "ec2,,iam" resp, err = submitCreateRequest("shouldNeverExist") if resp != nil && !resp.IsError() { t.Fatalf("created role without required bound_iam_principal_arn") @@ -346,7 +346,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { delete(data, "bound_ami_id") resp, err = submitCreateRequest("shouldNeverExist") if resp != nil && !resp.IsError() { - t.Fatalf("created role without required instance_identity_document binding") + t.Fatalf("created role without required ec2 binding") } if err != nil { t.Fatal(err) @@ -362,7 +362,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { } delete(data, "bound_iam_principal_arn") - data["infer_role_as_type"] = "ec2Instance" + data["role_inferred_type"] = "ec2_instance" data["inferred_aws_region"] = "us-east-1" resp, err = submitCreateRequest("multipleTypesInferred") if err != nil { From 53f11aa556e08ceefb214d05e87727fbc3274d32 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Wed, 25 Jan 2017 01:36:55 -0500 Subject: [PATCH 05/20] Update docs for the aws auth backend --- website/source/docs/auth/aws-ec2.html.md | 637 +++++++++++++++++++---- website/source/layouts/docs.erb | 4 +- 2 files changed, 543 insertions(+), 98 deletions(-) diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 11f16ad3cee4..b67e7f99de5d 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -1,23 +1,37 @@ --- layout: "docs" -page_title: "Auth Backend: AWS-EC2" -sidebar_current: "docs-auth-aws-ec2" +page_title: "Auth Backend: AWS" +sidebar_current: "docs-auth-aws" description: |- - The aws-ec2 backend allows automated authentication of AWS EC2 instances. + The aws backend allows automated authentication of AWS entities. --- -# Auth Backend: aws-ec2 +# Auth Backend: aws -The aws-ec2 auth backend provides a secure introduction mechanism for AWS EC2 -instances, allowing automated retrieval of a Vault token. Unlike most Vault -authentication backends, this backend does not require first-deploying, or +The aws auth backend provides an automated mechanism to retrieve +a Vault token for AWS EC2 instances and IAM principals. Unlike most Vault +authentication backends, this backend does not require manual first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client -certificates, etc). Instead, it treats AWS as a Trusted Third Party and uses +certificates, etc), by operators under many circumstances. It treats +AWS as a Trusted Third Party and uses either the cryptographically signed dynamic metadata information that uniquely -represents each EC2 instance. +represents each EC2 instance or a special AWS request signed with AWS IAM +credentials. The metadata information is automatically supplied by AWS to all +EC2 instances, and IAM credentials are automatically supplied to AWS instances +in IAM instance profiles, Lambda functions, and others, and it is this +information already provided by AWS which Vault can use to authenticate +clients. ## Authentication Workflow +There are two authentication types present in the aws backend: ec2 (which was +formerly the only type supplied in the aws-ec2 auth backend) and iam. Each has a +different authentication workflow, and each can solve different use cases. See +the section on comparing the two auth methods below to help determine which +method is more appropriate for your use cases. + +### EC2 Authentication Method + EC2 instances have access to metadata describing the instance. (For those not familiar with instance metadata, details can be found [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html).) @@ -38,6 +52,51 @@ verifies the current running status of the instance via the EC2 API. There are various modifications to this workflow that provide more or less security, as detailed later in this documentation. +### IAM Authentication Method + +The AWS STS API includes a method, +[`sts:GetCallerIdentity`](http://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html), +which allows you to validate the identity of a client. The client signs +a `GetCallerIdentity` query using the [AWS Signature v4 +algorithm](http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) and +submits 4 pieces of information to the Vault server to recreate a valid signed +request: the request URL, the request body, the request headers, and the request +method, as the AWS signature is computed over those fields. The Vault server +then reconstructs the query and forwards it on to the AWS STS service and +validates the result back. Clients don't need network-level access to talk to +the AWS STS API endpoint; they merely need access to the credentials to sign the +request. However, it means that the Vault server does need network-level access +to send requests to the STS endpoint. + +Importantly, the credentials used to sign the GetCallerIdentity request can come +from the EC2 instance metadata service for an EC2 instance, or from the AWS +environment variables in an AWS Lambda function execution, which obviates the +need for an operator to manually provision some sort of identity material first. +However, the credentials can, in principle, come from anywhere, not just from +the locations AWS hasprovided for you. + +Each signed AWS request includes the current timestamp to mitigate the risk of +replay attacks. In addition, Vault allows you to require an additional header, +`X-Vault-AWSIAM-Server-ID`, to be present to mitigate against different types of replay +attacks (such as a signed `GetCallerIdentity` request stolen from a dev Vault +instance and used to authenticate to a prod Vault instance). Vault further +requires that this header be one of the headers included in the AWS signature +and relies upon AWS to authenticate that signature. + +While AWS API endpoints support both signed GET and POST requests, for +simplicity, the aws-iam backend supports only POST requests. It also does not +support `presigned` requests, i.e., requests with `X-Amz-Credential`, +`X-Amz-signature`, and `X-Amz-SignedHeaders` GET query parameter containing the +authenticating information. + +It's also important to note that Amazon does NOT appear to include any sort +of authorization around calls to `GetCallerIdentity`. For example, if you have +an IAM policy on your credential that requires all access to be MFA authenticated, +non-MFA authenticated credentials (i.e., raw credentials, not those retrieved +by calling `GetSessionToken` and supplying an MFA code) will still be able to +authenticate to Vault using this backend. It does not appear possible to enforce +an IAM principal to be MFA authenticated while authenticating to Vault. + ## Authorization Workflow The basic mechanism of operation is per-role. Roles are registered in the @@ -45,9 +104,16 @@ backend and associated with various optional restrictions, such as the set of allowed policies and max TTLs on the generated tokens. Each role can be specified with the constraints that are to be met during the login. For example, one such constraint that is supported is to bind against AMI ID. A -role which is bound to a specific AMI, can only be used for login by those +role which is bound to a specific AMI, can only be used for login by EC2 instances that are deployed on the same AMI. +In general, role bindings that are specific to an EC2 instance are only checked +when the ec2 auth method is used to login, while bindings specific to IAM +principals are only checked when the iam auth method is used to login. However, +the iam method includes the ability for you to "infer" an EC2 instance ID from +the authenticated client and apply many of the bindings that would otherwise +only apply specifically to EC2 instances. + In many cases, an organization will use a "seed AMI" that is specialized after bootup by configuration management or similar processes. For this reason, a role entry in the backend can also be associated with a "role tag". These tags @@ -64,8 +130,146 @@ the backend to verify the authenticity of a found role tag and ensure that it ha not been tampered with. There is also a mechanism to blacklist role tags if one has been found to be distributed outside of its intended set of machines. +## IAM Authentication Inferences + +With the iam auth method, normally Vault will see the IAM principal that +authenticated, either the IAM user or role. However, when you have an EC2 +instance in an IAM instance profile, Vault can actually see the instance ID of +the instance and can "infer" that it's an EC2 instance. However, there are +important security caveats to be aware of before configuring Vault to make that +inference. + +Each AWS IAM role has a "trust policy" which specifies which entities are +trusted to call +[`sts:AssumeRole`](http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) +on the role and retrieve credentials that can be used to authenticate with that +role. When AssumeRole is called, a parameter called RoleSessionName is passed +in, which is chosen arbitrarily by the entity which calls AssumeRole. If you +have a role with an ARN `arn:aws:iam::123456789012:role/MyRole`, then the +credentials returned by calling AssumeRole on that role will be +`arn:aws:sts::123456789012:assumed-role/MyRole/RoleSessionName` where +RoleSessionName is the session name in the AssumeRole API call. It is this +latter value which Vault actually sees. + +When you have an EC2 instance in an instance profile, the corresponding role's +trust policy specifies that the principal `"Service": "ec2.amazonaws.com"` is +trusted to call AssumeRole. When this is configured, EC2 calls AssumeRole on +behalf of your instance, with a RoleSessionName corresponding to the +instance's instance ID. Thus, it is possible for Vault to extract the instance +ID out of the value it sees when an EC2 instance in an instance profile +authenticates to Vault with the iam authentication method. This is known as +"inferencing." Vault can be configured, on a role-by-role basis, to infer that a +caller is an EC2 instance and, if so, apply further bindings that apply +specifically to EC2 instances -- most of the bindings available to the ec2 +authentication backend. + +However, it is very important to note that if any entity other than an AWS +service is permitted to call AssumeRole on your role, then that entity can +simply pass in your instance's instance ID and spoof your instance to Vault. +This also means that anybody who is able to modify your role's trust policy +(e.g., via +[`iam:UpdateAssumeRolePolicy`](http://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAssumeRolePolicy.html), +then that person could also spoof your instances. If this is a concern but you +would like to take advantage of inferencing, then you should tightly restrict +who is able to call AssumeRole on the role, tightly restrict who is able to call +UpdateAssumeRolePolicy on the role, and monitor CloudTrail logs for calls to +AssumeRole and UpdateAssumeRolePolicy. All of these caveats apply equally to +using the iam authentication method without inferencing; the point is merely +that Vault cannot offer an iron-clad guarantee about the inference and it is up +to operators to determine, based on their own AWS controls and use cases, +whether or not it's appropriate to configure inferencing. + +## Mixing Authentication Types + +Vault allows you to configure whether to allow the ec2 auth method, the aws auth +method, or both auth methods for a given role. If you do this, it is important +to understand that _only those bindings applicable to the client's chosen auth +type will be enforced by Vault_. Some examples: + +1. You configure a role only allowing the ec2 auth type, with a bound AMI ID. A + client would not be able to login using the iam auth type. +1. You configure a role only allowing the iam auth type, with a bound IAM + principal ARN. A client would not be able to login with the ec2 auth method. +1. You configure a role only allowing the iam auth type and further configure + inferencing. You have a bound AMI ID and a bound IAM principal ARN. A client + must login using the iam method; the RoleSessionName must be a valid instance + ID viewable by Vault, and the instance must have come from the bound AMI ID. +1. You configure a role to allow both iam and ec2 auth types, but you have not + configured inferencing. You configure both a bound AMI ID and a bound IAM + principal ARN. If a client chooses to login with the ec2 auth method, only the + bound AMI is checked; the bound IAM principal ARN is ignored. Similarly, if a + client logs in with the iam auth method, then only the bound IAM principal ARN + is checked; the bound AMI ID is ignored. +1. You configure a role to allow both iam and ec2 auth types, and you have + further configured inferencing, with a bound IAM principal ARN and a bound AMI + ID. If a client logs in with the ec2 auth method, then only the bound AMI ID + is checked. If a client logs in with the iam auth method, then the same + checks are performed as in example 3. + + +## Comparison of the EC2 and IAM Methods + +The iam and ec2 authentication methods serve similar and somewhat overlapping +functionality, in that both authenticate some type of AWS entity to Vault. To +help you determine which method is more appropriate for your use case, here is a +comparison of the two authentication methods. + +* What type of entity is authenticated: + * The ec2 auth method authenticates only AWS EC2 instances and is specialized + to handle EC2 instances, such as restricting access to EC2 instances from + a particular AMI, EC2 instances in a particular instance profile, or EC2 + instances with a specialized tag value (via the role_tag feature). + * The iam auth method authenticates generic AWS IAM principals. This can + include IAM users, IAM roles assumed from other accounts, AWS Lambdas that + are launched in an IAM role, or even EC2 instances that are launched in an + IAM instance profile. However, because it authenticates more generalized IAM + principals, this backend doesn't offer more granular controls beyond binding + to a given IAM principal without the use of inferencing. +* How the entities are authenticated + * The ec2 auth method authenticates instances by making use of the EC2 + instance identity document, which is a cryptographically signed document + containing metadata about the instance. This document changes relatively + infrequently, so Vault adds a number of other constructs to mitigate against + replay attacks, such as client nonces, role tags, instance migrations, etc. + Because the instance identity document is signed by AWS, you have a strong + guarantee that it came from an EC2 instance. + * The iam auth method authenticates by having clients provide a specially + signed AWS API request which the backend then passes on to AWS to validate + the signature and tell Vault who created it. The actual secret (i.e., + the AWS secret access key) is never transmitted over the wire, and the + AWS signature algorithm automatically expires requests after 15 minutes, + providing simple and robust protection against replay attacks. The use of + inferencing, however, provides a weaker guarantee that the credentials came + from an EC2 instance in an IAM instance profile compared to the ec2 + authentication mechanism. + * The instance identity document used in the ec2 auth method is more likely to + be stolen given its relatively static nature, but it's harder to spoof. On + the other hand, the credentials of an EC2 instance in an IAM instance + profile are less likely to be stolen given their dynamic and short-lived + nature, but it's easier to spoof credentials that might have come from an + EC2 instance. +* Specific use cases + * If you have a long-lived EC2 instance which you are unable to relaunch into + an IAM instance profile, then the ec2 auth method is probably the best + solution for you. (While you could store long-lived AWS IAM user credentials + on disk and use those to authenticate to Vault, that would not be + recommended.) + * If you have non-EC2 instance entities, such as IAM users, Lambdas in IAM + roles, or developer laptops using [AdRoll's + Hologram](https://github.com/AdRoll/hologram) then you would need to use the + iam auth method. + * If you have EC2 instances which are already in an IAM instance profile, then + you could use either auth method. If you need more granular filtering beyond just + the instance profile of given EC2 instances (such as filtering based off + the AMI the instance was launched from), then you would need to + use the ec2 auth method, launch your EC2 instances into unique instance + profiles for each different Vault role you would want them to authenticate + to, or make use of inferencing. + ## Client Nonce +Note: this only applies to the ec2 authentication method. + If an unintended party gains access to the PKCS#7 signature of the identity document (which by default is available to every process and user that gains access to an EC2 instance), it can impersonate that instance and fetch a Vault @@ -79,11 +283,11 @@ investigation. During the first login, the backend stores the instance ID that authenticated in a `whitelist`. One method of operation of the backend is to disallow any authentication attempt for an instance ID contained in the whitelist, using the -'disallow_reauthentication' option on the role, meaning that an instance is +`disallow_reauthentication` option on the role, meaning that an instance is allowed to login only once. However, this has consequences for token rotation, as it means that once a token has expired, subsequent authentication attempts would fail. By default, reauthentication is enabled in this backend, and can be -turned off using 'disallow_reauthentication' parameter on the registered role. +turned off using `disallow_reauthentication` parameter on the registered role. In the default method of operation, the backend will return a unique nonce during the first authentication attempt, as part of auth `metadata`. Clients @@ -122,6 +326,9 @@ access. ### Dynamic Management of Policies Via Role Tags +Note: This only applies to the ec2 auth method or the iam auth method when +inferencing is used. + If the instance is required to have customized set of policies based on the role it plays, the `role_tag` option can be used to provide a tag to set on instances, for a given role. When this option is set, during login, along with @@ -131,7 +338,7 @@ instance. The tag holds information that represents a *subset* of privileges tha are set on the role and are used to further restrict the set of the role's privileges for that particular instance. -A `role_tag` can be created using `auth/aws-ec2/role//tag` endpoint +A `role_tag` can be created using `auth/aws/role//tag` endpoint and is immutable. The information present in the tag is SHA256 hashed and HMAC protected. The per-role key to HMAC is only maintained in the backend. This prevents an adversarial operator from modifying the tag when setting it on the EC2 instance @@ -152,11 +359,13 @@ other resources provided by or resident in Vault. ### Handling Lost Client Nonces +Note: This only applies to the ec2 auth method. + If an EC2 instance loses its client nonce (due to a reboot, a stop/start of the client, etc.), subsequent login attempts will not succeed. If the client nonce is lost, normally the only option is to delete the entry corresponding to the instance ID from the identity `whitelist` in the backend. This can be done via -the `auth/aws-ec2/identity-whitelist/` endpoint. This allows a new +the `auth/aws/identity-whitelist/` endpoint. This allows a new client nonce to be accepted by the backend during the next login request. Under certain circumstances there is another useful setting. When the instance @@ -189,6 +398,8 @@ role tag has no effect. ### Disabling Reauthentication +Note: this only applies to the ec2 authentication method. + If in a given organization's architecture, a client fetches a long-lived Vault token and has no need to rotate the token, all future logins for that instance ID can be disabled. If the option `disallow_reauthentication` is set, only one @@ -210,13 +421,16 @@ role tag has no effect. ### Blacklisting Role Tags +Note: this only applies to the ec2 authentication method or the iam auth method +when inferencing is used. + Role tags are tied to a specific role, but the backend has no control over, which instances using that role, should have any particular role tag; that is purely up to the operator. Although role tags are only restrictive (a tag cannot escalate privileges above what is set on its role), if a role tag is found to have been used incorrectly, and the administrator wants to ensure that the role tag has no further effect, the role tag can be placed on a `blacklist` via the endpoint -`auth/aws-ec2/roletag-blacklist/`. Note that this will not invalidate the +`auth/aws/roletag-blacklist/`. Note that this will not invalidate the tokens that were already issued; this only blocks any further login requests from those instances that have the blacklisted tag attached to them. @@ -245,6 +459,8 @@ endpoints. ### Varying Public Certificates +Note: this only applies to the ec2 authentication method. + The AWS public certificate, which contains the public key used to verify the PKCS#7 signature, varies for different AWS regions. The primary AWS public certificate, which covers most AWS regions, is already included in Vault and @@ -252,11 +468,11 @@ does not need to be added. Instances whose PKCS#7 signatures cannot be verified by the default public certificate included in Vault can register a different public certificate which can be found [here] (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html), -via the `auth/aws-ec2/config/certificate/` endpoint. +via the `auth/aws/config/certificate/` endpoint. ### Dangling Tokens -An EC2 instance, after authenticating itself with the backend gets a Vault token. +An EC2 instance, after authenticating itself with the backend, gets a Vault token. After that, if the instance terminates or goes down for any reason, the backend will not be aware of such events. The token issued will still be valid, until it expires. The token will likely be expired sooner than its lifetime when the @@ -269,7 +485,7 @@ instance fails to renew the token on time. #### Enable AWS EC2 authentication in Vault. ``` -$ vault auth-enable aws-ec2 +$ vault auth-enable aws ``` #### Configure the credentials required to make AWS API calls @@ -283,50 +499,139 @@ The IAM account or role to which the credentials map must allow the `bound_iam_role_arn` below), `iam:GetInstanceProfile` must also be allowed. ``` -$ vault write auth/aws-ec2/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA +$ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA ``` #### Configure the policies on the role. ``` -$ vault write auth/aws-ec2/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h +$ vault write auth/aws/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h + +$ vault write auth/aws/role/dev-role-iam allowed_auth_methods=iam \ + bound_iam_principal_arn=arn:aws:iam::123456789012:role/MyRole policies=prod,dev max_ttl=500h ``` +#### Configure a required X-Vault-AWSIAM-Server-ID Header (recommended) + +``` +$ vault write auth/aws/client/config iam_auth_header_vaule=vault.example.xom +``` + + #### Perform the login operation ``` -$ vault write auth/aws-ec2/login role=dev-role \ +$ vault write auth/aws/login role=dev-role \ pkcs7=MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA nonce=5defbf9e-a8f9-3063-bdfc-54b7a42a1f95 ``` +For the iam auth method, generating the signed request is a non-standard +operation. The Vault cli supports generating this for you: + +``` +$ vault auth -method=aws header_value=vault.example.com role=dev-role-iam +``` + +This assumes you have AWS credentials configured in the standard locations AWS +SDKs search for credentials (environment variables, ~/.aws/credentials, IAM +instance profile in that order). If you do not have IAM credentials available at +any of these locations, you can explicitly pass them in on the command line +(though this is not recommended), omitting `aws_security_token` if not +applicable . + +``` +$ vault auth -method=aws header_value=vault.example.com role=dev-role-iam \ + aws_access_key_id= \ + aws_secret_access_key= \ + aws_security_token= +``` + +For reference, the following Go program also demonstrates how to generate the +required parameters (assuming you are using a default AWS credential provider), +filling in the value for the header value as appropriate: + +``` +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" +) + +func main() { + sess, err := session.NewSession() + if err != nil { + fmt.Println("failed to create session,", err) + return + } + + svc := sts.New(sess) + var params *sts.GetCallerIdentityInput + stsRequest, _ := svc.GetCallerIdentityRequest(params) + stsRequest.HTTPRequest.Header.Add("X-Vault-AWSIAM-Server-ID", "vault.example.com") + stsRequest.Sign() + + headersJson, err := json.Marshal(stsRequest.HTTPRequest.Header) + if err != nil { + fmt.Println(fmt.Errorf("Error:", err)) + return + } + requestBody, err := ioutil.ReadAll(stsRequest.HTTPRequest.Body) + if err != nil { + fmt.Println(fmt.Errorf("Error:", err)) + return + } + fmt.Println("request_method=" + stsRequest.HTTPRequest.Method) + fmt.Println("request_url=" + stsRequest.HTTPRequest.URL.String()) + fmt.Println("request_headers=" + base64.StdEncoding.EncodeToString(headersJson)) + fmt.Println("request_body=" + base64.StdEncoding.EncodeToString(requestBody)) +} + +``` +Using this, we can get the values to pass in to the `vault write` operation: + +``` +$ vault write auth/aws/login role=dev-role-iam \ + request_method=POST \ + request_url=https://sts.amazonaws.com/ \ + request_body=QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== \ + request_headers=eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ== +``` ### Via the API -#### Enable AWS EC2 authentication in Vault. +#### Enable AWS authentication in Vault. ``` -curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/sys/auth/aws" -d '{"type":"aws-ec2"}' +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/sys/auth/aws" -d '{"type":"aws"}' ``` #### Configure the credentials required to make AWS API calls. ``` -curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws-ec2/config/client" -d '{"access_key":"VKIAJBRHKH6EVTTNXDHA", "secret_key":"vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj"}' +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/config/client" -d '{"access_key":"VKIAJBRHKH6EVTTNXDHA", "secret_key":"vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj"}' ``` #### Configure the policies on the role. ``` -curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws-ec2/role/dev-role -d '{"bound_ami_id":"ami-fce3c696","policies":"prod,dev","max_ttl":"500h"}' +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role -d '{"bound_ami_id":"ami-fce3c696","policies":"prod,dev","max_ttl":"500h"}' + +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role-iam -d '{"allowed_auth_methods":"iam","policies":"prod,dev","max_ttl":"500h","bound_iam_principal_arn":"arn:aws:iam::123456789012:role/MyRole"}' ``` #### Perform the login operation ``` -curl -X POST "http://127.0.0.1:8200/v1/auth/aws-ec2/login" -d -'{"role":"dev-role","pkcs7":"'$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')'","nonce":"5defbf9e-a8f9-3063-bdfc-54b7a42a1f95"}' -``` +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev-role","pkcs7":"'$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')'","nonce":"5defbf9e-a8f9-3063-bdfc-54b7a42a1f95"}' +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev", "request_method": "POST", "request_url": "https://sts.amazonaws.com/", "request_body": "QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==", "request_headers": "eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ==" }' +``` The response will be in JSON. For example: @@ -341,7 +646,8 @@ The response will be in JSON. For example: "region": "us-east-1", "nonce": "5defbf9e-a8f9-3063-bdfc-54b7a42a1f95", "instance_id": "i-a832f734", - "ami_id": "ami-f083709d" + "ami_id": "ami-f083709d", + "auth_type": "ec2" }, "policies": [ "default", @@ -362,19 +668,19 @@ The response will be in JSON. For example: ``` ## API -### /auth/aws-ec2/config/client +### /auth/aws/config/client #### POST
Description
- Configures the credentials required to perform API calls to AWS. - The instance identity document fetched from the PKCS#7 signature - will provide the EC2 instance ID. The credentials configured using - this endpoint will be used to query the status of the instances via - DescribeInstances API. If static credentials are not provided using - this endpoint, then the credentials will be retrieved from the - environment variables `AWS_ACCESS_KEY`, `AWS_SECRET_KEY` and `AWS_REGION` - respectively. If the credentials are still not found and if the + Configures the credentials required to perform API calls to AWS as well as + custom endpoints to talk to AWS APIs. The instance identity document + fetched from the PKCS#7 signature will provide the EC2 instance ID. The + credentials configured using this endpoint will be used to query the status + of the instances via DescribeInstances API. If static credentials are not + provided using this endpoint, then the credentials will be retrieved from + the environment variables `AWS_ACCESS_KEY`, `AWS_SECRET_KEY` and + `AWS_REGION` respectively. If the credentials are still not found and if the backend is configured on an EC2 instance with metadata querying capabilities, the credentials are fetched automatically.
@@ -383,7 +689,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/config/client`
+
`/auth/aws/config/client`
Parameters
@@ -408,6 +714,35 @@ The response will be in JSON. For example: URL to override the default generated endpoint for making AWS EC2 API calls. +
    +
  • + iam_endpoint + optional + URL to override the default generated endpoint for making AWS IAM API calls. +
  • +
+
    +
  • + sts_endpoint + optional + URL to override the default generated endpoint for making AWS STS API calls. +
  • +
+
    +
  • + iam_auth_header_value + optional + The value to require in the `X-Vault-AWSIAM-Server-ID` header as part of + GetCallerIdentity requests that are used in the iam auth method. If not + set, then no value is required or validated. If set, clients must + include an X-Vault-AWSIAM-Server-ID header in the headers of login + requests, and further this header must be among the signed headers + validated by AWS. This is to protect against different types of replay + attacks, for example a signed request sent to a dev server being resent + to a production server. Consider setting this to the Vault server's DNS + name. +
  • +
Returns
@@ -427,7 +762,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/client`
+
`/auth/aws/config/client`
Parameters
@@ -445,6 +780,9 @@ The response will be in JSON. For example: "secret_key": "vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj", "access_key": "VKIAJBRHKH6EVTTNXDHA" "endpoint" "", + "iam_endpoint" "", + "sts_endpoint" "", + "iam_auth_header_value" "", }, "lease_duration": 0, "renewable": false, @@ -467,7 +805,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws-ec2/config/client`
+
`/auth/aws/config/client`
Parameters
@@ -480,7 +818,7 @@ The response will be in JSON. For example:
-### /auth/aws-ec2/config/certificate/ +### /auth/aws/config/certificate/ #### POST
Description
@@ -496,7 +834,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/config/certificate/`
+
`/auth/aws/config/certificate/`
Parameters
@@ -544,7 +882,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/certificate/`
+
`/auth/aws/config/certificate/`
Parameters
@@ -581,7 +919,7 @@ The response will be in JSON. For example:
LIST/GET
URL
-
`/auth/aws-ec2/config/certificates` (LIST) or `/auth/aws-ec2/config/certificates?list=true` (GET)
+
`/auth/aws/config/certificates` (LIST) or `/auth/aws/config/certificates?list=true` (GET)
Parameters
@@ -609,7 +947,7 @@ The response will be in JSON. For example:
-### /auth/aws-ec2/config/tidy/identity-whitelist +### /auth/aws/config/tidy/identity-whitelist ##### POST
Description
@@ -621,7 +959,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/config/tidy/identity-whitelist`
+
`/auth/aws/config/tidy/identity-whitelist`
Parameters
@@ -660,7 +998,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/tidy/identity-whitelist`
+
`/auth/aws/config/tidy/identity-whitelist`
Parameters
@@ -698,7 +1036,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws-ec2/config/tidy/identity-whitelist`
+
`/auth/aws/config/tidy/identity-whitelist`
Parameters
@@ -712,7 +1050,7 @@ The response will be in JSON. For example: -### /auth/aws-ec2/config/tidy/roletag-blacklist +### /auth/aws/config/tidy/roletag-blacklist ##### POST
Description
@@ -724,7 +1062,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/config/tidy/roletag-blacklist`
+
`/auth/aws/config/tidy/roletag-blacklist`
Parameters
@@ -762,7 +1100,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/tidy/roletag-blacklist`
+
`/auth/aws/config/tidy/roletag-blacklist`
Parameters
@@ -800,7 +1138,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws-ec2/config/tidy/roletag-blacklist`
+
`/auth/aws/config/tidy/roletag-blacklist`
Parameters
@@ -814,23 +1152,23 @@ The response will be in JSON. For example: -### /auth/aws-ec2/role/[role] +### /auth/aws/role/[role] #### POST
Description
- Registers a role in the backend. Only those instances which are using -the role registered using this endpoint, will be able to perform the login -operation. Contraints can be specified on the role, that are applied on the -instances attempting to login. At least one constraint should be specified -on the role. + Registers a role in the backend. Only those instances or principals which + are using the role registered using this endpoint, will be able to perform + the login operation. Contraints can be specified on the role, that are + applied on the instances or principals attempting to login. At least one + constraint should be specified on the role.
Method
POST
URL
-
`/auth/aws-ec2/role/`
+
`/auth/aws/role/`
Parameters
@@ -841,12 +1179,24 @@ on the role. Name of the role. +
    +
  • + allowed_auth_types + optional + The auth types permitted for this role, separated by commas. Valid + choices are "ec2" or "iam". If no value is chosen, then it will default + to "ec2" for backwards compatibility. Only those bindings applicable to + the auth type chosen by clients will be checked by Vault upon login. +
  • +
  • bound_ami_id optional If set, defines a constraint on the EC2 instances that they -should be using the AMI ID specified by this parameter. +should be using the AMI ID specified by this parameter. This constraint is +checked during ec2 auth as well as the iam auth method only when inferring an EC2 +instance.
    @@ -854,7 +1204,8 @@ should be using the AMI ID specified by this parameter. bound_account_id optional If set, defines a constraint on the EC2 instances that the account ID -in its identity document to match the one specified by this parameter. +in its identity document to match the one specified by this parameter. This +constraint is checked only by the ec2 auth method.
    @@ -865,7 +1216,9 @@ in its identity document to match the one specified by this parameter. must match the IAM role ARN specified by this parameter. The value is prefix-matched (as though it were a glob ending in `*`). The configured IAM user or EC2 instance role must be allowed to execute the -`iam:GetInstanceProfile` action if this is specified. +`iam:GetInstanceProfile` action if this is specified. This constraint is checked +by the ec2 auth method as well as the iam auth method only when inferring an EC2 +instance.
    @@ -875,7 +1228,8 @@ IAM user or EC2 instance role must be allowed to execute the If set, defines a constraint on the EC2 instances to be associated with an IAM instance profile ARN which has a prefix that matches the value specified by this parameter. The value is prefix-matched (as though it were a glob ending -in `*`). +in `*`). This constraint is checked by the ec2 auth method as well as the iam +auth method only when inferring an ec2 instance.
    @@ -885,7 +1239,39 @@ in `*`). If set, enables the role tags for this role. The value set for this field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using 'role//tag' endpoint. - Defaults to an empty string, meaning that role tags are disabled. + Defaults to an empty string, meaning that role tags are disabled. This + constraint is checked by the ec2 auth method as well as the iam auth + method only when inferring an EC2 instance. + +
+
    +
  • + bound_iam_principal_arn + optional + Defines the IAM principal that must be authenticated using the iam + auth method. It should look like + "arn:aws:iam::123456789012:user/MyUserName" or + "arn:aws:iam::123456789012:role/MyRoleName". This constraint is only + checked by the iam auth method. +
  • +
+
    +
  • + role_inferred_type + optional + When set, instructs Vault to turn on inferencing. The only current valid + value is "ec2_instance" instructing Vault to infer that the role comes + from an EC2 instance in an IAM instance profile. This only applies to + the iam auth method. +
  • +
+
    +
  • + inferred_aws_region + optional + When role inferencing is activated, the region to search for the + inferred entities (e.g., EC2 instances). Required if role inferencing is + activated. This only applies to the iam auth method.
    @@ -914,14 +1300,14 @@ in `*`).
  • allow_instance_migration optional - If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution. + If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution. This only applies to authentications via the ec2 auth method.
  • disallow_reauthentication optional - If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws-ec2/identity-whitelist/' endpoint. Defaults to 'false'. + If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws/identity-whitelist/' endpoint. Defaults to 'false'. this only applies to authentications via the ec2 auth method.
@@ -943,7 +1329,7 @@ in `*`).
GET
URL
-
`/auth/aws-ec2/role/`
+
`/auth/aws/role/`
Parameters
@@ -990,7 +1376,7 @@ in `*`).
LIST/GET
URL
-
`/auth/aws-ec2/roles` (LIST) or `/auth/aws-ec2/roles?list=true` (GET)
+
`/auth/aws/roles` (LIST) or `/auth/aws/roles?list=true` (GET)
Parameters
@@ -1031,7 +1417,7 @@ in `*`).
DELETE
URL
-
`/auth/aws-ec2/role/`
+
`/auth/aws/role/`
Parameters
@@ -1044,7 +1430,7 @@ in `*`).
-### /auth/aws-ec2/role/[role]/tag +### /auth/aws/role/[role]/tag #### POST
Description
@@ -1068,7 +1454,7 @@ instance can be allowed to gain in a worst-case scenario.
POST
URL
-
`/auth/aws-ec2/role//tag`
+
`/auth/aws/role//tag`
Parameters
@@ -1106,7 +1492,7 @@ instance can be allowed to gain in a worst-case scenario.
  • disallow_reauthentication optional - If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws-ec2/identity-whitelist endpoint. Defaults to 'false'. + If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/identity-whitelist endpoint. Defaults to 'false'.
    • @@ -1139,15 +1525,18 @@ instance can be allowed to gain in a worst-case scenario.
    -### /auth/aws-ec2/login +### /auth/aws/login #### POST
    Description
    Fetch a token. This endpoint verifies the pkcs7 signature of the instance - identity document. Verifies that the instance is actually in a running state. + identity document or the signature of the signed GetCallerIdentity request. + With the ec2 auth method, or when inferring an EC2 instance, verifies that + the instance is actually in a running state. Cross checks the constraints defined on the role with which the login is being - performed. As an alternative to pkcs7 signature, the identity document along + performed. With the ec2 auth method, as an alternative to pkcs7 signature, + the identity document along with its RSA digest can be supplied to this endpoint.
    @@ -1155,7 +1544,7 @@ instance can be allowed to gain in a worst-case scenario.
    POST
    URL
    -
    `/auth/aws-ec2/login`
    +
    `/auth/aws/login`
    Parameters
    @@ -1165,16 +1554,26 @@ instance can be allowed to gain in a worst-case scenario. optional Name of the role against which the login is being attempted. If `role` is not specified, then the login endpoint looks for a role - bearing the name of the AMI ID of the EC2 instance that is trying to login. + bearing the name of the AMI ID of the EC2 instance that is trying to + login if using the ec2 auth method, or the "friendly name" (i.e., role + name or username) of the IAM principal authenticated. If a matching role is not found, login fails. +
      +
    • + auth_method + optional + The auth method to use, either ec2 or iam. If omitted, assumes ec2. +
    • +
    • identity required Base64 encoded EC2 instance identity document. This needs to be supplied along - with the `signature` parameter. If using `curl` for fetching the identity + with the `signature` parameter when using the ec2 auth method. If using `curl` + for fetching the identity document, consider using the option `-w 0` while piping the output to `base64` binary.
    • @@ -1184,7 +1583,8 @@ instance can be allowed to gain in a worst-case scenario. signature required Base64 encoded SHA256 RSA signature of the instance identity document. This - needs to be supplied along with `identity` parameter. + needs to be supplied along with `identity` parameter when using the ec2 + auth method.
      @@ -1193,7 +1593,7 @@ instance can be allowed to gain in a worst-case scenario. required PKCS7 signature of the identity document with all `\n` characters removed. Either this needs to be set *OR* both `identity` and `signature` need to be - set. + set when using the ec2 auth method.
      @@ -1209,7 +1609,51 @@ instance can be allowed to gain in a worst-case scenario. that clients provide a strong nonce. If a nonce is provided but with an empty value, it indicates intent to disable reauthentication. Note that, when `disallow_reauthentication` option is enabled on either the role or the role - tag, the `nonce` holds no significance. + tag, the `nonce` holds no significance. This is ignored unless using the + ec2 auth method. + +
    +
      +
    • + request_method + required + 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. +
    • +
    +
      +
    • + request_url + required + HTTP URL used in the signed request. Most likely just + https://sts.amazonaws.com/ as most requests will probably use POST with + an empty URI. This is required when using the iam auth method. +
    • +
    +
      +
    • + request_body + required + 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. +
    • +
    +
      +
    • + request_headers + required + Base64-encoded, JSON-serialized representation of the HTTP request + headers. The JSON serialization assumes that each header key maps to an + array of string values (though the length of that array will probably + only be one). If the iam_auth_header_value is configured in Vault for + the aws auth mount, then the headers must include the + X-Vault-AWSIAM-Server-Id header, its value must match the value + configured, and the header must be included in the signed headers. This + is required when using the iam auth method.
    @@ -1226,7 +1670,8 @@ instance can be allowed to gain in a worst-case scenario. "role_tag_max_ttl": "0", "instance_id": "i-de0f1344" "ami_id": "ami-fce36983" - "role": "dev-role" + "role": "dev-role", + "auth_method": "ec2" }, "policies": [ "default", @@ -1247,7 +1692,7 @@ instance can be allowed to gain in a worst-case scenario.
    -### /auth/aws-ec2/roletag-blacklist/ +### /auth/aws/roletag-blacklist/ #### POST
    Description
    @@ -1263,7 +1708,7 @@ instance can be allowed to gain in a worst-case scenario.
    POST
    URL
    -
    `/auth/aws-ec2/roletag-blacklist/`
    +
    `/auth/aws/roletag-blacklist/`
    Parameters
    @@ -1294,7 +1739,7 @@ instance can be allowed to gain in a worst-case scenario.
    GET
    URL
    -
    `/auth/aws-ec2/broletag-blacklist/`
    +
    `/auth/aws/broletag-blacklist/`
    Parameters
    @@ -1333,7 +1778,7 @@ instance can be allowed to gain in a worst-case scenario.
    LIST/GET
    URL
    -
    `/auth/aws-ec2/roletag-blacklist` (LIST) or `/auth/aws-ec2/roletag-blacklist?list=true` (GET)
    +
    `/auth/aws/roletag-blacklist` (LIST) or `/auth/aws/roletag-blacklist?list=true` (GET)
    Parameters
    @@ -1373,7 +1818,7 @@ instance can be allowed to gain in a worst-case scenario.
    DELETE
    URL
    -
    `/auth/aws-ec2/roletag-blacklist/`
    +
    `/auth/aws/roletag-blacklist/`
    Parameters
    @@ -1386,7 +1831,7 @@ instance can be allowed to gain in a worst-case scenario.
    -### /auth/aws-ec2/tidy/roletag-blacklist +### /auth/aws/tidy/roletag-blacklist #### POST
    Description
    @@ -1398,7 +1843,7 @@ instance can be allowed to gain in a worst-case scenario.
    POST
    URL
    -
    `/auth/aws-ec2/tidy/roletag-blacklist`
    +
    `/auth/aws/tidy/roletag-blacklist`
    Parameters
    @@ -1417,7 +1862,7 @@ instance can be allowed to gain in a worst-case scenario.
    -### /auth/aws-ec2/identity-whitelist/ +### /auth/aws/identity-whitelist/ #### GET
    Description
    @@ -1429,7 +1874,7 @@ instance can be allowed to gain in a worst-case scenario.
    GET
    URL
    -
    `/auth/aws-ec2/identity-whitelist/`
    +
    `/auth/aws/identity-whitelist/`
    Parameters
    @@ -1478,7 +1923,7 @@ instance can be allowed to gain in a worst-case scenario.
    LIST/GET
    URL
    -
    `/auth/aws-ec2/identity-whitelist` (LIST) or `/auth/aws-ec2/identity-whitelist?list=true` (GET)
    +
    `/auth/aws/identity-whitelist` (LIST) or `/auth/aws/identity-whitelist?list=true` (GET)
    Parameters
    None. @@ -1517,7 +1962,7 @@ instance can be allowed to gain in a worst-case scenario.
    DELETE
    URL
    -
    `/auth/aws-ec2/identity-whitelist/`
    +
    `/auth/aws/identity-whitelist/`
    Parameters
    @@ -1530,7 +1975,7 @@ instance can be allowed to gain in a worst-case scenario.
    -### /auth/aws-ec2/tidy/identity-whitelist +### /auth/aws/tidy/identity-whitelist #### POST
    Description
    @@ -1542,7 +1987,7 @@ instance can be allowed to gain in a worst-case scenario.
    POST
    URL
    -
    `/auth/aws-ec2/tidy/identity-whitelist`
    +
    `/auth/aws/tidy/identity-whitelist`
    Parameters
    diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 1c4fcf7f2fa7..c866e37cb941 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -178,8 +178,8 @@ AppRole - > - AWS EC2 + > + AWS > From f6ad11c0538efe8fa2710578a12e4f983e4010f2 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Fri, 27 Jan 2017 07:58:40 -0500 Subject: [PATCH 06/20] Refactor aws bind validation --- builtin/credential/aws/backend_test.go | 6 +- builtin/credential/aws/path_role.go | 90 +++++++++++++++--------- builtin/credential/aws/path_role_test.go | 11 +-- 3 files changed, 65 insertions(+), 42 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 238baf3f481c..c2a5fe31d5e5 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -38,7 +38,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { "bound_ami_id": "abcd-123", } resp, err := b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/abcd-123", Storage: storage, Data: data, @@ -105,7 +105,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { // register a different role resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/ami-6789", Storage: storage, Data: data, @@ -1303,7 +1303,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { "bound_ami_id": "ami-1234567", } roleRequestEc2 := &logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/ec2only", Storage: storage, Data: roleDataEc2, diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index fde66c0fd33d..e34942147eed 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -366,16 +366,14 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.InferredAWSRegion = inferredAWSRegionRaw.(string) } - allowInstanceIdentityDocument, allowCallerIdentity := false, false + allowEc2Auth, allowIamAuth := false, false parseAllowedAuthTypes := func(input string) string { allowedAuthTypes := []string{} for _, t := range strings.Split(input, ",") { if t == "ec2" { - allowInstanceIdentityDocument = true allowedAuthTypes = append(allowedAuthTypes, t) } else if t == "iam" { - allowCallerIdentity = true allowedAuthTypes = append(allowedAuthTypes, t) } else if t != "" { return fmt.Sprintf("unrecognized auth type: '%s'", t) @@ -397,45 +395,69 @@ func (b *backend) pathRoleCreateUpdate( } } - if allowInstanceIdentityDocument || (allowCallerIdentity && roleEntry.BoundIamPrincipalARN == "") { - // Ensure that at least one bound is set on the role - switch { - case roleEntry.BoundAccountID != "": - case roleEntry.BoundAmiID != "": - case roleEntry.BoundIamInstanceProfileARN != "": - case roleEntry.BoundIamRoleARN != "": - default: + for _, t := range roleEntry.AllowedAuthTypes { + if t == "ec2" { + allowEc2Auth = true + } else if t == "iam" { + allowIamAuth = true + } else { + return nil, fmt.Errorf("Unrecognized auth_type in roleEntry: %s", t) + } + } - return logical.ErrorResponse("at least be one bound parameter should be specified on the role with auth_type ec2 or auth_type iam"), nil + allowEc2Binds := allowEc2Auth + + if roleEntry.RoleInferredType != "" { + if !allowIamAuth { + return logical.ErrorResponse("specified role_inferred_type but didn't allow iam auth_type"), nil + } else if roleEntry.RoleInferredType != "ec2_instance" { + return logical.ErrorResponse(fmt.Sprintf("specified invalid role_inferred_type: %s", roleEntry.RoleInferredType)), nil + } else if roleEntry.InferredAWSRegion == "" { + return logical.ErrorResponse("specified role_inferred_type but not inferred_aws_region"), nil } + allowEc2Binds = true + } else if roleEntry.InferredAWSRegion != "" { + return logical.ErrorResponse("specified inferred_aws_region but not role_inferred_type"), nil } - if allowCallerIdentity { - if roleEntry.RoleInferredType != "" { - if roleEntry.RoleInferredType != "ec2_instance" { - return logical.ErrorResponse(fmt.Sprintf("invalid role_inferred_type value: '%s'", roleEntry.RoleInferredType)), nil - } - if roleEntry.InferredAWSRegion == "" { - return logical.ErrorResponse("must set inferred_aws_region when setting role_inferred_type"), nil - } - } else { - if roleEntry.BoundIamPrincipalARN == "" { - return logical.ErrorResponse("must set bound_iam_principal_arn if not setting role_inferred_type"), nil - } - if roleEntry.InferredAWSRegion != "" { - return logical.ErrorResponse("must not set inferred_aws_region without role_inferred_type"), nil - } + numBinds := 0 + + if roleEntry.BoundAccountID != "" { + if !allowEc2Auth { + return logical.ErrorResponse("specified bound_account_id but not allowing ec2 auth_method"), nil } - } else { - if roleEntry.RoleInferredType != "" { - return logical.ErrorResponse("must only set role_inferred_type when allowing iam auth_type"), nil + numBinds++ + } + + if roleEntry.BoundAmiID != "" { + if !allowEc2Binds { + return logical.ErrorResponse("specified bound_ami_id but not allowing ec2 auth_method or inferring ec2_instance"), nil } - if roleEntry.BoundIamPrincipalARN != "" { - return logical.ErrorResponse("must only set bound_iam_principal_arn when allowing iam auth_type"), nil + numBinds++ + } + + if roleEntry.BoundIamInstanceProfileARN != "" { + if !allowEc2Binds { + return logical.ErrorResponse("specified bound_iam_instance_profile_arn but not allowing ec2 auth_method or inferring ec2_instance"), nil + } + } + + if roleEntry.BoundIamRoleARN != "" { + if !allowEc2Binds { + return logical.ErrorResponse("specified bound_iam_role_arn but not allowing ec2 auth_method or inferring ec2_instance"), nil } - if roleEntry.InferredAWSRegion != "" { - return logical.ErrorResponse("must not set inferred_aws_region without allowing iam auth_type"), nil + numBinds++ + } + + if roleEntry.BoundIamPrincipalARN != "" { + if !allowIamAuth { + return logical.ErrorResponse("specified bound_iam_principal_arn but not allowing iam auth_method"), nil } + numBinds++ + } + + if numBinds == 0 { + return logical.ErrorResponse("at least be one bound parameter should be specified on the role"), nil } policiesStr, ok := data.GetOk("policies") diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index 0b64b1c6564a..dd41056dafff 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/vault/logical" ) -func TestBackend_pathRoleInstanceIdentityDocument(t *testing.T) { +func TestBackend_pathRoleEc2(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -64,7 +64,7 @@ func TestBackend_pathRoleInstanceIdentityDocument(t *testing.T) { Storage: storage, }) if resp != nil && resp.IsError() { - t.Fatalf("failed to create role") + t.Fatalf("failed to create role: %s", resp.Data["error"]) } if err != nil { t.Fatal(err) @@ -82,14 +82,15 @@ func TestBackend_pathRoleInstanceIdentityDocument(t *testing.T) { } // add another entry, to test listing of role entries + data["bound_ami_id"] = "ami-abcd456" resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/ami-abcd456", Data: data, Storage: storage, }) if resp != nil && resp.IsError() { - t.Fatalf("failed to create role") + t.Fatalf("failed to create role: %s", resp.Data["error"]) } if err != nil { t.Fatal(err) @@ -134,7 +135,7 @@ func TestBackend_pathRoleInstanceIdentityDocument(t *testing.T) { } -func TestBackend_pathRoleSignedCallerIdentity(t *testing.T) { +func TestBackend_pathIam(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage From a422873b2d9704021a779b21a2bc583ef41ec377 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Wed, 1 Feb 2017 21:53:06 -0500 Subject: [PATCH 07/20] Fix env var override in aws backend test Intent is to override the AWS environment variables with the TEST_* versions if they are set, but the reverse was happening. --- builtin/credential/aws/backend_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index c2a5fe31d5e5..8a240c662a30 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1195,7 +1195,7 @@ func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[s // it requires the following environment variables to be set: // TEST_AWS_ACCESS_KEY_ID // TEST_AWS_SECRET_ACCESS_KEY -// TEST_AWS_SECURITY_TOKEN (optional, if you are using short-lived creds) +// TEST_AWS_SECURITY_TOKEN or TEST_AWS_SESSION_TOKEN (optional, if you are using short-lived creds) // These are intentionally NOT the "standard" variables to prevent accidentally // using prod creds in acceptance tests func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { @@ -1227,8 +1227,8 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // potentially pick up credentials from the ~/.config files), but probably // good enough rather than having to muck around in the low-level details for _, envvar := range []string{ - "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SECURITY_TOKEN"} { - os.Setenv("TEST_"+envvar, os.Getenv(envvar)) + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SECURITY_TOKEN", "AWS_SESSION_TOKEN"} { + os.Setenv(envvar, os.Getenv("TEST_"+envvar)) } awsSession, err := session.NewSession() if err != nil { From 4ffc99e21fe3e4bf7775e5fa46bfac528f01d149 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Sun, 26 Feb 2017 18:44:53 -0500 Subject: [PATCH 08/20] Update docs on use of IAM authentication profile AWS now allows you to change the instance profile of a running instance, so the use case of "a long-lived instance that's not in an instance profile" no longer means you have to use the the EC2 auth method. You can now just change the instance profile on the fly. --- website/source/docs/auth/aws-ec2.html.md | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 06adc9b31cdd..00d60f97e803 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -73,7 +73,7 @@ from the EC2 instance metadata service for an EC2 instance, or from the AWS environment variables in an AWS Lambda function execution, which obviates the need for an operator to manually provision some sort of identity material first. However, the credentials can, in principle, come from anywhere, not just from -the locations AWS hasprovided for you. +the locations AWS has provided for you. Each signed AWS request includes the current timestamp to mitigate the risk of replay attacks. In addition, Vault allows you to require an additional header, @@ -84,7 +84,7 @@ requires that this header be one of the headers included in the AWS signature and relies upon AWS to authenticate that signature. While AWS API endpoints support both signed GET and POST requests, for -simplicity, the aws-iam backend supports only POST requests. It also does not +simplicity, the aws backend supports only POST requests. It also does not support `presigned` requests, i.e., requests with `X-Amz-Credential`, `X-Amz-signature`, and `X-Amz-SignedHeaders` GET query parameter containing the authenticating information. @@ -249,21 +249,16 @@ comparison of the two authentication methods. nature, but it's easier to spoof credentials that might have come from an EC2 instance. * Specific use cases - * If you have a long-lived EC2 instance which you are unable to relaunch into - an IAM instance profile, then the ec2 auth method is probably the best - solution for you. (While you could store long-lived AWS IAM user credentials - on disk and use those to authenticate to Vault, that would not be - recommended.) * If you have non-EC2 instance entities, such as IAM users, Lambdas in IAM roles, or developer laptops using [AdRoll's Hologram](https://github.com/AdRoll/hologram) then you would need to use the iam auth method. - * If you have EC2 instances which are already in an IAM instance profile, then - you could use either auth method. If you need more granular filtering beyond just - the instance profile of given EC2 instances (such as filtering based off - the AMI the instance was launched from), then you would need to - use the ec2 auth method, launch your EC2 instances into unique instance - profiles for each different Vault role you would want them to authenticate + * If you have EC2 instances, then you could use either auth method. If you + need more granular filtering beyond just the instance profile of given EC2 + instances (such as filtering based off the AMI the instance was launched + from), then you would need to use the ec2 auth method, change the instance + profile associated with your EC2 instances so they have unique IAM roles + for each different Vault role you would want them to authenticate to, or make use of inferencing. ## Client Nonce @@ -1374,7 +1369,11 @@ constraint is checked only by the ec2 auth method. bound_region optional If set, defines a constraint on the EC2 instances that the region in - its identity document to match the one specified by this parameter. + its identity document to match the one specified by this parameter. This + constraint is only checked by the ec2 auth method (as IAM is a global + service so it doesn't make sense to bind by region with the iam auth + method, and binding by region is implied with the inferred AWS region + when inferring an EC2 instance).
      From f4b3841ad45dc18ff8c95b8d5ab44c173b94dd4f Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 7 Mar 2017 00:01:44 -0500 Subject: [PATCH 09/20] Fix typo in aws auth cli help --- builtin/credential/aws/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 2f7f040c7e01..298ab426c3ba 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -96,7 +96,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { func (h *CLIHandler) Help() string { help := ` -The AWS credentaial provider allows you to authenticate with +The AWS credential provider allows you to authenticate with AWS IAM credentials. To use it, you specify valid AWS IAM credentials in one of a number of ways. They can be specified explicitly on the command line (which in general you should not do), via the standard AWS From 15c3ec88e1b296009fa52f10795941aa7f5a6ebf Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 28 Mar 2017 00:45:57 -0400 Subject: [PATCH 10/20] Respond to PR feedback --- builtin/credential/aws/backend_test.go | 9 ++- builtin/credential/aws/cli.go | 11 +++- builtin/credential/aws/client.go | 7 ++- builtin/credential/aws/path_config_client.go | 6 +- .../credential/aws/path_config_client_test.go | 8 +-- builtin/credential/aws/path_login.go | 61 +++++++++++++------ builtin/credential/aws/path_login_test.go | 24 ++++---- builtin/credential/aws/path_role.go | 45 ++++++++------ cli/commands.go | 1 - vault/auth.go | 7 +++ website/source/docs/auth/aws-ec2.html.md | 43 ++++++------- 11 files changed, 129 insertions(+), 93 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 6037a57846ce..88ef8d3db709 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1293,7 +1293,6 @@ func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[s return nil, err } return map[string]interface{}{ - "auth_type": "iam", "request_method": request.Method, "request_url": request.URL.String(), "request_headers": base64.StdEncoding.EncodeToString(headersJson), @@ -1379,7 +1378,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { const testInvalidRoleName = "invalid-role" clientConfigData := map[string]interface{}{ - "iam_auth_header_value": testVaultHeaderValue, + "iam_server_id_header_value": testVaultHeaderValue, } clientRequest := &logical.Request{ Operation: logical.UpdateOperation, @@ -1399,7 +1398,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { "allowed_auth_types": "iam", } roleRequest := &logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/" + testValidRoleName, Storage: storage, Data: roleData, @@ -1465,7 +1464,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // and reading the body modifies the underlying request, so it's just cleaner // to get new requests. stsRequestInvalidHeader, _ := stsService.GetCallerIdentityRequest(stsInputParams) - stsRequestInvalidHeader.HTTPRequest.Header.Add(magicVaultHeader, "InvalidValue") + stsRequestInvalidHeader.HTTPRequest.Header.Add(iamServerIdHeader, "InvalidValue") stsRequestInvalidHeader.Sign() loginData, err = buildCallerIdentityLoginData(stsRequestInvalidHeader.HTTPRequest, testValidRoleName) if err != nil { @@ -1484,7 +1483,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // Now, valid request against invalid role stsRequestValid, _ := stsService.GetCallerIdentityRequest(stsInputParams) - stsRequestValid.HTTPRequest.Header.Add(magicVaultHeader, testVaultHeaderValue) + stsRequestValid.HTTPRequest.Header.Add(iamServerIdHeader, testVaultHeaderValue) stsRequestValid.Sign() loginData, err = buildCallerIdentityLoginData(stsRequestValid.HTTPRequest, testInvalidRoleName) if err != nil { diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 298ab426c3ba..65761d8a5292 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -32,6 +32,8 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { headerValue = "" } + // Grab any supplied credentials off the command line + // Ensure we're able to fall back to the SDK default credential providers credConfig := &awsutil.CredentialsConfig{ AccessKey: m["aws_access_key_id"], SecretKey: m["aws_secret_access_key"], @@ -45,6 +47,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { return "", fmt.Errorf("could not compile valid credential providers from static config, environemnt, shared, or instance metadata") } + // Use the credentials we've found to construct an STS session stsSession, err := session.NewSessionWithOptions(session.Options{ Config: aws.Config{Credentials: creds}, }) @@ -55,10 +58,14 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { var params *sts.GetCallerIdentityInput svc := sts.New(stsSession) stsRequest, _ := svc.GetCallerIdentityRequest(params) + + // Inject the required auth header value, if suplied, and then sign the request including that header if headerValue != "" { - stsRequest.HTTPRequest.Header.Add(magicVaultHeader, headerValue) + stsRequest.HTTPRequest.Header.Add(iamServerIdHeader, headerValue) } stsRequest.Sign() + + // Now extract out the relevant parts of the request headersJson, err := json.Marshal(stsRequest.HTTPRequest.Header) if err != nil { return "", err @@ -72,9 +79,9 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { headers := base64.StdEncoding.EncodeToString(headersJson) body := base64.StdEncoding.EncodeToString(requestBody) + // And pass them on to the Vault server path := fmt.Sprintf("auth/%s/login", mount) secret, err := c.Logical().Write(path, map[string]interface{}{ - "auth_type": "iam", "request_method": method, "request_url": targetUrl, "request_headers": headers, diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index b76a93e64f88..b86150a4f4b0 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -34,11 +34,12 @@ func (b *backend) getClientConfig(s logical.Storage, region, clientType string) endpoint := aws.String("") if config != nil { // Override the default endpoint with the configured endpoint. - if clientType == "ec2" && config.Endpoint != "" { + switch { + case clientType == "ec2" && config.Endpoint != "": endpoint = aws.String(config.Endpoint) - } else if clientType == "iam" && config.IAMEndpoint != "" { + case clientType == "iam" && config.IAMEndpoint != "": endpoint = aws.String(config.IAMEndpoint) - } else if clientType == "sts" && config.STSEndpoint != "" { + case clientType == "sts" && config.STSEndpoint != "": endpoint = aws.String(config.STSEndpoint) } diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index ab382a0ea002..72cd25db159c 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -40,7 +40,7 @@ func pathConfigClient(b *backend) *framework.Path { Description: "URL to override the default generated endpoint for making AWS STS API calls.", }, - "iam_auth_header_value": &framework.FieldSchema{ + "iam_server_id_header_value": &framework.FieldSchema{ Type: framework.TypeString, Default: "", Description: "Value to require in the X-Vault-AWSIAM-Server-ID request header", @@ -200,14 +200,14 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry.STSEndpoint = data.Get("sts_endpoint").(string) } - headerValStr, ok := data.GetOk("iam_auth_header_value") + headerValStr, ok := data.GetOk("iam_server_id_header_value") if ok { if configEntry.HeaderValue != headerValStr.(string) { // NOT setting changedCreds here, since this isn't really cached configEntry.HeaderValue = headerValStr.(string) } } else if req.Operation == logical.CreateOperation { - configEntry.HeaderValue = data.Get("iam_auth_header_value").(string) + configEntry.HeaderValue = data.Get("iam_server_id_header_value").(string) } // Since this endpoint supports both create operation and update operation, diff --git a/builtin/credential/aws/path_config_client_test.go b/builtin/credential/aws/path_config_client_test.go index 94590eb46f6b..48f9943ba5fa 100644 --- a/builtin/credential/aws/path_config_client_test.go +++ b/builtin/credential/aws/path_config_client_test.go @@ -41,8 +41,8 @@ func TestBackend_pathConfigClient(t *testing.T) { } data := map[string]interface{}{ - "sts_endpoint": "https://my-custom-sts-endpoint.example.com", - "iam_auth_header_value": "vault_server_identification_314159", + "sts_endpoint": "https://my-custom-sts-endpoint.example.com", + "iam_server_id_header_value": "vault_server_identification_314159", } resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, @@ -69,8 +69,8 @@ func TestBackend_pathConfigClient(t *testing.T) { if resp == nil || resp.IsError() { t.Fatal("failed to read the client config entry") } - if resp.Data["iam_auth_header_value"] != data["vault_header_value"] { + if resp.Data["iam_server_id_header_value"] != data["vault_header_value"] { t.Fatalf("expected vault_header_value: '%#v'; returned vault_header_value: '%#v'", - data["iam_auth_header_value"], resp.Data["iam_auth_header_value"]) + data["iam_server_id_header_value"], resp.Data["iam_server_id_header_value"]) } } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 0dc1efcaf963..398dd6beb652 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -42,13 +42,6 @@ bearing the name of the AMI ID of the EC2 instance that is trying to login. If a matching role is not found, login fails.`, }, - "auth_type": { - Type: framework.TypeString, - Default: "ec2", - Description: `The login type to use upon logging in. The valid choices are -ec2 (default) or iam.`, - }, - "pkcs7": { Type: framework.TypeString, Description: `PKCS7 signature of the identity document when using an auth_type @@ -79,7 +72,7 @@ presigned request. Currently, POST is the only supported value`, "request_url": { Type: framework.TypeString, - Description: `Full URL against which to make the AWS request when auth_method is + Description: `Full URL against which to make the AWS request when auth_type is iam. If using a POST request with the action specified in the body, this should just be "/".`, }, @@ -364,13 +357,21 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id func (b *backend) pathLoginUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - auth_type := data.Get("auth_type") - if auth_type == "ec2" { + anyEc2, allEc2 := hasValuesForEc2Auth(data) + anyIam, allIam := hasValuesForIamAuth(data) + switch { + case anyEc2 && anyIam: + return logical.ErrorResponse("supplied auth values for both ec2 and iam auth types"), nil + case anyEc2 && !allEc2: + return logical.ErrorResponse("supplied some of the auth values for the ec2 auth type but not all"), nil + case anyEc2: return b.pathLoginUpdateEc2(req, data) - } else if auth_type == "iam" { + case anyIam && !allIam: + return logical.ErrorResponse("supplied some of the auth values for the iam auth type but not all"), nil + case anyIam: return b.pathLoginUpdateIam(req, data) - } else { - return logical.ErrorResponse("unrecognized auth_type, must be one of ec2 or iam"), nil + default: + return logical.ErrorResponse("didn't supply required authentication values"), nil } } @@ -763,7 +764,6 @@ func (b *backend) pathLoginUpdateEc2( "role_tag_max_ttl": rTagMaxTTL.String(), "role": roleName, "ami_id": identityDocParsed.AmiID, - "auth_type": "ec2", }, LeaseOptions: logical.LeaseOptions{ Renewable: true, @@ -1092,7 +1092,7 @@ func (b *backend) pathLoginUpdateIam( if config.HeaderValue != "" { ok, msg := ensureVaultHeaderValue(headers, parsedUrl, config.HeaderValue) if !ok { - return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %s", magicVaultHeader, msg)), nil + return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %s", iamServerIdHeader, msg)), nil } } if config.STSEndpoint != "" { @@ -1145,7 +1145,7 @@ func (b *backend) pathLoginUpdateIam( // It's a bit of a hack to use the inferred "EC2" region to get a region for the IAM client // IAM is a "global" service and so doesn't really have a region, so it wouldn't matter - // Except that the region is used to infer the partion (i.e., aws, aws-cn, or aws-us-gov), + // Except that the region is used to infer the partition (i.e., aws, aws-cn, or aws-us-gov), // and we can safely assume that the EC2 client will be in the same partition as IAM iamClient, err := b.clientIAM(req.Storage, roleEntry.InferredAWSRegion, accountID) if err != nil { @@ -1222,6 +1222,27 @@ func (b *backend) pathLoginUpdateIam( return resp, nil } +// These two methods (hasValuesFor*) return two bools +// The first is a hasAll, that is, does the request have all the values +// necessary for this auth method +// The second is a hasAny, that is, does the request have any of the fields +// exclusive to this auth method +func hasValuesForEc2Auth(data *framework.FieldData) (bool, bool) { + _, hasPkcs7 := data.GetOk("pkcs7") + _, hasIdentity := data.GetOk("identity") + _, hasSignature := data.GetOk("signature") + return (hasPkcs7 || (hasIdentity && hasSignature)), (hasPkcs7 || hasIdentity || hasSignature) +} + +func hasValuesForIamAuth(data *framework.FieldData) (bool, bool) { + _, hasRequestMethod := data.GetOk("request_method") + _, hasRequestUrl := data.GetOk("request_url") + _, hasRequestBody := data.GetOk("request_body") + _, hasRequestHeaders := data.GetOk("request_headers") + return (hasRequestMethod && hasRequestUrl && hasRequestBody && hasRequestHeaders), + (hasRequestMethod || hasRequestUrl || hasRequestBody || hasRequestHeaders) +} + func parseIamArn(iamArn string) (string, string, string, error) { fullParts := strings.Split(iamArn, ":") principalFullName := fullParts[5] @@ -1241,13 +1262,13 @@ func parseIamArn(iamArn string) (string, string, string, error) { func ensureVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) (bool, string) { providedValue := "" for k, v := range headers { - if strings.ToLower(magicVaultHeader) == strings.ToLower(k) { + if strings.ToLower(iamServerIdHeader) == strings.ToLower(k) { providedValue = strings.Join(v, ",") break } } if providedValue == "" { - return false, fmt.Sprintf("didn't find %s", magicVaultHeader) + return false, fmt.Sprintf("didn't find %s", iamServerIdHeader) } // NOT doing a constant time compare here since the value is NOT intended to be secret @@ -1268,7 +1289,7 @@ func ensureVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHe return false, "found multiple SignedHeaders components" } signedHeaders := string(matches[1]) - return ensureHeaderIsSigned(signedHeaders, magicVaultHeader) + return ensureHeaderIsSigned(signedHeaders, iamServerIdHeader) } // 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 @@ -1414,7 +1435,7 @@ type roleTagLoginResponse struct { DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } -const magicVaultHeader = "X-Vault-AWSIAM-Server-Id" +const iamServerIdHeader = "X-Vault-AWSIAM-Server-Id" const pathLoginSyn = ` Authenticates an EC2 instance with Vault. diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 60dde74c6228..14f168030103 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -92,25 +92,25 @@ func TestBackend_ensureVaultHeaderValue(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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } postHeadersInvalid := http.Header{ - "Host": []string{"Foo"}, - magicVaultHeader: []string{"InvalidValue"}, - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "Host": []string{"Foo"}, + iamServerIdHeader: []string{"InvalidValue"}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } postHeadersUnsigned := http.Header{ - "Host": []string{"Foo"}, - magicVaultHeader: []string{canaryHeaderValue}, - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } postHeadersValid := http.Header{ - "Host": []string{"Foo"}, - magicVaultHeader: []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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } postHeadersSplit := http.Header{ - "Host": []string{"Foo"}, - magicVaultHeader: []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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, } found, errMsg := ensureVaultHeaderValue(postHeadersMissing, requestUrl, canaryHeaderValue) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 84a0de196678..c89be7b5a2da 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -66,8 +66,7 @@ the value specified by this parameter. The value is prefix-matched auth_type is ec2.`, }, "role_inferred_type": { - Type: framework.TypeString, - Default: false, + Type: framework.TypeString, Description: `When auth_type is iam, the AWS entity type to infer from the authenticated principal. The only supported value is ec2_instance, which will extract the EC2 instance ID from the @@ -402,16 +401,18 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.InferredAWSRegion = inferredAWSRegionRaw.(string) } - allowEc2Auth, allowIamAuth := false, false + var allowEc2Auth, allowIamAuth bool parseAllowedAuthTypes := func(input string) string { allowedAuthTypes := []string{} for _, t := range strings.Split(input, ",") { - if t == "ec2" { + switch t { + case "ec2": allowedAuthTypes = append(allowedAuthTypes, t) - } else if t == "iam" { + case "iam": allowedAuthTypes = append(allowedAuthTypes, t) - } else if t != "" { + case "": + default: return fmt.Sprintf("unrecognized auth type: '%s'", t) } } @@ -431,12 +432,19 @@ func (b *backend) pathRoleCreateUpdate( } } + // Reparse the existing roleEntry.AllowedAuthTypes + // We need to do this to support the use case where an existing role is being updated, and + // allowed_auth_types isn't being updated as part of the role. We need to make sure that any + // new bindings requested are still valid bindings. For example, let's say a role is created + // with an auth_type of iam and no inferencing is configured; then, in a subsequent role update, + // bound_ami_id is specified. This is how that edge case is caught. for _, t := range roleEntry.AllowedAuthTypes { - if t == "ec2" { + switch t { + case "ec2": allowEc2Auth = true - } else if t == "iam" { + case "iam": allowIamAuth = true - } else { + default: return nil, fmt.Errorf("Unrecognized auth_type in roleEntry: %s", t) } } @@ -444,11 +452,12 @@ func (b *backend) pathRoleCreateUpdate( allowEc2Binds := allowEc2Auth if roleEntry.RoleInferredType != "" { - if !allowIamAuth { + switch { + case !allowIamAuth: return logical.ErrorResponse("specified role_inferred_type but didn't allow iam auth_type"), nil - } else if roleEntry.RoleInferredType != "ec2_instance" { + case roleEntry.RoleInferredType != "ec2_instance": return logical.ErrorResponse(fmt.Sprintf("specified invalid role_inferred_type: %s", roleEntry.RoleInferredType)), nil - } else if roleEntry.InferredAWSRegion == "" { + case roleEntry.InferredAWSRegion == "": return logical.ErrorResponse("specified role_inferred_type but not inferred_aws_region"), nil } allowEc2Binds = true @@ -460,42 +469,42 @@ func (b *backend) pathRoleCreateUpdate( if roleEntry.BoundAccountID != "" { if !allowEc2Auth { - return logical.ErrorResponse("specified bound_account_id but not allowing ec2 auth_method"), nil + return logical.ErrorResponse("specified bound_account_id but not allowing ec2 auth_type"), nil } numBinds++ } if roleEntry.BoundRegion != "" { if !allowEc2Auth { - return logical.ErrorResponse("specified bound_region but not allowing ec2 auth_method"), nil + return logical.ErrorResponse("specified bound_region but not allowing ec2 auth_type"), nil } numBinds++ } if roleEntry.BoundAmiID != "" { if !allowEc2Binds { - return logical.ErrorResponse("specified bound_ami_id but not allowing ec2 auth_method or inferring ec2_instance"), nil + return logical.ErrorResponse("specified bound_ami_id but not allowing ec2 auth_type or inferring ec2_instance"), nil } numBinds++ } if roleEntry.BoundIamInstanceProfileARN != "" { if !allowEc2Binds { - return logical.ErrorResponse("specified bound_iam_instance_profile_arn but not allowing ec2 auth_method or inferring ec2_instance"), nil + return logical.ErrorResponse("specified bound_iam_instance_profile_arn but not allowing ec2 auth_type or inferring ec2_instance"), nil } numBinds++ } if roleEntry.BoundIamRoleARN != "" { if !allowEc2Binds { - return logical.ErrorResponse("specified bound_iam_role_arn but not allowing ec2 auth_method or inferring ec2_instance"), nil + return logical.ErrorResponse("specified bound_iam_role_arn but not allowing ec2 auth_type or inferring ec2_instance"), nil } numBinds++ } if roleEntry.BoundIamPrincipalARN != "" { if !allowIamAuth { - return logical.ErrorResponse("specified bound_iam_principal_arn but not allowing iam auth_method"), nil + return logical.ErrorResponse("specified bound_iam_principal_arn but not allowing iam auth_type"), nil } numBinds++ } diff --git a/cli/commands.go b/cli/commands.go index 8b878156befb..7494c0676ee9 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -72,7 +72,6 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { "approle": credAppRole.Factory, "cert": credCert.Factory, "aws": credAws.Factory, - "aws-ec2": credAws.Factory, "app-id": credAppId.Factory, "github": credGitHub.Factory, "userpass": credUserpass.Factory, diff --git a/vault/auth.go b/vault/auth.go index c3197bce2c53..5a5e68b2f984 100644 --- a/vault/auth.go +++ b/vault/auth.go @@ -35,6 +35,10 @@ const ( var ( // errLoadAuthFailed if loadCredentials encounters an error errLoadAuthFailed = errors.New("failed to setup auth table") + + // credentialAliases maps old backend names to new backend names, allowing us + // to move/rename backends but maintain backwards compatibility + credentialAliases = map[string]string{"aws-ec2": "aws"} ) // enableCredential is used to enable a new credential backend @@ -457,6 +461,9 @@ func (c *Core) teardownCredentials() error { // newCredentialBackend is used to create and configure a new credential backend by name func (c *Core) newCredentialBackend( t string, sysView logical.SystemView, view logical.Storage, conf map[string]string) (logical.Backend, error) { + if alias, ok := credentialAliases[t]; ok { + t = alias + } f, ok := c.credentialBackends[t] if !ok { return nil, fmt.Errorf("unknown backend type: %s", t) diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 0c8decdd3f0f..734519f1e0e4 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -24,11 +24,12 @@ clients. ## Authentication Workflow -There are two authentication types present in the aws backend: ec2 (which was -formerly the only type supplied in the aws-ec2 auth backend) and iam. Each has a -different authentication workflow, and each can solve different use cases. See -the section on comparing the two auth methods below to help determine which -method is more appropriate for your use cases. +There are two authentication types present in the aws backend: `ec2` and `iam`. +Based on how you attempt to authenticate, Vault will determine if you are +attempting to use the `ec2` or `iam` type. Each has a different authentication +workflow, and each can solve different use cases. See the section on comparing +the two auth methods below to help determine which method is more appropriate +for your use cases. ### EC2 Authentication Method @@ -86,7 +87,7 @@ and relies upon AWS to authenticate that signature. While AWS API endpoints support both signed GET and POST requests, for simplicity, the aws backend supports only POST requests. It also does not support `presigned` requests, i.e., requests with `X-Amz-Credential`, -`X-Amz-signature`, and `X-Amz-SignedHeaders` GET query parameter containing the +`X-Amz-signature`, and `X-Amz-SignedHeaders` GET query parameters containing the authenticating information. It's also important to note that Amazon does NOT appear to include any sort @@ -188,19 +189,19 @@ type will be enforced by Vault_. Some examples: 1. You configure a role only allowing the ec2 auth type, with a bound AMI ID. A client would not be able to login using the iam auth type. -1. You configure a role only allowing the iam auth type, with a bound IAM +2. You configure a role only allowing the iam auth type, with a bound IAM principal ARN. A client would not be able to login with the ec2 auth method. -1. You configure a role only allowing the iam auth type and further configure +3. You configure a role only allowing the iam auth type and further configure inferencing. You have a bound AMI ID and a bound IAM principal ARN. A client must login using the iam method; the RoleSessionName must be a valid instance ID viewable by Vault, and the instance must have come from the bound AMI ID. -1. You configure a role to allow both iam and ec2 auth types, but you have not +4. You configure a role to allow both iam and ec2 auth types, but you have not configured inferencing. You configure both a bound AMI ID and a bound IAM principal ARN. If a client chooses to login with the ec2 auth method, only the bound AMI is checked; the bound IAM principal ARN is ignored. Similarly, if a client logs in with the iam auth method, then only the bound IAM principal ARN is checked; the bound AMI ID is ignored. -1. You configure a role to allow both iam and ec2 auth types, and you have +5. You configure a role to allow both iam and ec2 auth types, and you have further configured inferencing, with a bound IAM principal ARN and a bound AMI ID. If a client logs in with the ec2 auth method, then only the bound AMI ID is checked. If a client logs in with the iam auth method, then the same @@ -516,7 +517,7 @@ $ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5 ``` $ vault write auth/aws/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h -$ vault write auth/aws/role/dev-role-iam allowed_auth_methods=iam \ +$ vault write auth/aws/role/dev-role-iam allowed_auth_types=iam \ bound_iam_principal_arn=arn:aws:iam::123456789012:role/MyRole policies=prod,dev max_ttl=500h ``` @@ -631,7 +632,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/config/cl ``` curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role -d '{"bound_ami_id":"ami-fce3c696","policies":"prod,dev","max_ttl":"500h"}' -curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role-iam -d '{"allowed_auth_methods":"iam","policies":"prod,dev","max_ttl":"500h","bound_iam_principal_arn":"arn:aws:iam::123456789012:role/MyRole"}' +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role-iam -d '{"allowed_auth_types":"iam","policies":"prod,dev","max_ttl":"500h","bound_iam_principal_arn":"arn:aws:iam::123456789012:role/MyRole"}' ``` #### Perform the login operation @@ -655,8 +656,7 @@ The response will be in JSON. For example: "region": "us-east-1", "nonce": "5defbf9e-a8f9-3063-bdfc-54b7a42a1f95", "instance_id": "i-a832f734", - "ami_id": "ami-f083709d", - "auth_type": "ec2" + "ami_id": "ami-f083709d" }, "policies": [ "default", @@ -739,7 +739,7 @@ The response will be in JSON. For example:
    • - iam_auth_header_value + iam_server_id_header_value optional The value to require in the `X-Vault-AWSIAM-Server-ID` header as part of GetCallerIdentity requests that are used in the iam auth method. If not @@ -791,7 +791,7 @@ The response will be in JSON. For example: "endpoint" "", "iam_endpoint" "", "sts_endpoint" "", - "iam_auth_header_value" "", + "iam_server_id_header_value" "", }, "lease_duration": 0, "renewable": false, @@ -1772,13 +1772,6 @@ auth method only when inferring an ec2 instance. If a matching role is not found, login fails.
    -
      -
    • - auth_method - optional - The auth method to use, either ec2 or iam. If omitted, assumes ec2. -
    • -
    • identity @@ -1861,7 +1854,7 @@ auth method only when inferring an ec2 instance. Base64-encoded, JSON-serialized representation of the HTTP request headers. The JSON serialization assumes that each header key maps to an array of string values (though the length of that array will probably - only be one). If the iam_auth_header_value is configured in Vault for + only be one). If the iam_server_id_header_value is configured in Vault for the aws auth mount, then the headers must include the X-Vault-AWSIAM-Server-Id header, its value must match the value configured, and the header must be included in the signed headers. This @@ -1883,7 +1876,7 @@ auth method only when inferring an ec2 instance. "instance_id": "i-de0f1344" "ami_id": "ami-fce36983" "role": "dev-role", - "auth_method": "ec2" + "auth_type": "ec2" }, "policies": [ "default", From 5c82f59219a71b6e1fceb9277e62e39bedb45f53 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 28 Mar 2017 23:32:47 -0400 Subject: [PATCH 11/20] More PR feedback --- builtin/credential/aws/path_login.go | 2 +- builtin/credential/aws/path_role.go | 10 +++++ website/source/docs/auth/aws-ec2.html.md | 53 +++--------------------- 3 files changed, 16 insertions(+), 49 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 398dd6beb652..b603e04de618 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -1362,7 +1362,7 @@ func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, func roleAllowsAuthMethod(authMethod string, roleEntry *awsRoleEntry) bool { allowedAuthMethod := false for _, allowedAuthType := range roleEntry.AllowedAuthTypes { - if allowedAuthType == "iam" { + if allowedAuthType == authMethod { allowedAuthMethod = true break } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index c89be7b5a2da..5ba7b557e5ac 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -282,6 +282,16 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole } } + // Check if there was no pre-existing AllowedAuthTypes set (from older versions) + if len(result.AllowedAuthTypes) == 0 { + // then default to the original behavior of ec2 + result.AllowedAuthTypes = []string{"ec2"} + // and save the result + if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { + return nil, fmt.Errorf("failed to save default allowed_auth_types") + } + } + return &result, nil } diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 734519f1e0e4..34d9550e5554 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -556,54 +556,11 @@ $ vault auth -method=aws header_value=vault.example.com role=dev-role-iam \ aws_security_token= ``` -For reference, the following Go program also demonstrates how to generate the -required parameters (assuming you are using a default AWS credential provider), -filling in the value for the header value as appropriate: - -``` -package main - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" -) - -func main() { - sess, err := session.NewSession() - if err != nil { - fmt.Println("failed to create session,", err) - return - } - - svc := sts.New(sess) - var params *sts.GetCallerIdentityInput - stsRequest, _ := svc.GetCallerIdentityRequest(params) - stsRequest.HTTPRequest.Header.Add("X-Vault-AWSIAM-Server-ID", "vault.example.com") - stsRequest.Sign() - - headersJson, err := json.Marshal(stsRequest.HTTPRequest.Header) - if err != nil { - fmt.Println(fmt.Errorf("Error:", err)) - return - } - requestBody, err := ioutil.ReadAll(stsRequest.HTTPRequest.Body) - if err != nil { - fmt.Println(fmt.Errorf("Error:", err)) - return - } - fmt.Println("request_method=" + stsRequest.HTTPRequest.Method) - fmt.Println("request_url=" + stsRequest.HTTPRequest.URL.String()) - fmt.Println("request_headers=" + base64.StdEncoding.EncodeToString(headersJson)) - fmt.Println("request_body=" + base64.StdEncoding.EncodeToString(requestBody)) -} - -``` -Using this, we can get the values to pass in to the `vault write` operation: +An example of how to generate the required request values for the `login` method +can be found found in the [vault cli +source code](https://github.com/hashicorp/vault/blob/master/builtin/credential/aws/cli.go). +Using an approach such as this, the request parameters can be generated and +passed to the `login` method: ``` $ vault write auth/aws/login role=dev-role-iam \ From 95c024c5b359162c97f80fb6e561a0915de59752 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 30 Mar 2017 04:04:07 -0400 Subject: [PATCH 12/20] Respond to additional PR feedback --- builtin/credential/aws/backend_test.go | 16 +- builtin/credential/aws/cli.go | 14 +- builtin/credential/aws/path_config_client.go | 27 +- .../credential/aws/path_config_client_test.go | 10 +- builtin/credential/aws/path_login.go | 295 +++++++++--------- builtin/credential/aws/path_login_test.go | 34 +- builtin/credential/aws/path_role.go | 68 ++-- builtin/credential/aws/path_role_test.go | 17 +- website/source/docs/auth/aws-ec2.html.md | 20 +- 9 files changed, 244 insertions(+), 257 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 88ef8d3db709..64a6100c6fd9 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1092,7 +1092,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. // place the correct IAM role ARN, but make the auth type wrong data["bound_iam_role_arn"] = iamARN data["bound_iam_principal_arn"] = iamARN - data["allowed_auth_types"] = "iam" + data["allowed_auth_types"] = iamAuthType resp, err = b.HandleRequest(roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1106,7 +1106,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. // Place the correct auth type delete(data, "bound_iam_principal_arn") - data["allowed_auth_types"] = "ec2" + data["allowed_auth_types"] = ec2AuthType resp, err = b.HandleRequest(roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1293,11 +1293,11 @@ func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[s return nil, err } return map[string]interface{}{ - "request_method": request.Method, - "request_url": request.URL.String(), - "request_headers": base64.StdEncoding.EncodeToString(headersJson), - "request_body": base64.StdEncoding.EncodeToString(requestBody), - "request_role": roleName, + "iam_http_request_method": request.Method, + "iam_request_url": request.URL.String(), + "iam_request_headers": base64.StdEncoding.EncodeToString(headersJson), + "iam_request_body": base64.StdEncoding.EncodeToString(requestBody), + "request_role": roleName, }, nil } @@ -1395,7 +1395,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { roleData := map[string]interface{}{ "bound_iam_principal_arn": testIdentityArn, "policies": "root", - "allowed_auth_types": "iam", + "allowed_auth_types": iamAuthType, } roleRequest := &logical.Request{ Operation: logical.CreateOperation, diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 65761d8a5292..594b792955b7 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -82,11 +82,11 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { // And pass them on to the Vault server path := fmt.Sprintf("auth/%s/login", mount) secret, err := c.Logical().Write(path, map[string]interface{}{ - "request_method": method, - "request_url": targetUrl, - "request_headers": headers, - "request_body": body, - "role": role, + "iam_http_request_method": method, + "iam_request_url": targetUrl, + "iam_request_headers": headers, + "iam_request_body": body, + "role": role, }) if err != nil { @@ -97,8 +97,6 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { } return secret.Auth.ClientToken, nil - - return "", nil } func (h *CLIHandler) Help() string { @@ -123,7 +121,7 @@ Key/Value Pairs: aws_access_key_id= Explicitly specified AWS access key aws_secret_access_key= Explicitly specified AWS secret key aws_security_token= Security token for temporary credentials - header_value The Value of the X-Vault-AWSIAM-Server-ID header. + header_value The Value of the X-Vault-AWS-IAM-Server-ID header. role The name of the role you're requesting a token for ` diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 72cd25db159c..2c6e1a6ca70e 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -43,7 +43,7 @@ func pathConfigClient(b *backend) *framework.Path { "iam_server_id_header_value": &framework.FieldSchema{ Type: framework.TypeString, Default: "", - Description: "Value to require in the X-Vault-AWSIAM-Server-ID request header", + Description: "Value to require in the X-Vault-AWS-IAM-Server-ID request header", }, }, @@ -193,7 +193,12 @@ func (b *backend) pathConfigClientCreateUpdate( stsEndpointStr, ok := data.GetOk("sts_endpoint") if ok { if configEntry.STSEndpoint != stsEndpointStr.(string) { - // NOT setting changedCreds here, since this isn't really cached + // We don't directly cache STS clients as they are ever directly used. + // However, they are potentially indirectly used as credential providers + // for the EC2 and IAM clients, and thus we would be indirectly caching + // them there. So, if we change the STS endpoint, we should flush those + // cached clients. + changedCreds = true configEntry.STSEndpoint = stsEndpointStr.(string) } } else if req.Operation == logical.CreateOperation { @@ -202,12 +207,12 @@ func (b *backend) pathConfigClientCreateUpdate( headerValStr, ok := data.GetOk("iam_server_id_header_value") if ok { - if configEntry.HeaderValue != headerValStr.(string) { + if configEntry.IAMServerIdHeaderValue != headerValStr.(string) { // NOT setting changedCreds here, since this isn't really cached - configEntry.HeaderValue = headerValStr.(string) + configEntry.IAMServerIdHeaderValue = headerValStr.(string) } } else if req.Operation == logical.CreateOperation { - configEntry.HeaderValue = data.Get("iam_server_id_header_value").(string) + configEntry.IAMServerIdHeaderValue = data.Get("iam_server_id_header_value").(string) } // Since this endpoint supports both create operation and update operation, @@ -235,12 +240,12 @@ func (b *backend) pathConfigClientCreateUpdate( // Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to // interact with the AWS EC2 API. type clientConfig struct { - AccessKey string `json:"access_key" structs:"access_key" mapstructure:"access_key"` - SecretKey string `json:"secret_key" structs:"secret_key" mapstructure:"secret_key"` - Endpoint string `json:"endpoint" structs:"endpoint" mapstructure:"endpoint"` - IAMEndpoint string `json:"iam_endpoint" structs:"iam_endpoint" mapstructure:"iam_endpoint"` - STSEndpoint string `json:"sts_endpoint" structs:"sts_endpoint" mapstructure:"sts_endpoint"` - HeaderValue string `json:"vault_header_value" structs:"vault_header_value" mapstructure:"vault_header_value"` + AccessKey string `json:"access_key" structs:"access_key" mapstructure:"access_key"` + SecretKey string `json:"secret_key" structs:"secret_key" mapstructure:"secret_key"` + Endpoint string `json:"endpoint" structs:"endpoint" mapstructure:"endpoint"` + IAMEndpoint string `json:"iam_endpoint" structs:"iam_endpoint" mapstructure:"iam_endpoint"` + STSEndpoint string `json:"sts_endpoint" structs:"sts_endpoint" mapstructure:"sts_endpoint"` + IAMServerIdHeaderValue string `json:"iam_server_id_header_value" structs:"iam_server_id_header_value" mapstructure:"iam_server_id_header_value"` } const pathConfigClientHelpSyn = ` diff --git a/builtin/credential/aws/path_config_client_test.go b/builtin/credential/aws/path_config_client_test.go index 48f9943ba5fa..268571024041 100644 --- a/builtin/credential/aws/path_config_client_test.go +++ b/builtin/credential/aws/path_config_client_test.go @@ -31,12 +31,12 @@ func TestBackend_pathConfigClient(t *testing.T) { t.Fatal(err) } // at this point, resp == nil is valid as no client config exists - // if resp != nil, then resp.Data must have EndPoint and HeaderValue as nil + // if resp != nil, then resp.Data must have EndPoint and IAMServerIdHeaderValue as nil if resp != nil { if resp.IsError() { t.Fatalf("failed to read client config entry") - } else if resp.Data["endpoint"] != nil || resp.Data["vault_header_value"] != nil { - t.Fatalf("returned endpoint or vault_header_value non-nil") + } else if resp.Data["endpoint"] != nil || resp.Data["iam_server_id_header_value"] != nil { + t.Fatalf("returned endpoint or iam_server_id_header_value non-nil") } } @@ -69,8 +69,8 @@ func TestBackend_pathConfigClient(t *testing.T) { if resp == nil || resp.IsError() { t.Fatal("failed to read the client config entry") } - if resp.Data["iam_server_id_header_value"] != data["vault_header_value"] { - t.Fatalf("expected vault_header_value: '%#v'; returned vault_header_value: '%#v'", + if resp.Data["iam_server_id_header_value"] != data["iam_server_id_header_value"] { + t.Fatalf("expected iam_server_id_header_value: '%#v'; returned iam_server_id_header_value: '%#v'", data["iam_server_id_header_value"], resp.Data["iam_server_id_header_value"]) } } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index b603e04de618..030f57f0dafd 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -28,6 +28,9 @@ import ( const ( reauthenticationDisabledNonce = "reauthentication-disabled-nonce" + iamAuthType = "iam" + ec2AuthType = "ec2" + ec2EntityType = "ec2_instance" ) func pathLogin(b *backend) *framework.Path { @@ -63,26 +66,26 @@ option is enabled on either the role or the role tag, the 'nonce' holds no significance.`, }, - "request_method": { + "iam_http_request_method": { 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`, }, - "request_url": { + "iam_request_url": { Type: framework.TypeString, Description: `Full URL against which to make the AWS request when auth_type is iam. If using a POST request with the action specified in the body, this should just be "/".`, }, - "request_body": { + "iam_request_body": { Type: framework.TypeString, Description: `Base64-encoded request body when auth_type is iam. This must match the request body included in the signature.`, }, - "request_headers": { + "iam_request_headers": { Type: framework.TypeString, Description: `Base64-encoded JSON representation of the request headers when auth_type is iam. This must at a minimum include the headers over @@ -114,6 +117,9 @@ needs to be supplied along with 'identity' parameter.`, // instanceIamRoleARN fetches the IAM role ARN associated with the given // instance profile name func (b *backend) instanceIamRoleARN(iamClient *iam.IAM, instanceProfileName string) (string, error) { + if iamClient == nil { + return "", fmt.Errorf("nil iamClient") + } if instanceProfileName == "" { return "", fmt.Errorf("missing instance profile name") } @@ -378,7 +384,38 @@ func (b *backend) pathLoginUpdate( // Returns whether the EC2 instance meets the requirements of the particular // AWS role entry. func (b *backend) verifyInstanceMeetsRoleRequirements( - s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, iamClient *iam.IAM) (*roleTagLoginResponse, string, error) { + s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, identityDoc *identityDocument) (*roleTagLoginResponse, error, error) { + + switch { + case instance == nil: + return nil, nil, fmt.Errorf("nil instance") + case roleEntry == nil: + return nil, nil, fmt.Errorf("nil roleEntry") + case identityDoc == nil: + return nil, nil, fmt.Errorf("nil identityDoc") + } + + // Verify that the AccountID of the instance trying to login matches the + // AccountID specified as a constraint on role + if roleEntry.BoundAccountID != "" && identityDoc.AccountID != roleEntry.BoundAccountID { + return nil, fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil + } + + // Check if an STS configuration exists for the AWS account + sts, err := b.lockedAwsStsEntry(s, identityDoc.AccountID) + if err != nil { + return nil, fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil + } + // An empty STS role signifies the master account + stsRole := "" + if sts != nil { + stsRole = sts.StsRole + } + + iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole) + if err != nil { + iamClient = nil + } // Verify that the AMI ID of the instance trying to login matches the // AMI ID specified as a constraint on the role. @@ -389,29 +426,32 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // already calling the API to validate the Instance ID anyway, so it shouldn't // matter. The benefit is that we have the exact same code whether auth_type // is ec2 or iam. - if roleEntry.BoundAmiID != "" && *instance.ImageId != roleEntry.BoundAmiID { - return nil, fmt.Sprintf("AMI ID '%s' does not belong to role '%s'", instance.ImageId, roleName), nil + if roleEntry.BoundAmiID != "" { + if instance.ImageId == nil { + return nil, nil, fmt.Errorf("AMI ID in the instance description is nil") + } + if roleEntry.BoundAmiID != *instance.ImageId { + return nil, fmt.Errorf("AMI ID %q does not belong to role %q", instance.ImageId, roleName), nil + } } // Validate the SubnetID if corresponding bound was set on the role if roleEntry.BoundSubnetID != "" { - subnetIDPtr := instance.SubnetId - if subnetIDPtr == nil { - return nil, "", fmt.Errorf("Subnet ID in the instance description is nil") + if instance.SubnetId == nil { + return nil, nil, fmt.Errorf("subnet ID in the instance description is nil") } - if roleEntry.BoundSubnetID != *subnetIDPtr { - return nil, fmt.Sprintf("Subnet ID %q does not satisfy the constraint on role %q", *subnetIDPtr, roleName), nil + if roleEntry.BoundSubnetID != *instance.SubnetId { + return nil, fmt.Errorf("subnet ID %q does not satisfy the constraint on role %q", *instance.SubnetId, roleName), nil } } // Validate the VpcID if corresponding bound was set on the role if roleEntry.BoundVpcID != "" { - vpcIDPtr := instance.VpcId - if vpcIDPtr == nil { - return nil, "", fmt.Errorf("VPC ID in the instance description is nil") + if instance.VpcId == nil { + return nil, nil, fmt.Errorf("VPC ID in the instance description is nil") } - if roleEntry.BoundVpcID != *vpcIDPtr { - return nil, fmt.Sprintf("VPC ID %q does not satisfy the constraint on role %q", *vpcIDPtr, roleName), nil + if roleEntry.BoundVpcID != *instance.VpcId { + return nil, fmt.Errorf("VPC ID %q does not satisfy the constraint on role %q", *instance.VpcId, roleName), nil } } @@ -420,14 +460,14 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // on the role if roleEntry.BoundIamInstanceProfileARN != "" { if instance.IamInstanceProfile == nil { - return nil, "", fmt.Errorf("IAM instance profile in the instance description is nil") + return nil, nil, fmt.Errorf("IAM instance profile in the instance description is nil") } if instance.IamInstanceProfile.Arn == nil { - return nil, "", fmt.Errorf("IAM instance profile ARN in the instance description is nil") + return nil, nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") } iamInstanceProfileARN := *instance.IamInstanceProfile.Arn if !strings.HasPrefix(iamInstanceProfileARN, roleEntry.BoundIamInstanceProfileARN) { - return nil, fmt.Sprintf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil + return nil, fmt.Errorf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil } } @@ -435,17 +475,17 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // the IAM role ARN specified as a constraint on the role. if roleEntry.BoundIamRoleARN != "" { if instance.IamInstanceProfile == nil { - return nil, "", fmt.Errorf("IAM instance profile in the instance description is nil") + return nil, nil, fmt.Errorf("IAM instance profile in the instance description is nil") } if instance.IamInstanceProfile.Arn == nil { - return nil, "", fmt.Errorf("IAM instance profile ARN in the instance description is nil") + return nil, nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") } // Fetch the instance profile ARN from the instance description iamInstanceProfileARN := *instance.IamInstanceProfile.Arn if iamInstanceProfileARN == "" { - return nil, "", fmt.Errorf("IAM instance profile ARN in the instance description is empty") + return nil, nil, fmt.Errorf("IAM instance profile ARN in the instance description is empty") } // Extract out the instance profile name from the instance @@ -454,38 +494,38 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1] if iamInstanceProfileName == "" { - return nil, "", fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") + return nil, nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") } // Use instance profile ARN to fetch the associated role ARN if iamClient == nil { - return nil, "", fmt.Errorf("Could not fetch IAM client") + return nil, nil, fmt.Errorf("could not fetch IAM client") } iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName) if err != nil { - return nil, "", fmt.Errorf("IAM role ARN could not be fetched: %v", err) + return nil, nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err) } if iamRoleARN == "" { - return nil, "", fmt.Errorf("IAM role ARN could not be fetched") + return nil, nil, fmt.Errorf("IAM role ARN could not be fetched") } if !strings.HasPrefix(iamRoleARN, roleEntry.BoundIamRoleARN) { - return nil, fmt.Sprintf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil + return nil, fmt.Errorf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil } } - var roleTagResp *roleTagLoginResponse = nil + var roleTagResp *roleTagLoginResponse if roleEntry.RoleTag != "" { roleTagResp, err := b.handleRoleTagLogin(s, roleName, roleEntry, instance) if err != nil { - return nil, "", err + return nil, nil, err } if roleTagResp == nil { - return nil, "failed to fetch and verify the role tag", nil + return nil, fmt.Errorf("failed to fetch and verify the role tag"), nil } } - return roleTagResp, "", nil + return roleTagResp, nil, nil } // pathLoginUpdateEc2 is used to create a Vault token by the EC2 instances @@ -551,14 +591,6 @@ func (b *backend) pathLoginUpdateEc2( roleName = identityDocParsed.AmiID } - // Validate the instance ID by making a call to AWS EC2 DescribeInstances API - // and fetching the instance description. Validation succeeds only if the - // instance is in 'running' state. - instance, err := b.validateInstance(req.Storage, identityDocParsed.InstanceID, identityDocParsed.Region, identityDocParsed.AccountID) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %v", err)), nil - } - // Get the entry for the role used by the instance roleEntry, err := b.lockedAWSRole(req.Storage, roleName) if err != nil { @@ -568,14 +600,16 @@ func (b *backend) pathLoginUpdateEc2( return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil } - if !roleAllowsAuthMethod("ec2", roleEntry) { + if !strutil.StrListContains(roleEntry.AllowedAuthTypes, ec2AuthType) { return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil } - // Verify that the AccountID of the instance trying to login matches the - // AccountID specified as a constraint on role - if roleEntry.BoundAccountID != "" && identityDocParsed.AccountID != roleEntry.BoundAccountID { - return logical.ErrorResponse(fmt.Sprintf("Account ID %q does not belong to role %q", identityDocParsed.AccountID, roleName)), nil + // Validate the instance ID by making a call to AWS EC2 DescribeInstances API + // and fetching the instance description. Validation succeeds only if the + // instance is in 'running' state. + instance, err := b.validateInstance(req.Storage, identityDocParsed.InstanceID, identityDocParsed.Region, identityDocParsed.AccountID) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %v", err)), nil } // Verify that the `Region` of the instance trying to login matches the @@ -584,28 +618,12 @@ func (b *backend) pathLoginUpdateEc2( return logical.ErrorResponse(fmt.Sprintf("Region %q does not satisfy the constraint on role %q", identityDocParsed.Region, roleName)), nil } - // Check if an STS configuration exists for the AWS account - sts, err := b.lockedAwsStsEntry(req.Storage, identityDocParsed.AccountID) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("error fetching STS config for account ID %q: %q\n", identityDocParsed.AccountID, err)), nil - } - // An empty STS role signifies the master account - stsRole := "" - if sts != nil { - stsRole = sts.StsRole - } - - iamClient, err := b.clientIAM(req.Storage, identityDocParsed.Region, stsRole) - if err != nil { - iamClient = nil - } - - roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, iamClient) + roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDocParsed) if err != nil { return nil, err } - if validationError != "" { - return logical.ErrorResponse(validationError), nil + if validationError != nil { + return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil } // Get the entry from the identity whitelist, if there is one @@ -878,15 +896,15 @@ func (b *backend) pathLoginRenew( authType, ok := req.Auth.Metadata["auth_type"] if !ok { // backwards compatibility for clients that have leases from before we added auth_type - authType = "ec2" + authType = ec2AuthType } - if authType == "ec2" { + if authType == ec2AuthType { return b.pathLoginRenewEc2(req, data) - } else if authType == "iam" { + } else if authType == iamAuthType { return b.pathLoginRenewIam(req, data) } else { - return nil, fmt.Errorf("unrecognized auth_type: '%s'", authType) + return nil, fmt.Errorf("unrecognized auth_type: %q", authType) } } @@ -909,23 +927,23 @@ func (b *backend) pathLoginRenewIam( return nil, fmt.Errorf("role entry not found") } - if entityType, ok := req.Auth.Metadata["inferredEntityType"]; !ok { - if entityType == "ec2_instance" { - instanceID, ok := req.Auth.Metadata["inferredEntityId"] + if entityType, ok := req.Auth.Metadata["inferred_entity_type"]; !ok { + if entityType == ec2EntityType { + instanceID, ok := req.Auth.Metadata["inferred_entity_id"] if !ok { return nil, fmt.Errorf("no inferred entity ID in auth metadata") } _, err := b.validateInstance(req.Storage, instanceID, roleEntry.InferredAWSRegion, req.Auth.Metadata["accountID"]) if err != nil { - return nil, fmt.Errorf("failed to verify instance ID '%s': %s", instanceID, err) + return nil, fmt.Errorf("failed to verify instance ID %q: %s", instanceID, err) } } else { - return nil, fmt.Errorf("unrecognized entity_type in metadata: '%s'", entityType) + return nil, fmt.Errorf("unrecognized entity_type in metadata: %q", entityType) } } if roleEntry.BoundIamPrincipalARN != canonicalArn { - return nil, fmt.Errorf("role no longer bound to arn '%s'", canonicalArn) + return nil, fmt.Errorf("role no longer bound to arn %q", canonicalArn) } return framework.LeaseExtend(roleEntry.TTL, roleEntry.MaxTTL, b.System())(req, data) @@ -1033,53 +1051,51 @@ func (b *backend) pathLoginRenewEc2( func (b *backend) pathLoginUpdateIam( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - // BEGIN boring data parsing - method := data.Get("request_method").(string) + method := data.Get("iam_http_request_method").(string) if method == "" { return logical.ErrorResponse("missing method"), nil } // In the future, might consider supporting GET if method != "POST" { - return logical.ErrorResponse("invalid request_method; currently only 'POST' is supported"), nil + return logical.ErrorResponse("invalid iam_http_request_method; currently only 'POST' is supported"), nil } - rawUrl := data.Get("request_url").(string) + rawUrl := data.Get("iam_request_url").(string) if rawUrl == "" { - return logical.ErrorResponse("missing request_url"), nil + return logical.ErrorResponse("missing iam_request_url"), nil } parsedUrl, err := url.Parse(rawUrl) if err != nil { - return logical.ErrorResponse("error parsing request_url"), nil + return logical.ErrorResponse("error parsing iam_request_url"), 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("request_body").(string) + bodyB64 := data.Get("iam_request_body").(string) if bodyB64 == "" { - return logical.ErrorResponse("missing request_body"), nil + return logical.ErrorResponse("missing iam_request_body"), nil } bodyRaw, err := base64.StdEncoding.DecodeString(bodyB64) if err != nil { - return logical.ErrorResponse("request_body is invalid base64"), nil + return logical.ErrorResponse("failed to base64 decode iam_request_body"), nil } body := string(bodyRaw) - headersB64 := data.Get("request_headers").(string) + headersB64 := data.Get("iam_request_headers").(string) if headersB64 == "" { - return logical.ErrorResponse("missing request_headers"), nil + return logical.ErrorResponse("missing iam_request_headers"), nil } headersJson, err := base64.StdEncoding.DecodeString(headersB64) if err != nil { - return logical.ErrorResponse("request_headers is invalid base64"), nil + return logical.ErrorResponse("failed to base64 decode iam_request_headers"), nil } var headers http.Header err = jsonutil.DecodeJSON(headersJson, &headers) if err != nil { - return logical.ErrorResponse(fmt.Sprintf("request_headers '%s' is invalid JSON: %s", headersJson, err)), nil + return logical.ErrorResponse(fmt.Sprintf("failed to JSON decode iam_request_headers %q: %s", headersJson, err)), nil } - // END boring data parsing config, err := b.lockedClientConfigEntry(req.Storage) if err != nil { @@ -1089,10 +1105,10 @@ func (b *backend) pathLoginUpdateIam( endpoint := "https://sts.amazonaws.com" if config != nil { - if config.HeaderValue != "" { - ok, msg := ensureVaultHeaderValue(headers, parsedUrl, config.HeaderValue) - if !ok { - return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %s", iamServerIdHeader, msg)), nil + if config.IAMServerIdHeaderValue != "" { + err = validateVaultHeaderValue(headers, parsedUrl, config.IAMServerIdHeaderValue) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %s", iamServerIdHeader, err)), nil } } if config.STSEndpoint != "" { @@ -1106,7 +1122,7 @@ func (b *backend) pathLoginUpdateIam( } canonicalArn, principalName, sessionName, err := parseIamArn(clientArn) if err != nil { - return logical.ErrorResponse("unrecognized IAM principal type"), nil + return logical.ErrorResponse(fmt.Sprintf("Error parsing arn: %s", err)), nil } roleName := data.Get("role").(string) @@ -1122,14 +1138,14 @@ func (b *backend) pathLoginUpdateIam( return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil } - if !roleAllowsAuthMethod("iam", roleEntry) { + if !strutil.StrListContains(roleEntry.AllowedAuthTypes, iamAuthType) { return logical.ErrorResponse(fmt.Sprintf("auth method iam not allowed for role %s", roleName)), nil } // The role creation should ensure that either we're inferring this is an EC2 instance // or that we're binding an ARN if roleEntry.BoundIamPrincipalARN != "" && roleEntry.BoundIamPrincipalARN != canonicalArn { - return logical.ErrorResponse(fmt.Sprintf("IAM Principal '%s' does not belong to the role '%s'", clientArn, roleName)), nil + return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", clientArn, roleName)), nil } policies := roleEntry.Policies @@ -1137,27 +1153,28 @@ func (b *backend) pathLoginUpdateIam( inferredEntityType := "" inferredEntityId := "" - if roleEntry.RoleInferredType == "ec2_instance" { + if roleEntry.RoleInferredType == ec2EntityType { instance, err := b.validateInstance(req.Storage, sessionName, roleEntry.InferredAWSRegion, accountID) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", sessionName, roleEntry.InferredAWSRegion)), nil } - // It's a bit of a hack to use the inferred "EC2" region to get a region for the IAM client - // IAM is a "global" service and so doesn't really have a region, so it wouldn't matter - // Except that the region is used to infer the partition (i.e., aws, aws-cn, or aws-us-gov), - // and we can safely assume that the EC2 client will be in the same partition as IAM - iamClient, err := b.clientIAM(req.Storage, roleEntry.InferredAWSRegion, accountID) - if err != nil { - iamClient = nil + // build a fake identity doc to pass on metadata about the instance to verifyInstanceMeetsRoleRequirements + identityDoc := &identityDocument{ + Tags: nil, // Don't really need the tags, so not doing the work of converting them from Instance.Tags to identityDocument.Tags + InstanceID: *instance.InstanceId, + AmiID: *instance.ImageId, + AccountID: accountID, + Region: roleEntry.InferredAWSRegion, + PendingTime: instance.LaunchTime.Format(time.RFC3339), } - roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, iamClient) + roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDoc) if err != nil { return nil, err } - if validationError != "" { - return logical.ErrorResponse(validationError), nil + if validationError != nil { + return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil } if roleTagResp != nil { @@ -1168,7 +1185,7 @@ func (b *backend) pathLoginUpdateIam( rTagMaxTTL = roleTagResp.MaxTTL } - inferredEntityType = "ec2_instance" + inferredEntityType = ec2EntityType inferredEntityId = sessionName } @@ -1176,13 +1193,13 @@ func (b *backend) pathLoginUpdateIam( Auth: &logical.Auth{ Policies: policies, Metadata: map[string]string{ - "client_arn": clientArn, - "canonical_arn": canonicalArn, - "auth_type": "iam", - "role_tag_max_ttl": rTagMaxTTL.String(), - "inferredEntityType": inferredEntityType, - "inferredEntityId": inferredEntityId, - "account_id": accountID, + "client_arn": clientArn, + "canonical_arn": canonicalArn, + "auth_type": iamAuthType, + "role_tag_max_ttl": rTagMaxTTL.String(), + "inferred_entity_type": inferredEntityType, + "inferred_entity_id": inferredEntityId, + "account_id": accountID, }, InternalData: map[string]interface{}{ "role_name": roleName, @@ -1235,31 +1252,40 @@ func hasValuesForEc2Auth(data *framework.FieldData) (bool, bool) { } func hasValuesForIamAuth(data *framework.FieldData) (bool, bool) { - _, hasRequestMethod := data.GetOk("request_method") - _, hasRequestUrl := data.GetOk("request_url") - _, hasRequestBody := data.GetOk("request_body") - _, hasRequestHeaders := data.GetOk("request_headers") + _, 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), (hasRequestMethod || hasRequestUrl || hasRequestBody || hasRequestHeaders) } func parseIamArn(iamArn string) (string, string, string, error) { + // iamArn should look like one of the following: + // 1. arn:aws:iam:::user/ + // 2. arn:aws:sts:::assumed-role// + // if we get something like 2, then we want to transform that back to what + // most people would expect, which is arn;aws:iam:::role/ fullParts := strings.Split(iamArn, ":") principalFullName := fullParts[5] + // principalFullName would now be something like user/ or assumed-role// parts := strings.Split(principalFullName, "/") principalName := parts[1] + // now, principalName should either be or transformedArn := iamArn sessionName := "" if parts[0] == "assumed-role" { transformedArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", fullParts[4], principalName) + // fullParts[4] is the sessionName = parts[2] + // sessionName is } else if parts[0] != "user" { - return "", "", "", fmt.Errorf("unrecognized principal type: '%s'", parts[0]) + return "", "", "", fmt.Errorf("unrecognized principal type: %q", parts[0]) } return transformedArn, principalName, sessionName, nil } -func ensureVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) (bool, string) { +func validateVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) error { providedValue := "" for k, v := range headers { if strings.ToLower(iamServerIdHeader) == strings.ToLower(k) { @@ -1268,12 +1294,12 @@ func ensureVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHe } } if providedValue == "" { - return false, fmt.Sprintf("didn't find %s", iamServerIdHeader) + return fmt.Errorf("didn't find %s", iamServerIdHeader) } // NOT doing a constant time compare here since the value is NOT intended to be secret if providedValue != requiredHeaderValue { - return false, fmt.Sprintf("expected %s but got %s", requiredHeaderValue, providedValue) + return fmt.Errorf("expected %s but got %s", requiredHeaderValue, providedValue) } if authzHeaders, ok := headers["Authorization"]; ok { @@ -1283,17 +1309,17 @@ func ensureVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHe authzHeader := strings.Join(authzHeaders, ",") matches := re.FindSubmatch([]byte(authzHeader)) if len(matches) < 1 { - return false, "vault header wasn't signed" + return fmt.Errorf("vault header wasn't signed") } if len(matches) > 2 { - return false, "found multiple SignedHeaders components" + return fmt.Errorf("found multiple SignedHeaders components") } signedHeaders := string(matches[1]) return ensureHeaderIsSigned(signedHeaders, iamServerIdHeader) } // 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 false, "Missing Authorization header" + return fmt.Errorf("missing Authorization header") } func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) *http.Request { @@ -1342,14 +1368,14 @@ func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, return request } -func ensureHeaderIsSigned(signedHeaders, headerToSign string) (bool, string) { +func ensureHeaderIsSigned(signedHeaders, headerToSign string) error { // Not doing a constant time compare here, the values aren't secret for _, header := range strings.Split(signedHeaders, ";") { if header == strings.ToLower(headerToSign) { - return true, "" + return nil } } - return false, fmt.Sprintf("Vault header wasn't signed") + return fmt.Errorf("vault header wasn't signed") } func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, error) { @@ -1359,17 +1385,6 @@ func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, return result, err } -func roleAllowsAuthMethod(authMethod string, roleEntry *awsRoleEntry) bool { - allowedAuthMethod := false - for _, allowedAuthType := range roleEntry.AllowedAuthTypes { - if allowedAuthType == authMethod { - allowedAuthMethod = true - break - } - } - return allowedAuthMethod -} - func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) (string, string, error) { // NOTE: We need to ensure we're calling STS, instead of acting as an unintended network proxy // The protection against this is that this method will only call the endpoint specified in the @@ -1435,7 +1450,7 @@ type roleTagLoginResponse struct { DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } -const iamServerIdHeader = "X-Vault-AWSIAM-Server-Id" +const iamServerIdHeader = "X-Vault-AWS-IAM-Server-ID" const pathLoginSyn = ` Authenticates an EC2 instance with Vault. diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 14f168030103..4a7cc6aeddb6 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -81,7 +81,7 @@ func TestBackend_pathLogin_parseIamArn(t *testing.T) { } } -func TestBackend_ensureVaultHeaderValue(t *testing.T) { +func TestBackend_validateVaultHeaderValue(t *testing.T) { const canaryHeaderValue = "Vault-Server" requestUrl, err := url.Parse("https://sts.amazonaws.com/") if err != nil { @@ -89,12 +89,12 @@ func TestBackend_ensureVaultHeaderValue(t *testing.T) { } postHeadersMissing := http.Header{ "Host": []string{"Foo"}, - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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"}, } postHeadersInvalid := http.Header{ "Host": []string{"Foo"}, iamServerIdHeader: []string{"InvalidValue"}, - "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-vault-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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"}, } postHeadersUnsigned := http.Header{ "Host": []string{"Foo"}, @@ -104,37 +104,37 @@ func TestBackend_ensureVaultHeaderValue(t *testing.T) { postHeadersValid := 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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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-awsiam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + "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"}, } - found, errMsg := ensureVaultHeaderValue(postHeadersMissing, requestUrl, canaryHeaderValue) - if found { + err = validateVaultHeaderValue(postHeadersMissing, requestUrl, canaryHeaderValue) + if err == nil { t.Error("validated POST request with missing Vault header") } - found, errMsg = ensureVaultHeaderValue(postHeadersInvalid, requestUrl, canaryHeaderValue) - if found { + err = validateVaultHeaderValue(postHeadersInvalid, requestUrl, canaryHeaderValue) + if err == nil { t.Error("validated POST request with invalid Vault header value") } - found, errMsg = ensureVaultHeaderValue(postHeadersUnsigned, requestUrl, canaryHeaderValue) - if found { + err = validateVaultHeaderValue(postHeadersUnsigned, requestUrl, canaryHeaderValue) + if err == nil { t.Error("validated POST request with unsigned Vault header") } - found, errMsg = ensureVaultHeaderValue(postHeadersValid, requestUrl, canaryHeaderValue) - if !found { - t.Errorf("did NOT validate valid POST request: %s", errMsg) + err = validateVaultHeaderValue(postHeadersValid, requestUrl, canaryHeaderValue) + if err != nil { + t.Errorf("did NOT validate valid POST request: %s", err) } - found, errMsg = ensureVaultHeaderValue(postHeadersSplit, requestUrl, canaryHeaderValue) - if !found { - t.Errorf("did NOT validate valid POST request with split Authorization header: %s", errMsg) + err = validateVaultHeaderValue(postHeadersSplit, requestUrl, canaryHeaderValue) + if err != nil { + t.Errorf("did NOT validate valid POST request with split Authorization header: %s", err) } } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 5ba7b557e5ac..8e1c3f056b7a 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -8,6 +8,7 @@ import ( "github.com/fatih/structs" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -22,7 +23,7 @@ func pathRole(b *backend) *framework.Path { }, "allowed_auth_types": { Type: framework.TypeString, - Default: "ec2", + Default: ec2AuthType, Description: `The comma-separated list of allowed auth_type values that are allowed to authenticate to this role.`, }, @@ -65,7 +66,7 @@ the value specified by this parameter. The value is prefix-matched (as though it were a glob ending in '*'). This is only checked when auth_type is ec2.`, }, - "role_inferred_type": { + "inferred_entity_type": { Type: framework.TypeString, Description: `When auth_type is iam, the AWS entity type to infer from the authenticated principal. The only supported @@ -79,7 +80,7 @@ among running instances, then authentication will fail.`, "inferred_aws_region": { Type: framework.TypeString, Description: `When auth_type is iam and -role_inferred_type is set, the region to assume the inferred entity exists in.`, +inferred_entity_type is set, the region to assume the inferred entity exists in.`, }, "bound_vpc_id": { Type: framework.TypeString, @@ -101,7 +102,7 @@ field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using 'role//tag' endpoint. Defaults to an empty string, meaning that role tags are disabled. This is only checked if auth_type is ec2 or -role_inferred_type is ec2_instance`, +inferred_entity_type is ec2_instance`, }, "period": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -285,7 +286,7 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole // Check if there was no pre-existing AllowedAuthTypes set (from older versions) if len(result.AllowedAuthTypes) == 0 { // then default to the original behavior of ec2 - result.AllowedAuthTypes = []string{"ec2"} + result.AllowedAuthTypes = []string{ec2AuthType} // and save the result if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { return nil, fmt.Errorf("failed to save default allowed_auth_types") @@ -403,7 +404,7 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.BoundIamPrincipalARN = boundIamPrincipalARNRaw.(string) } - if inferRoleTypeRaw, ok := data.GetOk("role_inferred_type"); ok { + if inferRoleTypeRaw, ok := data.GetOk("inferred_entity_type"); ok { roleEntry.RoleInferredType = inferRoleTypeRaw.(string) } @@ -413,49 +414,20 @@ func (b *backend) pathRoleCreateUpdate( var allowEc2Auth, allowIamAuth bool - parseAllowedAuthTypes := func(input string) string { - allowedAuthTypes := []string{} - for _, t := range strings.Split(input, ",") { - switch t { - case "ec2": - allowedAuthTypes = append(allowedAuthTypes, t) - case "iam": - allowedAuthTypes = append(allowedAuthTypes, t) - case "": - default: - return fmt.Sprintf("unrecognized auth type: '%s'", t) - } - } - roleEntry.AllowedAuthTypes = allowedAuthTypes - return "" - } - if allowedAuthTypesRaw, ok := data.GetOk("allowed_auth_types"); ok { - err := parseAllowedAuthTypes(allowedAuthTypesRaw.(string)) - if err != "" { - return logical.ErrorResponse(err), nil - } + roleEntry.AllowedAuthTypes = strutil.ParseDedupAndSortStrings(allowedAuthTypesRaw.(string), ",") } else if req.Operation == logical.CreateOperation { - err := parseAllowedAuthTypes(data.Get("allowed_auth_types").(string)) - if err != "" { - return logical.ErrorResponse(err), nil - } + roleEntry.AllowedAuthTypes = strutil.ParseDedupAndSortStrings(data.Get("allowed_auth_types").(string), ",") } - // Reparse the existing roleEntry.AllowedAuthTypes - // We need to do this to support the use case where an existing role is being updated, and - // allowed_auth_types isn't being updated as part of the role. We need to make sure that any - // new bindings requested are still valid bindings. For example, let's say a role is created - // with an auth_type of iam and no inferencing is configured; then, in a subsequent role update, - // bound_ami_id is specified. This is how that edge case is caught. for _, t := range roleEntry.AllowedAuthTypes { switch t { - case "ec2": + case ec2AuthType: allowEc2Auth = true - case "iam": + case iamAuthType: allowIamAuth = true default: - return nil, fmt.Errorf("Unrecognized auth_type in roleEntry: %s", t) + return nil, fmt.Errorf("Unrecognized auth_type in roleEntry: %q", t) } } @@ -464,15 +436,15 @@ func (b *backend) pathRoleCreateUpdate( if roleEntry.RoleInferredType != "" { switch { case !allowIamAuth: - return logical.ErrorResponse("specified role_inferred_type but didn't allow iam auth_type"), nil - case roleEntry.RoleInferredType != "ec2_instance": - return logical.ErrorResponse(fmt.Sprintf("specified invalid role_inferred_type: %s", roleEntry.RoleInferredType)), nil + return logical.ErrorResponse("specified inferred_entity_type but didn't allow iam auth_type"), nil + case roleEntry.RoleInferredType != ec2EntityType: + return logical.ErrorResponse(fmt.Sprintf("specified invalid inferred_entity_type: %s", roleEntry.RoleInferredType)), nil case roleEntry.InferredAWSRegion == "": - return logical.ErrorResponse("specified role_inferred_type but not inferred_aws_region"), nil + return logical.ErrorResponse("specified inferred_entity_type but not inferred_aws_region"), nil } allowEc2Binds = true } else if roleEntry.InferredAWSRegion != "" { - return logical.ErrorResponse("specified inferred_aws_region but not role_inferred_type"), nil + return logical.ErrorResponse("specified inferred_aws_region but not inferred_entity_type"), nil } numBinds := 0 @@ -493,21 +465,21 @@ func (b *backend) pathRoleCreateUpdate( if roleEntry.BoundAmiID != "" { if !allowEc2Binds { - return logical.ErrorResponse("specified bound_ami_id but not allowing ec2 auth_type or inferring ec2_instance"), nil + return logical.ErrorResponse(fmt.Sprintf("specified bound_ami_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } if roleEntry.BoundIamInstanceProfileARN != "" { if !allowEc2Binds { - return logical.ErrorResponse("specified bound_iam_instance_profile_arn but not allowing ec2 auth_type or inferring ec2_instance"), nil + return logical.ErrorResponse(fmt.Sprintf("specified bound_iam_instance_profile_arn but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } if roleEntry.BoundIamRoleARN != "" { if !allowEc2Binds { - return logical.ErrorResponse("specified bound_iam_role_arn but not allowing ec2 auth_type or inferring ec2_instance"), nil + return logical.ErrorResponse(fmt.Sprintf("specified bound_iam_role_arn but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index af4ad04ed820..e74b3f15ea6a 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -169,7 +169,7 @@ func TestBackend_pathIam(t *testing.T) { } data := map[string]interface{}{ - "allowed_auth_types": "iam", + "allowed_auth_types": iamAuthType, "policies": "p,q,r,s", "max_ttl": "2h", "bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName", @@ -203,7 +203,7 @@ func TestBackend_pathIam(t *testing.T) { t.Fatalf("bad: policies: expected %#v\ngot: %#v\n", data, resp.Data) } - data["role_inferred_type"] = "invalid" + data["inferred_entity_type"] = "invalid" resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, Path: "role/ShouldNeverExist", @@ -211,13 +211,13 @@ func TestBackend_pathIam(t *testing.T) { Storage: storage, }) if resp == nil || !resp.IsError() { - t.Fatalf("Created role with invalid role_inferred_type") + t.Fatalf("Created role with invalid inferred_entity_type") } if err != nil { t.Fatal(err) } - data["role_inferred_type"] = "ec2_instance" + data["inferred_entity_type"] = ec2EntityType resp, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, Path: "role/ShouldNeverExist", @@ -329,12 +329,9 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { } resp, err := submitCreateRequest("shouldNeverExist") - if resp != nil && !resp.IsError() { + if (resp != nil && !resp.IsError()) || err == nil { t.Fatalf("created role with invalid allowed_auth_type") } - if err != nil { - t.Fatal(err) - } data["allowed_auth_types"] = "ec2,,iam" resp, err = submitCreateRequest("shouldNeverExist") @@ -365,7 +362,7 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { } delete(data, "bound_iam_principal_arn") - data["role_inferred_type"] = "ec2_instance" + data["inferred_entity_type"] = ec2EntityType data["inferred_aws_region"] = "us-east-1" resp, err = submitCreateRequest("multipleTypesInferred") if err != nil { @@ -428,7 +425,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) { } expected := map[string]interface{}{ - "allowed_auth_types": []string{"ec2"}, + "allowed_auth_types": []string{ec2AuthType}, "bound_ami_id": "testamiid", "bound_account_id": "testaccountid", "bound_region": "testregion", diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 34d9550e5554..83661213aac0 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -564,10 +564,10 @@ passed to the `login` method: ``` $ vault write auth/aws/login role=dev-role-iam \ - request_method=POST \ - request_url=https://sts.amazonaws.com/ \ - request_body=QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== \ - request_headers=eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ== + iam_http_request_method=POST \ + iam_request_url=https://sts.amazonaws.com/ \ + iam_request_body=QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== \ + iam_request_headers=eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ== ``` ### Via the API @@ -597,7 +597,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev- ``` curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev-role","pkcs7":"'$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')'","nonce":"5defbf9e-a8f9-3063-bdfc-54b7a42a1f95"}' -curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev", "request_method": "POST", "request_url": "https://sts.amazonaws.com/", "request_body": "QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==", "request_headers": "eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ==" }' +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev", "iam_http_request_method": "POST", "iam_request_url": "https://sts.amazonaws.com/", "iam_request_body": "QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==", "iam_request_headers": "eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ==" }' ``` The response will be in JSON. For example: @@ -1398,7 +1398,7 @@ auth method only when inferring an ec2 instance.
    • - role_inferred_type + inferred_entity_type optional When set, instructs Vault to turn on inferencing. The only current valid value is "ec2_instance" instructing Vault to infer that the role comes @@ -1777,7 +1777,7 @@ auth method only when inferring an ec2 instance.
    • - request_method + iam_http_request_method required HTTP method used in the signed request. Currently only POST is supported, but other methods may be supported in the future. This is @@ -1786,7 +1786,7 @@ auth method only when inferring an ec2 instance.
    • - request_url + iam_request_url required HTTP URL used in the signed request. Most likely just https://sts.amazonaws.com/ as most requests will probably use POST with @@ -1795,7 +1795,7 @@ auth method only when inferring an ec2 instance.
    • - request_body + iam_request_body required Base64-encoded body of the signed request. Most likely QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== @@ -1806,7 +1806,7 @@ auth method only when inferring an ec2 instance.
    • - request_headers + iam_request_headers required Base64-encoded, JSON-serialized representation of the HTTP request headers. The JSON serialization assumes that each header key maps to an From 2b11a63582145dc6841c914ef98686ec1c080390 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Sun, 2 Apr 2017 00:24:07 -0400 Subject: [PATCH 13/20] Address more feedback on aws auth PR --- builtin/credential/aws/backend_test.go | 4 +- builtin/credential/aws/client.go | 58 ++++++++++------------- builtin/credential/aws/path_login.go | 34 ++++++------- builtin/credential/aws/path_login_test.go | 6 +-- builtin/credential/aws/path_role.go | 10 ++-- builtin/credential/aws/path_role_test.go | 2 +- website/source/docs/auth/aws-ec2.html.md | 2 +- 7 files changed, 56 insertions(+), 60 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 64a6100c6fd9..402f4b3281ba 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1339,6 +1339,8 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // good enough rather than having to muck around in the low-level details for _, envvar := range []string{ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SECURITY_TOKEN", "AWS_SESSION_TOKEN"} { + // restore existing environment variables (in case future tests need them) + defer os.Setenv(envvar, os.Getenv(envvar)) os.Setenv(envvar, os.Getenv("TEST_"+envvar)) } awsSession, err := session.NewSession() @@ -1348,7 +1350,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { } stsService := sts.New(awsSession) - var stsInputParams *sts.GetCallerIdentityInput + stsInputParams := &sts.GetCallerIdentityInput{} testIdentity, err := stsService.GetCallerIdentity(stsInputParams) if err != nil { diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index b86150a4f4b0..1647f4527b7f 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -13,14 +13,14 @@ import ( "github.com/hashicorp/vault/logical" ) -// getClientConfig creates a aws-sdk-go config, which is used to create client +// getRawClientConfig creates a aws-sdk-go config, which is used to create client // that can interact with AWS API. This builds credentials in the following // order of preference: // // * Static credentials from 'config/client' // * Environment variables // * Instance metadata role -func (b *backend) getClientConfig(s logical.Storage, region, clientType string) (*aws.Config, error) { +func (b *backend) getRawClientConfig(s logical.Storage, region, clientType string) (*aws.Config, error) { credsConfig := &awsutil.CredentialsConfig{ Region: region, } @@ -66,33 +66,35 @@ func (b *backend) getClientConfig(s logical.Storage, region, clientType string) }, nil } -// getStsClientConfig returns an aws-sdk-go config, with assumed credentials -// It uses getClientConfig to obtain config for the runtime environemnt, which is -// then used to obtain a set of assumed credentials. The credentials will expire -// after 15 minutes but will auto-refresh. -func (b *backend) getStsClientConfig(s logical.Storage, region, stsRole, clientType string) (*aws.Config, error) { - stsConfig, err := b.getClientConfig(s, region, "sts") - if err != nil { - return nil, err - } - if stsConfig == nil { - return nil, fmt.Errorf("could not configure STS client") - } +// getClientConfig returns an aws-sdk-go config, with optionally assumed credentials +// It uses getRawClientConfig to obtain config for the runtime environemnt, and if +// stsRole is a non-empty string, it will use AssumeRole to obtain a set of assumed +// credentials. The credentials will expire after 15 minutes but will auto-refresh. +func (b *backend) getClientConfig(s logical.Storage, region, stsRole, clientType string) (*aws.Config, error) { - config, err := b.getClientConfig(s, region, clientType) + config, err := b.getRawClientConfig(s, region, clientType) if err != nil { return nil, err } if config == nil { return nil, fmt.Errorf("could not compile valid credentials through the default provider chain") } - assumedCredentials := stscreds.NewCredentials(session.New(stsConfig), stsRole) - // Test that we actually have permissions to assume the role - if _, err = assumedCredentials.Get(); err != nil { - return nil, err - } - config.Credentials = assumedCredentials + if stsRole != "" { + assumeRoleConfig, err := b.getRawClientConfig(s, region, "sts") + if err != nil { + return nil, err + } + if assumeRoleConfig == nil { + return nil, fmt.Errorf("could not configure STS client") + } + assumedCredentials := stscreds.NewCredentials(session.New(assumeRoleConfig), stsRole) + // Test that we actually have permissions to assume the role + if _, err = assumedCredentials.Get(); err != nil { + return nil, err + } + config.Credentials = assumedCredentials + } return config, nil } @@ -141,12 +143,7 @@ func (b *backend) clientEC2(s logical.Storage, region string, stsRole string) (* // Create an AWS config object using a chain of providers var awsConfig *aws.Config var err error - // The empty stsRole signifies the master account - if stsRole == "" { - awsConfig, err = b.getClientConfig(s, region, "ec2") - } else { - awsConfig, err = b.getStsClientConfig(s, region, stsRole, "ec2") - } + awsConfig, err = b.getClientConfig(s, region, stsRole, "ec2") if err != nil { return nil, err @@ -192,12 +189,7 @@ func (b *backend) clientIAM(s logical.Storage, region string, stsRole string) (* // Create an AWS config object using a chain of providers var awsConfig *aws.Config var err error - // The empty stsRole signifies the master account - if stsRole == "" { - awsConfig, err = b.getClientConfig(s, region, "iam") - } else { - awsConfig, err = b.getStsClientConfig(s, region, stsRole, "iam") - } + awsConfig, err = b.getClientConfig(s, region, stsRole, "iam") if err != nil { return nil, err diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 030f57f0dafd..8bdcc70e9bfe 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -383,6 +383,10 @@ func (b *backend) pathLoginUpdate( // Returns whether the EC2 instance meets the requirements of the particular // AWS role entry. +// The first error return value is whether there's some sort of validation +// error that means the instance doesn't meet the role requirements +// The second error return value indicates whether there's an error in even +// trying to validate those requirements func (b *backend) verifyInstanceMeetsRoleRequirements( s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, identityDoc *identityDocument) (*roleTagLoginResponse, error, error) { @@ -412,11 +416,6 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( stsRole = sts.StsRole } - iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole) - if err != nil { - iamClient = nil - } - // Verify that the AMI ID of the instance trying to login matches the // AMI ID specified as a constraint on the role. // @@ -498,8 +497,11 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( } // Use instance profile ARN to fetch the associated role ARN - if iamClient == nil { - return nil, nil, fmt.Errorf("could not fetch IAM client") + iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole) + if err != nil { + return nil, nil, fmt.Errorf("could not fetch IAM client: %v", err) + } else if iamClient == nil { + return nil, nil, fmt.Errorf("received a nil iamClient") } iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName) if err != nil { @@ -935,7 +937,7 @@ func (b *backend) pathLoginRenewIam( } _, err := b.validateInstance(req.Storage, instanceID, roleEntry.InferredAWSRegion, req.Auth.Metadata["accountID"]) if err != nil { - return nil, fmt.Errorf("failed to verify instance ID %q: %s", instanceID, err) + return nil, fmt.Errorf("failed to verify instance ID %q: %v", instanceID, err) } } else { return nil, fmt.Errorf("unrecognized entity_type in metadata: %q", entityType) @@ -1094,7 +1096,7 @@ func (b *backend) pathLoginUpdateIam( var headers http.Header err = jsonutil.DecodeJSON(headersJson, &headers) if err != nil { - return logical.ErrorResponse(fmt.Sprintf("failed to JSON decode iam_request_headers %q: %s", headersJson, err)), nil + return logical.ErrorResponse(fmt.Sprintf("failed to JSON decode iam_request_headers %q: %v", headersJson, err)), nil } config, err := b.lockedClientConfigEntry(req.Storage) @@ -1108,21 +1110,21 @@ func (b *backend) pathLoginUpdateIam( if config.IAMServerIdHeaderValue != "" { err = validateVaultHeaderValue(headers, parsedUrl, config.IAMServerIdHeaderValue) if err != nil { - return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %s", iamServerIdHeader, err)), nil + return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %v", iamServerIdHeader, err)), nil } } if config.STSEndpoint != "" { - endpoint = config.Endpoint + endpoint = config.STSEndpoint } } clientArn, accountID, err := submitCallerIdentityRequest(method, endpoint, parsedUrl, body, headers) if err != nil { - return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %s", err)), nil + return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %v", err)), nil } canonicalArn, principalName, sessionName, err := parseIamArn(clientArn) if err != nil { - return logical.ErrorResponse(fmt.Sprintf("Error parsing arn: %s", err)), nil + return logical.ErrorResponse(fmt.Sprintf("Error parsing arn: %v", err)), nil } roleName := data.Get("role").(string) @@ -1153,7 +1155,7 @@ func (b *backend) pathLoginUpdateIam( inferredEntityType := "" inferredEntityId := "" - if roleEntry.RoleInferredType == ec2EntityType { + if roleEntry.InferredEntityType == ec2EntityType { instance, err := b.validateInstance(req.Storage, sessionName, roleEntry.InferredAWSRegion, accountID) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", sessionName, roleEntry.InferredAWSRegion)), nil @@ -1265,7 +1267,7 @@ func parseIamArn(iamArn string) (string, string, string, error) { // 1. arn:aws:iam:::user/ // 2. arn:aws:sts:::assumed-role// // if we get something like 2, then we want to transform that back to what - // most people would expect, which is arn;aws:iam:::role/ + // most people would expect, which is arn:aws:iam:::role/ fullParts := strings.Split(iamArn, ":") principalFullName := fullParts[5] // principalFullName would now be something like user/ or assumed-role// @@ -1394,7 +1396,7 @@ func submitCallerIdentityRequest(method, endpoint string, parsedUrl *url.URL, bo client := cleanhttp.DefaultClient() response, err := client.Do(request) if err != nil { - return "", "", fmt.Errorf("error making request: %s", err) + return "", "", fmt.Errorf("error making request: %v", err) } if response != nil { defer response.Body.Close() diff --git a/builtin/credential/aws/path_login_test.go b/builtin/credential/aws/path_login_test.go index 4a7cc6aeddb6..e96bed835034 100644 --- a/builtin/credential/aws/path_login_test.go +++ b/builtin/credential/aws/path_login_test.go @@ -85,7 +85,7 @@ func TestBackend_validateVaultHeaderValue(t *testing.T) { const canaryHeaderValue = "Vault-Server" requestUrl, err := url.Parse("https://sts.amazonaws.com/") if err != nil { - t.Fatalf("error parsing test URL: %s", err) + t.Fatalf("error parsing test URL: %v", err) } postHeadersMissing := http.Header{ "Host": []string{"Foo"}, @@ -130,11 +130,11 @@ func TestBackend_validateVaultHeaderValue(t *testing.T) { err = validateVaultHeaderValue(postHeadersValid, requestUrl, canaryHeaderValue) if err != nil { - t.Errorf("did NOT validate valid POST request: %s", err) + t.Errorf("did NOT validate valid POST request: %v", err) } err = validateVaultHeaderValue(postHeadersSplit, requestUrl, canaryHeaderValue) if err != nil { - t.Errorf("did NOT validate valid POST request with split Authorization header: %s", err) + t.Errorf("did NOT validate valid POST request with split Authorization header: %v", err) } } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 8e1c3f056b7a..47d1c440ae31 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -405,7 +405,7 @@ func (b *backend) pathRoleCreateUpdate( } if inferRoleTypeRaw, ok := data.GetOk("inferred_entity_type"); ok { - roleEntry.RoleInferredType = inferRoleTypeRaw.(string) + roleEntry.InferredEntityType = inferRoleTypeRaw.(string) } if inferredAWSRegionRaw, ok := data.GetOk("inferred_aws_region"); ok { @@ -433,12 +433,12 @@ func (b *backend) pathRoleCreateUpdate( allowEc2Binds := allowEc2Auth - if roleEntry.RoleInferredType != "" { + if roleEntry.InferredEntityType != "" { switch { case !allowIamAuth: return logical.ErrorResponse("specified inferred_entity_type but didn't allow iam auth_type"), nil - case roleEntry.RoleInferredType != ec2EntityType: - return logical.ErrorResponse(fmt.Sprintf("specified invalid inferred_entity_type: %s", roleEntry.RoleInferredType)), nil + case roleEntry.InferredEntityType != ec2EntityType: + return logical.ErrorResponse(fmt.Sprintf("specified invalid inferred_entity_type: %s", roleEntry.InferredEntityType)), nil case roleEntry.InferredAWSRegion == "": return logical.ErrorResponse("specified inferred_entity_type but not inferred_aws_region"), nil } @@ -603,7 +603,7 @@ type awsRoleEntry struct { BoundRegion string `json:"bound_region" structs:"bound_region" mapstructure:"bound_region"` BoundSubnetID string `json:"bound_subnet_id" structs:"bound_subnet_id" mapstructure:"bound_subnet_id"` BoundVpcID string `json:"bound_vpc_id" structs:"bound_vpc_id" mapstructure:"bound_vpc_id"` - RoleInferredType string `json:"infer_role_type" structs:"infer_role_type" mapstructure:"infer_role_type"` + InferredEntityType string `json:"inferred_entity_type" structs:"inferred_entity_type" mapstructure:"inferred_entity_type"` InferredAWSRegion string `json:"inferred_aws_region" structs:"inferred_aws_region" mapstructure:"inferred_aws_region"` RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index e74b3f15ea6a..b7c7bd8e3f49 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -434,7 +434,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) { "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", "bound_subnet_id": "testsubnetid", "bound_vpc_id": "testvpcid", - "infer_role_type": "", + "inferred_entity_type": "", "inferred_aws_region": "", "role_tag": "testtag", "allow_instance_migration": true, diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 83661213aac0..69d2c47793f7 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -1379,7 +1379,7 @@ auth method only when inferring an ec2 instance. optional If set, enables the role tags for this role. The value set for this field should be the 'key' of the tag on the EC2 instance. The 'value' - of the tag should be generated using 'role//tag' endpoint. + of the tag should be generated using `role//tag` endpoint. Defaults to an empty string, meaning that role tags are disabled. This constraint is checked by the ec2 auth method as well as the iam auth method only when inferring an EC2 instance. From 2f350eeb545cfa1f727f7831d9e21d5bf275ba54 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Wed, 5 Apr 2017 01:13:15 -0400 Subject: [PATCH 14/20] Make aws auth_type immutable per role --- builtin/credential/aws/backend_test.go | 42 ++---------- builtin/credential/aws/path_login.go | 87 +++++++++++------------- builtin/credential/aws/path_role.go | 73 +++++++++++--------- builtin/credential/aws/path_role_test.go | 57 ++++++++-------- website/source/docs/auth/aws-ec2.html.md | 66 ++++++++---------- 5 files changed, 142 insertions(+), 183 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 402f4b3281ba..2cb583915c40 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1043,7 +1043,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. } roleReq := &logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/" + roleName, Storage: storage, Data: data, @@ -1062,6 +1062,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. } // Place the correct AMI ID, but make the AccountID wrong + roleReq.Operation = logical.UpdateOperation data["bound_ami_id"] = amiID data["bound_account_id"] = "wrong-account-id" resp, err = b.HandleRequest(roleReq) @@ -1089,24 +1090,8 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) } - // place the correct IAM role ARN, but make the auth type wrong + // place the correct IAM role ARN data["bound_iam_role_arn"] = iamARN - data["bound_iam_principal_arn"] = iamARN - data["allowed_auth_types"] = iamAuthType - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) - } - - // Attempt to login and expect a fail because auth_type is wrong - resp, err = b.HandleRequest(loginRequest) - if err != nil || resp == nil || (resp != nil && !resp.IsError()) { - t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) - } - - // Place the correct auth type - delete(data, "bound_iam_principal_arn") - data["allowed_auth_types"] = ec2AuthType resp, err = b.HandleRequest(roleReq) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) @@ -1397,7 +1382,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { roleData := map[string]interface{}{ "bound_iam_principal_arn": testIdentityArn, "policies": "root", - "allowed_auth_types": iamAuthType, + "auth_type": iamAuthType, } roleRequest := &logical.Request{ Operation: logical.CreateOperation, @@ -1426,20 +1411,12 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { t.Fatalf("bad: failed to create role; resp:%#v\nerr:%v", resp, err) } - roleDataEc2["allowed_auth_types"] = "ec2,iam" - roleDataEc2["bound_iam_principal_arn"] = testIdentityArn - roleRequestEc2.Path = "role/ec2Iam" - resp, err = b.HandleRequest(roleRequestEc2) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: failed to create role; resp:%#v\nerr:%v", resp, err) - } - // now we're creating the invalid role we won't be able to login to roleData["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/FakeRole" roleRequest.Path = "role/" + testInvalidRoleName resp, err = b.HandleRequest(roleRequest) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) + t.Fatalf("bad: didn't fail to create role: resp:%#v\nerr:%v", resp, err) } // now, create the request without the signed header @@ -1518,13 +1495,4 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { if resp == nil || resp.Auth == nil || resp.IsError() { t.Errorf("bad: expected valid login: resp:%#v", resp) } - - loginData["role"] = "ec2Iam" - resp, err = b.HandleRequest(loginRequest) - if err != nil { - t.Fatal(err) - } - if resp == nil || resp.Auth == nil || resp.IsError() { - t.Errorf("bad: expected valid login: resp:%#v", resp) - } } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 8bdcc70e9bfe..c6082eb23fad 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -388,27 +388,27 @@ func (b *backend) pathLoginUpdate( // The second error return value indicates whether there's an error in even // trying to validate those requirements func (b *backend) verifyInstanceMeetsRoleRequirements( - s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, identityDoc *identityDocument) (*roleTagLoginResponse, error, error) { + s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, identityDoc *identityDocument) (error, error) { switch { case instance == nil: - return nil, nil, fmt.Errorf("nil instance") + return nil, fmt.Errorf("nil instance") case roleEntry == nil: - return nil, nil, fmt.Errorf("nil roleEntry") + return nil, fmt.Errorf("nil roleEntry") case identityDoc == nil: - return nil, nil, fmt.Errorf("nil identityDoc") + return nil, fmt.Errorf("nil identityDoc") } // Verify that the AccountID of the instance trying to login matches the // AccountID specified as a constraint on role if roleEntry.BoundAccountID != "" && identityDoc.AccountID != roleEntry.BoundAccountID { - return nil, fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil + return fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil } // Check if an STS configuration exists for the AWS account sts, err := b.lockedAwsStsEntry(s, identityDoc.AccountID) if err != nil { - return nil, fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil + return fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil } // An empty STS role signifies the master account stsRole := "" @@ -427,30 +427,30 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // is ec2 or iam. if roleEntry.BoundAmiID != "" { if instance.ImageId == nil { - return nil, nil, fmt.Errorf("AMI ID in the instance description is nil") + return nil, fmt.Errorf("AMI ID in the instance description is nil") } if roleEntry.BoundAmiID != *instance.ImageId { - return nil, fmt.Errorf("AMI ID %q does not belong to role %q", instance.ImageId, roleName), nil + return fmt.Errorf("AMI ID %q does not belong to role %q", instance.ImageId, roleName), nil } } // Validate the SubnetID if corresponding bound was set on the role if roleEntry.BoundSubnetID != "" { if instance.SubnetId == nil { - return nil, nil, fmt.Errorf("subnet ID in the instance description is nil") + return nil, fmt.Errorf("subnet ID in the instance description is nil") } if roleEntry.BoundSubnetID != *instance.SubnetId { - return nil, fmt.Errorf("subnet ID %q does not satisfy the constraint on role %q", *instance.SubnetId, roleName), nil + return fmt.Errorf("subnet ID %q does not satisfy the constraint on role %q", *instance.SubnetId, roleName), nil } } // Validate the VpcID if corresponding bound was set on the role if roleEntry.BoundVpcID != "" { if instance.VpcId == nil { - return nil, nil, fmt.Errorf("VPC ID in the instance description is nil") + return nil, fmt.Errorf("VPC ID in the instance description is nil") } if roleEntry.BoundVpcID != *instance.VpcId { - return nil, fmt.Errorf("VPC ID %q does not satisfy the constraint on role %q", *instance.VpcId, roleName), nil + return fmt.Errorf("VPC ID %q does not satisfy the constraint on role %q", *instance.VpcId, roleName), nil } } @@ -459,14 +459,14 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // on the role if roleEntry.BoundIamInstanceProfileARN != "" { if instance.IamInstanceProfile == nil { - return nil, nil, fmt.Errorf("IAM instance profile in the instance description is nil") + return nil, fmt.Errorf("IAM instance profile in the instance description is nil") } if instance.IamInstanceProfile.Arn == nil { - return nil, nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") + return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") } iamInstanceProfileARN := *instance.IamInstanceProfile.Arn if !strings.HasPrefix(iamInstanceProfileARN, roleEntry.BoundIamInstanceProfileARN) { - return nil, fmt.Errorf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil + return fmt.Errorf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil } } @@ -474,17 +474,17 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( // the IAM role ARN specified as a constraint on the role. if roleEntry.BoundIamRoleARN != "" { if instance.IamInstanceProfile == nil { - return nil, nil, fmt.Errorf("IAM instance profile in the instance description is nil") + return nil, fmt.Errorf("IAM instance profile in the instance description is nil") } if instance.IamInstanceProfile.Arn == nil { - return nil, nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") + return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") } // Fetch the instance profile ARN from the instance description iamInstanceProfileARN := *instance.IamInstanceProfile.Arn if iamInstanceProfileARN == "" { - return nil, nil, fmt.Errorf("IAM instance profile ARN in the instance description is empty") + return nil, fmt.Errorf("IAM instance profile ARN in the instance description is empty") } // Extract out the instance profile name from the instance @@ -493,41 +493,30 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1] if iamInstanceProfileName == "" { - return nil, nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") + return nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") } // Use instance profile ARN to fetch the associated role ARN iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole) if err != nil { - return nil, nil, fmt.Errorf("could not fetch IAM client: %v", err) + return nil, fmt.Errorf("could not fetch IAM client: %v", err) } else if iamClient == nil { - return nil, nil, fmt.Errorf("received a nil iamClient") + return nil, fmt.Errorf("received a nil iamClient") } iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName) if err != nil { - return nil, nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err) + return nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err) } if iamRoleARN == "" { - return nil, nil, fmt.Errorf("IAM role ARN could not be fetched") + return nil, fmt.Errorf("IAM role ARN could not be fetched") } if !strings.HasPrefix(iamRoleARN, roleEntry.BoundIamRoleARN) { - return nil, fmt.Errorf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil + return fmt.Errorf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil } } - var roleTagResp *roleTagLoginResponse - if roleEntry.RoleTag != "" { - roleTagResp, err := b.handleRoleTagLogin(s, roleName, roleEntry, instance) - if err != nil { - return nil, nil, err - } - if roleTagResp == nil { - return nil, fmt.Errorf("failed to fetch and verify the role tag"), nil - } - } - - return roleTagResp, nil, nil + return nil, nil } // pathLoginUpdateEc2 is used to create a Vault token by the EC2 instances @@ -602,7 +591,7 @@ func (b *backend) pathLoginUpdateEc2( return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil } - if !strutil.StrListContains(roleEntry.AllowedAuthTypes, ec2AuthType) { + if roleEntry.AuthType != ec2AuthType { return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil } @@ -620,7 +609,7 @@ func (b *backend) pathLoginUpdateEc2( return logical.ErrorResponse(fmt.Sprintf("Region %q does not satisfy the constraint on role %q", identityDocParsed.Region, roleName)), nil } - roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDocParsed) + validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDocParsed) if err != nil { return nil, err } @@ -628,6 +617,16 @@ func (b *backend) pathLoginUpdateEc2( return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil } + var roleTagResp *roleTagLoginResponse + if roleEntry.RoleTag != "" { + roleTagResp, err := b.handleRoleTagLogin(req.Storage, roleName, roleEntry, instance) + if err != nil { + return nil, err + } + if roleTagResp == nil { + return logical.ErrorResponse("failed to fetch and verify the role tag"), nil + } + } // Get the entry from the identity whitelist, if there is one storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDocParsed.InstanceID) if err != nil { @@ -1140,7 +1139,7 @@ func (b *backend) pathLoginUpdateIam( return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil } - if !strutil.StrListContains(roleEntry.AllowedAuthTypes, iamAuthType) { + if roleEntry.AuthType != iamAuthType { return logical.ErrorResponse(fmt.Sprintf("auth method iam not allowed for role %s", roleName)), nil } @@ -1171,7 +1170,7 @@ func (b *backend) pathLoginUpdateIam( PendingTime: instance.LaunchTime.Format(time.RFC3339), } - roleTagResp, validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDoc) + validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDoc) if err != nil { return nil, err } @@ -1179,14 +1178,6 @@ func (b *backend) pathLoginUpdateIam( return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil } - if roleTagResp != nil { - if len(roleTagResp.Policies) != 0 { - policies = roleTagResp.Policies - } - - rTagMaxTTL = roleTagResp.MaxTTL - } - inferredEntityType = ec2EntityType inferredEntityId = sessionName } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 47d1c440ae31..f369b98ab904 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -8,7 +8,6 @@ import ( "github.com/fatih/structs" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/policyutil" - "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -21,11 +20,11 @@ func pathRole(b *backend) *framework.Path { Type: framework.TypeString, Description: "Name of the role.", }, - "allowed_auth_types": { + "auth_type": { Type: framework.TypeString, Default: ec2AuthType, - Description: `The comma-separated list of allowed auth_type values that -are allowed to authenticate to this role.`, + Description: `The auth_type permitted to authenticate to this role. Cannot be +changed once set.`, }, "bound_ami_id": { Type: framework.TypeString, @@ -283,13 +282,13 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole } } - // Check if there was no pre-existing AllowedAuthTypes set (from older versions) - if len(result.AllowedAuthTypes) == 0 { + // Check if there was no pre-existing AuthType set (from older versions) + if result.AuthType == "" { // then default to the original behavior of ec2 - result.AllowedAuthTypes = []string{ec2AuthType} + result.AuthType = ec2AuthType // and save the result if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { - return nil, fmt.Errorf("failed to save default allowed_auth_types") + return nil, fmt.Errorf("failed to save default auth_type") } } @@ -412,30 +411,27 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.InferredAWSRegion = inferredAWSRegionRaw.(string) } - var allowEc2Auth, allowIamAuth bool - - if allowedAuthTypesRaw, ok := data.GetOk("allowed_auth_types"); ok { - roleEntry.AllowedAuthTypes = strutil.ParseDedupAndSortStrings(allowedAuthTypesRaw.(string), ",") - } else if req.Operation == logical.CreateOperation { - roleEntry.AllowedAuthTypes = strutil.ParseDedupAndSortStrings(data.Get("allowed_auth_types").(string), ",") - } - - for _, t := range roleEntry.AllowedAuthTypes { - switch t { - case ec2AuthType: - allowEc2Auth = true - case iamAuthType: - allowIamAuth = true - default: - return nil, fmt.Errorf("Unrecognized auth_type in roleEntry: %q", t) + // auth_type is a special case as it's immutable and can't be changed once a role is created + if authTypeRaw, ok := data.GetOk("auth_type"); ok { + if roleEntry.AuthType == "" { + switch authTypeRaw.(string) { + case ec2AuthType, iamAuthType: + roleEntry.AuthType = authTypeRaw.(string) + default: + return logical.ErrorResponse(fmt.Sprintf("unrecognized auth_type: %v", authTypeRaw.(string))), nil + } + } else if authTypeRaw.(string) != roleEntry.AuthType { + return logical.ErrorResponse("attempted to change auth_type on role"), nil } + } else if req.Operation == logical.CreateOperation { + roleEntry.AuthType = data.Get("auth_type").(string) } - allowEc2Binds := allowEc2Auth + allowEc2Binds := roleEntry.AuthType == ec2AuthType if roleEntry.InferredEntityType != "" { switch { - case !allowIamAuth: + case roleEntry.AuthType != iamAuthType: return logical.ErrorResponse("specified inferred_entity_type but didn't allow iam auth_type"), nil case roleEntry.InferredEntityType != ec2EntityType: return logical.ErrorResponse(fmt.Sprintf("specified invalid inferred_entity_type: %s", roleEntry.InferredEntityType)), nil @@ -450,14 +446,14 @@ func (b *backend) pathRoleCreateUpdate( numBinds := 0 if roleEntry.BoundAccountID != "" { - if !allowEc2Auth { - return logical.ErrorResponse("specified bound_account_id but not allowing ec2 auth_type"), nil + if !allowEc2Binds { + return logical.ErrorResponse(fmt.Sprintf("specified bound_account_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil } numBinds++ } if roleEntry.BoundRegion != "" { - if !allowEc2Auth { + if roleEntry.AuthType != ec2AuthType { return logical.ErrorResponse("specified bound_region but not allowing ec2 auth_type"), nil } numBinds++ @@ -485,7 +481,7 @@ func (b *backend) pathRoleCreateUpdate( } if roleEntry.BoundIamPrincipalARN != "" { - if !allowIamAuth { + if roleEntry.AuthType != iamAuthType { return logical.ErrorResponse("specified bound_iam_principal_arn but not allowing iam auth_type"), nil } numBinds++ @@ -504,15 +500,21 @@ func (b *backend) pathRoleCreateUpdate( disallowReauthenticationBool, ok := data.GetOk("disallow_reauthentication") if ok { + if roleEntry.AuthType != ec2AuthType { + return logical.ErrorResponse("specified disallow_reauthentication when not using ec2 auth type"), nil + } roleEntry.DisallowReauthentication = disallowReauthenticationBool.(bool) - } else if req.Operation == logical.CreateOperation { + } else if req.Operation == logical.CreateOperation && roleEntry.AuthType == ec2AuthType { roleEntry.DisallowReauthentication = data.Get("disallow_reauthentication").(bool) } allowInstanceMigrationBool, ok := data.GetOk("allow_instance_migration") if ok { + if roleEntry.AuthType != ec2AuthType { + return logical.ErrorResponse("specified allow_instance_migration when not using ec2 auth type"), nil + } roleEntry.AllowInstanceMigration = allowInstanceMigrationBool.(bool) - } else if req.Operation == logical.CreateOperation { + } else if req.Operation == logical.CreateOperation && roleEntry.AuthType == ec2AuthType { roleEntry.AllowInstanceMigration = data.Get("allow_instance_migration").(bool) } @@ -564,13 +566,16 @@ func (b *backend) pathRoleCreateUpdate( roleTagStr, ok := data.GetOk("role_tag") if ok { + if roleEntry.AuthType != ec2AuthType { + return logical.ErrorResponse("tried to enable role_tag when not using ec2 auth method"), nil + } roleEntry.RoleTag = roleTagStr.(string) // There is a limit of 127 characters on the tag key for AWS EC2 instances. // Complying to that requirement, do not allow the value of 'key' to be more than that. if len(roleEntry.RoleTag) > 127 { return logical.ErrorResponse("length of role tag exceeds the EC2 key limit of 127 characters"), nil } - } else if req.Operation == logical.CreateOperation { + } else if req.Operation == logical.CreateOperation && roleEntry.AuthType == ec2AuthType { roleEntry.RoleTag = data.Get("role_tag").(string) } @@ -594,7 +599,7 @@ func (b *backend) pathRoleCreateUpdate( // Struct to hold the information associated with an AMI ID in Vault. type awsRoleEntry struct { - AllowedAuthTypes []string `json:"allowed_auth_types" structs:"allowed_auth_types" mapstructure:"allowed_auth_types"` + AuthType string `json:"auth_type" structs:"auth_type" mapstructure:"auth_type"` BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"` BoundAccountID string `json:"bound_account_id" structs:"bound_account_id" mapstructure:"bound_account_id"` BoundIamPrincipalARN string `json:"bound_iam_principal_arn" structs:"bound_iam_principal_arn" mapstructure:"bound_iam_principal_arn"` diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index b7c7bd8e3f49..c65ec2262d47 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -169,7 +169,7 @@ func TestBackend_pathIam(t *testing.T) { } data := map[string]interface{}{ - "allowed_auth_types": iamAuthType, + "auth_type": iamAuthType, "policies": "p,q,r,s", "max_ttl": "2h", "bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName", @@ -185,7 +185,7 @@ func TestBackend_pathIam(t *testing.T) { t.Fatal(err) } if resp != nil && resp.IsError() { - t.Fatal("failed to create the role entry") + t.Fatalf("failed to create the role entry; resp: %#v", resp) } resp, err = b.HandleRequest(&logical.Request{ @@ -314,57 +314,60 @@ func TestBackend_pathRoleMixedTypes(t *testing.T) { } data := map[string]interface{}{ - "policies": "p,q,r,s", - "bound_ami_id": "ami-abc1234", - "allowed_auth_types": "ec2,invalid", + "policies": "p,q,r,s", + "bound_ami_id": "ami-abc1234", + "auth_type": "ec2,invalid", } - submitCreateRequest := func(roleName string) (*logical.Response, error) { + submitRequest := func(roleName string, op logical.Operation) (*logical.Response, error) { return b.HandleRequest(&logical.Request{ - Operation: logical.CreateOperation, + Operation: op, Path: "role/" + roleName, Data: data, Storage: storage, }) } - resp, err := submitCreateRequest("shouldNeverExist") - if (resp != nil && !resp.IsError()) || err == nil { - t.Fatalf("created role with invalid allowed_auth_type") + resp, err := submitRequest("shouldNeverExist", logical.CreateOperation) + if resp == nil || !resp.IsError() { + t.Fatalf("created role with invalid auth_type; resp: %#v", resp) + } + if err != nil { + t.Fatal(err) } - data["allowed_auth_types"] = "ec2,,iam" - resp, err = submitCreateRequest("shouldNeverExist") - if resp != nil && !resp.IsError() { - t.Fatalf("created role without required bound_iam_principal_arn") + data["auth_type"] = "ec2,,iam" + resp, err = submitRequest("shouldNeverExist", logical.CreateOperation) + if resp == nil || !resp.IsError() { + t.Fatalf("created role mixed auth types") } if err != nil { t.Fatal(err) } - data["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/MyRole" - delete(data, "bound_ami_id") - resp, err = submitCreateRequest("shouldNeverExist") - if resp != nil && !resp.IsError() { - t.Fatalf("created role without required ec2 binding") + data["auth_type"] = ec2AuthType + resp, err = submitRequest("ec2_to_iam", logical.CreateOperation) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create valid role; resp: %#v", resp) } if err != nil { t.Fatal(err) } - data["bound_ami_id"] = "ami-1234567" - resp, err = submitCreateRequest("multipleTypes") + data["auth_type"] = iamAuthType + delete(data, "bound_ami_id") + data["bound_iam_principal_arn"] = "arn:aws:iam::123456789012:role/MyRole" + resp, err = submitRequest("ec2_to_iam", logical.UpdateOperation) + if resp == nil || !resp.IsError() { + t.Fatalf("changed auth type on the role") + } if err != nil { t.Fatal(err) } - if resp.IsError() { - t.Fatalf("didn't allow creation of valid role with multiple bindings of different types") - } - delete(data, "bound_iam_principal_arn") data["inferred_entity_type"] = ec2EntityType data["inferred_aws_region"] = "us-east-1" - resp, err = submitCreateRequest("multipleTypesInferred") + resp, err = submitRequest("multipleTypesInferred", logical.CreateOperation) if err != nil { t.Fatal(err) } @@ -425,7 +428,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) { } expected := map[string]interface{}{ - "allowed_auth_types": []string{ec2AuthType}, + "auth_type": ec2AuthType, "bound_ami_id": "testamiid", "bound_account_id": "testaccountid", "bound_region": "testregion", diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 69d2c47793f7..6e62c515e8eb 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -101,12 +101,13 @@ an IAM principal to be MFA authenticated while authenticating to Vault. ## Authorization Workflow The basic mechanism of operation is per-role. Roles are registered in the -backend and associated with various optional restrictions, such as the set -of allowed policies and max TTLs on the generated tokens. Each role can -be specified with the constraints that are to be met during the login. For -example, one such constraint that is supported is to bind against AMI ID. A -role which is bound to a specific AMI, can only be used for login by EC2 -instances that are deployed on the same AMI. +backend and associated with a specific authentication type that cannot be +changed once the role has been created. Roles can also be associated with +various optional restrictions, such as the set of allowed policies and max TTLs +on the generated tokens. Each role can be specified with the constraints that +are to be met during the login. For example, one such constraint that is +supported is to bind against AMI ID. A role which is bound to a specific AMI, +can only be used for login by EC2 instances that are deployed on the same AMI. In general, role bindings that are specific to an EC2 instance are only checked when the ec2 auth method is used to login, while bindings specific to IAM @@ -117,7 +118,8 @@ only apply specifically to EC2 instances. In many cases, an organization will use a "seed AMI" that is specialized after bootup by configuration management or similar processes. For this reason, a -role entry in the backend can also be associated with a "role tag". These tags +role entry in the backend can also be associated with a "role tag" when using +the ec2 auth type. These tags are generated by the backend and are placed as the value of a tag with the given key on the EC2 instance. The role tag can be used to further restrict the parameters set on the role, but cannot be used to grant additional privileges. @@ -182,31 +184,19 @@ whether or not it's appropriate to configure inferencing. ## Mixing Authentication Types -Vault allows you to configure whether to allow the ec2 auth method, the aws auth -method, or both auth methods for a given role. If you do this, it is important -to understand that _only those bindings applicable to the client's chosen auth -type will be enforced by Vault_. Some examples: +Vault allows you to configure using either the ec2 auth method or the iam auth +method, but not both auth methods. Further, Vault will prevent you from +enforcing restrictions that it cannot enforce given the chosen auth type for a +role. Some examples of how this works in practice: -1. You configure a role only allowing the ec2 auth type, with a bound AMI ID. A +1. You configure a role with the ec2 auth type, with a bound AMI ID. A client would not be able to login using the iam auth type. -2. You configure a role only allowing the iam auth type, with a bound IAM +2. You configure a role with the iam auth type, with a bound IAM principal ARN. A client would not be able to login with the ec2 auth method. -3. You configure a role only allowing the iam auth type and further configure +3. You configure a role with the iam auth type and further configure inferencing. You have a bound AMI ID and a bound IAM principal ARN. A client must login using the iam method; the RoleSessionName must be a valid instance ID viewable by Vault, and the instance must have come from the bound AMI ID. -4. You configure a role to allow both iam and ec2 auth types, but you have not - configured inferencing. You configure both a bound AMI ID and a bound IAM - principal ARN. If a client chooses to login with the ec2 auth method, only the - bound AMI is checked; the bound IAM principal ARN is ignored. Similarly, if a - client logs in with the iam auth method, then only the bound IAM principal ARN - is checked; the bound AMI ID is ignored. -5. You configure a role to allow both iam and ec2 auth types, and you have - further configured inferencing, with a bound IAM principal ARN and a bound AMI - ID. If a client logs in with the ec2 auth method, then only the bound AMI ID - is checked. If a client logs in with the iam auth method, then the same - checks are performed as in example 3. - ## Comparison of the EC2 and IAM Methods @@ -260,7 +250,8 @@ comparison of the two authentication methods. from), then you would need to use the ec2 auth method, change the instance profile associated with your EC2 instances so they have unique IAM roles for each different Vault role you would want them to authenticate - to, or make use of inferencing. + to, or make use of inferencing. If you need to make use of role tags, then + you will need to use the ec2 auth method. ## Client Nonce @@ -517,7 +508,7 @@ $ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5 ``` $ vault write auth/aws/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h -$ vault write auth/aws/role/dev-role-iam allowed_auth_types=iam \ +$ vault write auth/aws/role/dev-role-iam auth_type=iam \ bound_iam_principal_arn=arn:aws:iam::123456789012:role/MyRole policies=prod,dev max_ttl=500h ``` @@ -589,7 +580,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/config/cl ``` curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role -d '{"bound_ami_id":"ami-fce3c696","policies":"prod,dev","max_ttl":"500h"}' -curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role-iam -d '{"allowed_auth_types":"iam","policies":"prod,dev","max_ttl":"500h","bound_iam_principal_arn":"arn:aws:iam::123456789012:role/MyRole"}' +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role-iam -d '{"auth_type":"iam","policies":"prod,dev","max_ttl":"500h","bound_iam_principal_arn":"arn:aws:iam::123456789012:role/MyRole"}' ``` #### Perform the login operation @@ -1294,12 +1285,12 @@ The response will be in JSON. For example:
    • - allowed_auth_types + auth_type optional - The auth types permitted for this role, separated by commas. Valid - choices are "ec2" or "iam". If no value is chosen, then it will default - to "ec2" for backwards compatibility. Only those bindings applicable to - the auth type chosen by clients will be checked by Vault upon login. + The auth type permitted for this role. Valid choices are "ec2" or "iam". + If no value is chosen, then it will default to "ec2" for backwards + compatibility. Only those bindings applicable to the auth type chosen by + clients will be checked by Vault upon login.
      @@ -1318,7 +1309,8 @@ instance. optional If set, defines a constraint on the EC2 instances that the account ID in its identity document to match the one specified by this parameter. This -constraint is checked only by the ec2 auth method. +constraint is checked during ec2 auth as well as the iam auth method only when +inferring an EC2 instance.
      @@ -1381,8 +1373,8 @@ auth method only when inferring an ec2 instance. field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using `role//tag` endpoint. Defaults to an empty string, meaning that role tags are disabled. This - constraint is checked by the ec2 auth method as well as the iam auth - method only when inferring an EC2 instance. + constraint is valid only with the ec2 auth method and is not checked + when using the iam auth method, even when inferencing is enabled.
      From 10e0949635c99fa15db73eda837f5d2203998c3f Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Mon, 17 Apr 2017 22:54:11 -0400 Subject: [PATCH 15/20] Address more aws auth PR feedback --- builtin/credential/aws/backend_test.go | 8 ++- builtin/credential/aws/cli.go | 2 +- builtin/credential/aws/path_login.go | 67 ++++++++++++------------ builtin/credential/aws/path_role.go | 35 +++++++------ builtin/credential/aws/path_role_test.go | 3 ++ website/source/docs/auth/aws-ec2.html.md | 9 ++-- 6 files changed, 69 insertions(+), 55 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 2cb583915c40..a539fbac18be 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -34,6 +34,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { // create a role entry data := map[string]interface{}{ + "auth_type": "ec2", "policies": "p,q,r,s", "bound_ami_id": "abcd-123", } @@ -704,6 +705,7 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { // create a role data := map[string]interface{}{ + "auth_type": "ec2", "policies": "p,q,r,s", "max_ttl": "120s", "role_tag": "VaultRole", @@ -780,6 +782,7 @@ func TestBackend_PathRoleTag(t *testing.T) { } data := map[string]interface{}{ + "auth_type": "ec2", "policies": "p,q,r,s", "max_ttl": "120s", "role_tag": "VaultRole", @@ -845,6 +848,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { // create an role entry data := map[string]interface{}{ + "auth_type": "ec2", "policies": "p,q,r,s", "role_tag": "VaultRole", "bound_ami_id": "abcd-123", @@ -1035,6 +1039,7 @@ func TestBackendAcc_LoginWithInstanceIdentityDocAndWhitelistIdentity(t *testing. // Place the wrong AMI ID in the role data. data := map[string]interface{}{ + "auth_type": "ec2", "policies": "root", "max_ttl": "120s", "bound_ami_id": "wrong_ami_id", @@ -1279,7 +1284,7 @@ func buildCallerIdentityLoginData(request *http.Request, roleName string) (map[s } return map[string]interface{}{ "iam_http_request_method": request.Method, - "iam_request_url": request.URL.String(), + "iam_request_url": base64.StdEncoding.EncodeToString([]byte(request.URL.String())), "iam_request_headers": base64.StdEncoding.EncodeToString(headersJson), "iam_request_body": base64.StdEncoding.EncodeToString(requestBody), "request_role": roleName, @@ -1397,6 +1402,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // configuring a valid role we won't be able to login to roleDataEc2 := map[string]interface{}{ + "auth_type": "ec2", "policies": "root", "bound_ami_id": "ami-1234567", } diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 594b792955b7..d7060e7f5364 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -75,7 +75,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { return "", err } method := stsRequest.HTTPRequest.Method - targetUrl := stsRequest.HTTPRequest.URL.String() + targetUrl := base64.StdEncoding.EncodeToString([]byte(stsRequest.HTTPRequest.URL.String())) headers := base64.StdEncoding.EncodeToString(headersJson) body := base64.StdEncoding.EncodeToString(requestBody) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index c6082eb23fad..9f0e1e6324fd 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -75,9 +75,9 @@ presigned request. Currently, POST is the only supported value`, "iam_request_url": { Type: framework.TypeString, - Description: `Full URL against which to make the AWS request when auth_type is -iam. If using a POST request with the action -specified in the body, this should just be "/".`, + Description: `Base64-encoded full URL against which to make the AWS request +when using iam auth_type. If using a POST request with the action specified in the +body, this should just be "Lw==" ("/" base64-encoded).`, }, "iam_request_body": { @@ -405,17 +405,6 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( return fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil } - // Check if an STS configuration exists for the AWS account - sts, err := b.lockedAwsStsEntry(s, identityDoc.AccountID) - if err != nil { - return fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil - } - // An empty STS role signifies the master account - stsRole := "" - if sts != nil { - stsRole = sts.StsRole - } - // Verify that the AMI ID of the instance trying to login matches the // AMI ID specified as a constraint on the role. // @@ -496,6 +485,17 @@ func (b *backend) verifyInstanceMeetsRoleRequirements( return nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN") } + // Check if an STS configuration exists for the AWS account + sts, err := b.lockedAwsStsEntry(s, identityDoc.AccountID) + if err != nil { + return fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil + } + // An empty STS role signifies the master account + stsRole := "" + if sts != nil { + stsRole = sts.StsRole + } + // Use instance profile ARN to fetch the associated role ARN iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole) if err != nil { @@ -614,19 +614,9 @@ func (b *backend) pathLoginUpdateEc2( return nil, err } if validationError != nil { - return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil + return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %v", validationError)), nil } - var roleTagResp *roleTagLoginResponse - if roleEntry.RoleTag != "" { - roleTagResp, err := b.handleRoleTagLogin(req.Storage, roleName, roleEntry, instance) - if err != nil { - return nil, err - } - if roleTagResp == nil { - return logical.ErrorResponse("failed to fetch and verify the role tag"), nil - } - } // Get the entry from the identity whitelist, if there is one storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDocParsed.InstanceID) if err != nil { @@ -707,6 +697,16 @@ func (b *backend) pathLoginUpdateEc2( policies := roleEntry.Policies rTagMaxTTL := time.Duration(0) + var roleTagResp *roleTagLoginResponse + if roleEntry.RoleTag != "" { + roleTagResp, err := b.handleRoleTagLogin(req.Storage, roleName, roleEntry, instance) + if err != nil { + return nil, err + } + if roleTagResp == nil { + return logical.ErrorResponse("failed to fetch and verify the role tag"), nil + } + } if roleTagResp != nil { // Role tag is enabled on the role. @@ -1054,7 +1054,7 @@ func (b *backend) pathLoginUpdateIam( method := data.Get("iam_http_request_method").(string) if method == "" { - return logical.ErrorResponse("missing method"), nil + return logical.ErrorResponse("missing iam_http_request_method"), nil } // In the future, might consider supporting GET @@ -1062,11 +1062,15 @@ func (b *backend) pathLoginUpdateIam( return logical.ErrorResponse("invalid iam_http_request_method; currently only 'POST' is supported"), nil } - rawUrl := data.Get("iam_request_url").(string) - if rawUrl == "" { + rawUrlB64 := data.Get("iam_request_url").(string) + if rawUrlB64 == "" { return logical.ErrorResponse("missing iam_request_url"), nil } - parsedUrl, err := url.Parse(rawUrl) + rawUrl, err := base64.StdEncoding.DecodeString(rawUrlB64) + if err != nil { + return logical.ErrorResponse("failed to base64 decode iam_request_url"), nil + } + parsedUrl, err := url.Parse(string(rawUrl)) if err != nil { return logical.ErrorResponse("error parsing iam_request_url"), nil } @@ -1150,7 +1154,6 @@ func (b *backend) pathLoginUpdateIam( } policies := roleEntry.Policies - rTagMaxTTL := time.Duration(0) inferredEntityType := "" inferredEntityId := "" @@ -1189,7 +1192,6 @@ func (b *backend) pathLoginUpdateIam( "client_arn": clientArn, "canonical_arn": canonicalArn, "auth_type": iamAuthType, - "role_tag_max_ttl": rTagMaxTTL.String(), "inferred_entity_type": inferredEntityType, "inferred_entity_id": inferredEntityId, "account_id": accountID, @@ -1217,9 +1219,6 @@ func (b *backend) pathLoginUpdateIam( if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < maxTTL { maxTTL = roleEntry.MaxTTL } - if rTagMaxTTL > time.Duration(0) && rTagMaxTTL < maxTTL { - maxTTL = rTagMaxTTL - } if shortestTTL > maxTTL { resp.AddWarning(fmt.Sprintf("Effective TTL of %q exceeded the effective max_ttl of %q; TTL value is capped accordingly", (shortestTTL / time.Second).String(), (maxTTL / time.Second).String())) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index f369b98ab904..1e44cebb327b 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -22,9 +22,9 @@ func pathRole(b *backend) *framework.Path { }, "auth_type": { Type: framework.TypeString, - Default: ec2AuthType, - Description: `The auth_type permitted to authenticate to this role. Cannot be -changed once set.`, + Default: iamAuthType, + Description: `The auth_type permitted to authenticate to this role. Must be one of +iam or ec2 and cannot be changed after role creation.`, }, "bound_ami_id": { Type: framework.TypeString, @@ -70,11 +70,13 @@ auth_type is ec2.`, Description: `When auth_type is iam, the AWS entity type to infer from the authenticated principal. The only supported value is ec2_instance, which will extract the EC2 instance ID from the -authenticated role and apply restrictions specific to EC2 instances (such as -role_tag). The configured EC2 client must be able to find the inferred instance -ID in the results, and the instance must be running. If unable to -determine the EC2 instance ID or unable to find the EC2 instance ID -among running instances, then authentication will fail.`, +authenticated role and apply the following restrictions specific to EC2 +instances: bound_ami_id, bound_account_id, bound_iam_role_arn, +bound_iam_instance_profile_arn, bound_vpc_id, bound_subnet_id. The configured +EC2 client must be able to find the inferred instance ID in the results, and the +instance must be running. If unable to determine the EC2 instance ID or unable +to find the EC2 instance ID among running instances, then authentication will +fail.`, }, "inferred_aws_region": { Type: framework.TypeString, @@ -100,8 +102,7 @@ subnet ID that matches the value specified by this parameter.`, field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using 'role//tag' endpoint. Defaults to an empty string, meaning that role tags are disabled. This -is only checked if auth_type is ec2 or -inferred_entity_type is ec2_instance`, +is only allowed if auth_type is ec2.`, }, "period": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -277,9 +278,11 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole result.BoundIamRoleARN = "" // Save the update - if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { + // TODO: Eventually we probably want to write these out to the storage backend + // Not doing that for now as this isn't guaranteed to be a safe code path to do so + /* if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { return nil, fmt.Errorf("failed to move instance profile ARN to bound_iam_instance_profile_arn field") - } + } */ } // Check if there was no pre-existing AuthType set (from older versions) @@ -287,9 +290,11 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole // then default to the original behavior of ec2 result.AuthType = ec2AuthType // and save the result - if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { + // TODO: Same as above, we probably want to write these out to the storage backend + // at some point + /* if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { return nil, fmt.Errorf("failed to save default auth_type") - } + } */ } return &result, nil @@ -421,7 +426,7 @@ func (b *backend) pathRoleCreateUpdate( return logical.ErrorResponse(fmt.Sprintf("unrecognized auth_type: %v", authTypeRaw.(string))), nil } } else if authTypeRaw.(string) != roleEntry.AuthType { - return logical.ErrorResponse("attempted to change auth_type on role"), nil + return logical.ErrorResponse("changing auth_type on a role is not allowed"), nil } } else if req.Operation == logical.CreateOperation { roleEntry.AuthType = data.Get("auth_type").(string) diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index c65ec2262d47..d8fa5dcd144a 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -25,6 +25,7 @@ func TestBackend_pathRoleEc2(t *testing.T) { } data := map[string]interface{}{ + "auth_type": "ec2", "policies": "p,q,r,s", "max_ttl": "2h", "bound_ami_id": "ami-abcd123", @@ -391,6 +392,7 @@ func TestAwsEc2_RoleCrud(t *testing.T) { } roleData := map[string]interface{}{ + "auth_type": "ec2", "bound_ami_id": "testamiid", "bound_account_id": "testaccountid", "bound_region": "testregion", @@ -500,6 +502,7 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) { } roleData := map[string]interface{}{ + "auth_type": "ec2", "bound_iam_instance_profile_arn": "testarn", "ttl": "10s", "max_ttl": "20s", diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 6e62c515e8eb..6a6cd781909b 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -556,7 +556,7 @@ passed to the `login` method: ``` $ vault write auth/aws/login role=dev-role-iam \ iam_http_request_method=POST \ - iam_request_url=https://sts.amazonaws.com/ \ + iam_request_url=aHR0cHM6Ly9zdHMuYW1hem9uYXdzLmNvbS8= \ iam_request_body=QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== \ iam_request_headers=eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ== ``` @@ -588,7 +588,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev- ``` curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev-role","pkcs7":"'$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')'","nonce":"5defbf9e-a8f9-3063-bdfc-54b7a42a1f95"}' -curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev", "iam_http_request_method": "POST", "iam_request_url": "https://sts.amazonaws.com/", "iam_request_body": "QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==", "iam_request_headers": "eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ==" }' +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev", "iam_http_request_method": "POST", "iam_request_url": "aHR0cHM6Ly9zdHMuYW1hem9uYXdzLmNvbS8=", "iam_request_body": "QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==", "iam_request_headers": "eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ==" }' ``` The response will be in JSON. For example: @@ -1780,8 +1780,9 @@ auth method only when inferring an ec2 instance.
    • iam_request_url required - HTTP URL used in the signed request. Most likely just - https://sts.amazonaws.com/ as most requests will probably use POST with + 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.
    From 6d22e0c477aebf02fbbe1348d2e77013f4fb2ecb Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 18 Apr 2017 22:34:20 -0400 Subject: [PATCH 16/20] Address more iam auth PR feedback --- builtin/credential/aws/cli.go | 2 +- builtin/credential/aws/path_config_client.go | 6 +- builtin/credential/aws/path_login.go | 10 +- builtin/credential/aws/path_role.go | 115 ++++++++++++----- website/source/docs/auth/aws-ec2.html.md | 125 +++++++++++-------- 5 files changed, 168 insertions(+), 90 deletions(-) diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index d7060e7f5364..c69187f5672b 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -117,7 +117,7 @@ If you need to explicitly pass in credentials, you would do it like this: Key/Value Pairs: mount=aws The mountpoint for the AWS credential provider. - Defaults to "aws-iam" + Defaults to "aws" aws_access_key_id= Explicitly specified AWS access key aws_secret_access_key= Explicitly specified AWS secret key aws_security_token= Security token for temporary credentials diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 2c6e1a6ca70e..3787aed3b1a6 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -225,8 +225,10 @@ func (b *backend) pathConfigClientCreateUpdate( return nil, err } - if err := req.Storage.Put(entry); err != nil { - return nil, err + if changedCreds || req.Operation == logical.CreateOperation { + if err := req.Storage.Put(entry); err != nil { + return nil, err + } } if changedCreds { diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 9f0e1e6324fd..bf50898405c3 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -76,8 +76,7 @@ presigned request. Currently, POST is the only supported value`, "iam_request_url": { Type: framework.TypeString, Description: `Base64-encoded full URL against which to make the AWS request -when using iam auth_type. If using a POST request with the action specified in the -body, this should just be "Lw==" ("/" base64-encoded).`, +when using iam auth_type.`, }, "iam_request_body": { @@ -934,7 +933,11 @@ func (b *backend) pathLoginRenewIam( if !ok { return nil, fmt.Errorf("no inferred entity ID in auth metadata") } - _, err := b.validateInstance(req.Storage, instanceID, roleEntry.InferredAWSRegion, req.Auth.Metadata["accountID"]) + instanceRegion, ok := req.Auth.Metadata["inferred_aws_region"] + if !ok { + return nil, fmt.Errorf("no inferred AWS region in auth metadata") + } + _, err := b.validateInstance(req.Storage, instanceID, instanceRegion, req.Auth.Metadata["accountID"]) if err != nil { return nil, fmt.Errorf("failed to verify instance ID %q: %v", instanceID, err) } @@ -1194,6 +1197,7 @@ func (b *backend) pathLoginUpdateIam( "auth_type": iamAuthType, "inferred_entity_type": inferredEntityType, "inferred_entity_id": inferredEntityId, + "inferred_aws_region": roleEntry.InferredAWSRegion, "account_id": accountID, }, InternalData: map[string]interface{}{ diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index a274ca96e586..1809f336acff 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -203,7 +203,49 @@ func (b *backend) lockedAWSRole(s logical.Storage, roleName string) (*awsRoleEnt b.roleMutex.RLock() defer b.roleMutex.RUnlock() - return b.nonLockedAWSRole(s, roleName) + roleEntry, err := b.nonLockedAWSRole(s, roleName) + if err != nil { + return nil, err + } + if roleEntry == nil { + return nil, nil + } + needUpgrade, err := upgradeRoleEntry(roleEntry) + if err != nil { + return nil, fmt.Errorf("error upgrading roleEntry: %v", err) + } + if needUpgrade { + // we need to get a write lock to upgrade the role entry, so we first need to release our read lock + // to prevent the write lock from deadlocking on it + b.roleMutex.RUnlock() + // now we need to ensure that we re-acquire the read lock so the above deferred RUnlock() doesn't + // cause a crash trying to unlock the already-unlocked mutex + defer b.roleMutex.RLock() + // Now grab a write lock on the mutex + b.roleMutex.Lock() + // and ensure we eventually unlock it + defer b.roleMutex.Unlock() + // Now that we have a R/W lock, we need to re-read the role entry in case it was + // written to between releasing the read lock and acquiring the write lock + roleEntry, err = b.nonLockedAWSRole(s, roleName) + if err != nil { + return nil, err + } + // somebody deleted the role, so no use in putting it back + if roleEntry == nil { + return nil, nil + } + // now re-check to see if we need to upgrade + if needUpgrade, err = upgradeRoleEntry(roleEntry); err != nil { + return nil, fmt.Errorf("error upgrading roleEntry: %v", err) + } + if needUpgrade { + if err = b.nonLockedSetAWSRole(s, roleName, roleEntry); err != nil { + return nil, fmt.Errorf("error saving upgraded roleEntry: %v", err) + } + } + } + return roleEntry, nil } // lockedSetAWSRole creates or updates a role in the storage. This method @@ -248,9 +290,41 @@ func (b *backend) nonLockedSetAWSRole(s logical.Storage, roleName string, return nil } +// If needed, updates the role entry and returns a bool indicating if it was updated +// (and thus needs to be persisted) +func upgradeRoleEntry(roleEntry *awsRoleEntry) (bool, error) { + if roleEntry == nil { + return false, fmt.Errorf("received nil roleEntry") + } + var upgraded bool + // Check if the value held by role ARN field is actually an instance profile ARN + if roleEntry.BoundIamRoleARN != "" && strings.Contains(roleEntry.BoundIamRoleARN, ":instance-profile/") { + // If yes, move it to the correct field + roleEntry.BoundIamInstanceProfileARN = roleEntry.BoundIamRoleARN + + // Reset the old field + roleEntry.BoundIamRoleARN = "" + + upgraded = true + } + + // Check if there was no pre-existing AuthType set (from older versions) + if roleEntry.AuthType == "" { + // then default to the original behavior of ec2 + roleEntry.AuthType = ec2AuthType + upgraded = true + } + + return upgraded, nil + +} + // nonLockedAWSRole returns the properties set on the given role. This method // does not acquire the read lock before reading the role from the storage. If // locking is desired, use lockedAWSRole instead. +// This method also does NOT check to see if a role upgrade is required. It is +// the responsibility of the caller to check if a role upgrade is required and, +// if so, to upgrade the role func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRoleEntry, error) { if roleName == "" { return nil, fmt.Errorf("missing role name") @@ -269,34 +343,6 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole return nil, err } - // Check if the value held by role ARN field is actually an instance profile ARN - if result.BoundIamRoleARN != "" && strings.Contains(result.BoundIamRoleARN, ":instance-profile/") { - // If yes, move it to the correct field - result.BoundIamInstanceProfileARN = result.BoundIamRoleARN - - // Reset the old field - result.BoundIamRoleARN = "" - - // Save the update - // TODO: Eventually we probably want to write these out to the storage backend - // Not doing that for now as this isn't guaranteed to be a safe code path to do so - /* if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { - return nil, fmt.Errorf("failed to move instance profile ARN to bound_iam_instance_profile_arn field") - } */ - } - - // Check if there was no pre-existing AuthType set (from older versions) - if result.AuthType == "" { - // then default to the original behavior of ec2 - result.AuthType = ec2AuthType - // and save the result - // TODO: Same as above, we probably want to write these out to the storage backend - // at some point - /* if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { - return nil, fmt.Errorf("failed to save default auth_type") - } */ - } - return &result, nil } @@ -372,6 +418,17 @@ func (b *backend) pathRoleCreateUpdate( } if roleEntry == nil { roleEntry = &awsRoleEntry{} + } else { + needUpdate, err := upgradeRoleEntry(roleEntry) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to update roleEntry: %v", err)), nil + } + if needUpdate { + err = b.nonLockedSetAWSRole(req.Storage, roleName, roleEntry) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to save upgraded roleEntry: %v", err)), nil + } + } } // Fetch and set the bound parameters. There can't be default values diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws-ec2.html.md index 690991e09c0c..a28df7f23995 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws-ec2.html.md @@ -78,7 +78,7 @@ the locations AWS has provided for you. Each signed AWS request includes the current timestamp to mitigate the risk of replay attacks. In addition, Vault allows you to require an additional header, -`X-Vault-AWSIAM-Server-ID`, to be present to mitigate against different types of replay +`X-Vault-AWS-IAM-Server-ID`, to be present to mitigate against different types of replay attacks (such as a signed `GetCallerIdentity` request stolen from a dev Vault instance and used to authenticate to a prod Vault instance). Vault further requires that this header be one of the headers included in the AWS signature @@ -513,7 +513,7 @@ $ vault write auth/aws/role/dev-role-iam auth_type=iam \ bound_iam_principal_arn=arn:aws:iam::123456789012:role/MyRole policies=prod,dev max_ttl=500h ``` -#### Configure a required X-Vault-AWSIAM-Server-ID Header (recommended) +#### Configure a required X-Vault-AWS-IAM-Server-ID Header (recommended) ``` $ vault write auth/aws/client/config iam_auth_header_vaule=vault.example.xom @@ -654,15 +654,24 @@ The response will be in JSON. For example:
    • access_key - required - AWS Access key with permissions to query EC2 DescribeInstances API. + optional + AWS Access key with permissions to query AWS APIs. The permissions + required depend on the specific configurations. If using the `iam` auth + method without inferencing, then no credentials are necessary. If using + the `ec2` auth method or using the `iam` auth method with inferencing, + then these credentials need access to `ec2:DescribeInstances`. If + additionally a `bound_iam_role` is specified, then these credentials + also need access to `iam:GetInstanceProfile`. If, however, an alterate + sts configuration is set for the target account, then the credentials + must be permissioned to call `sts:AssumeRole` on the configured role, + and that role must have the permissions described here.
    • secret_key - required - AWS Secret key with permissions to query EC2 DescribeInstances API. + optional + AWS Secret key with permissions to query AWS APIs.
      @@ -690,10 +699,10 @@ The response will be in JSON. For example:
    • iam_server_id_header_value optional - The value to require in the `X-Vault-AWSIAM-Server-ID` header as part of + The value to require in the `X-Vault-AWS-IAM-Server-ID` header as part of GetCallerIdentity requests that are used in the iam auth method. If not set, then no value is required or validated. If set, clients must - include an X-Vault-AWSIAM-Server-ID header in the headers of login + include an X-Vault-AWS-IAM-Server-ID header in the headers of login requests, and further this header must be among the signed headers validated by AWS. This is to protect against different types of replay attacks, for example a signed request sent to a dev server being resent @@ -1266,7 +1275,11 @@ The response will be in JSON. For example: are using the role registered using this endpoint, will be able to perform the login operation. Contraints can be specified on the role, that are applied on the instances or principals attempting to login. At least one - constraint should be specified on the role. + constraint should be specified on the role. The available constraints you + can choose are dependent on the `auth_type` of the role and, if the + `auth_type` is `iam`, then whether inferencing is enabled. A role will not + let you configure a constraint if it is not checked by the `auth_type` and + inferencing configuration of that role.
    Method
    @@ -1289,29 +1302,29 @@ The response will be in JSON. For example: auth_type optional The auth type permitted for this role. Valid choices are "ec2" or "iam". - If no value is chosen, then it will default to "ec2" for backwards - compatibility. Only those bindings applicable to the auth type chosen by - clients will be checked by Vault upon login. + If no value is specified, then it will default to "iam". Only those + bindings applicable to the auth type chosen by clients will be checked + by Vault upon login.
    • bound_ami_id optional - If set, defines a constraint on the EC2 instances that they -should be using the AMI ID specified by this parameter. This constraint is -checked during ec2 auth as well as the iam auth method only when inferring an EC2 -instance. + If set, defines a constraint on the EC2 instances that they should be + using the AMI ID specified by this parameter. This constraint is checked + during ec2 auth as well as the iam auth method only when inferring an + EC2 instance.
    • bound_account_id optional - If set, defines a constraint on the EC2 instances that the account ID -in its identity document to match the one specified by this parameter. This -constraint is checked during ec2 auth as well as the iam auth method only when -inferring an EC2 instance. + If set, defines a constraint on the EC2 instances that the account ID in + its identity document to match the one specified by this parameter. This + constraint is checked during ec2 auth as well as the iam auth method + only when inferring an EC2 instance.
      @@ -1319,11 +1332,9 @@ inferring an EC2 instance. bound_region optional If set, defines a constraint on the EC2 instances that the region in - its identity document to match the one specified by this parameter. This - constraint is only checked by the ec2 auth method (as IAM is a global - service so it doesn't make sense to bind by region with the iam auth - method, and binding by region is implied with the inferred AWS region - when inferring an EC2 instance). + its identity document must match the one specified by this parameter. This + constraint is only checked by the ec2 auth method as well as the iam + auth method only when inferring an ec2 instance..
      @@ -1331,7 +1342,9 @@ inferring an EC2 instance. bound_vpc_id optional If set, defines a constraint on the EC2 instance to be associated with - the VPC ID that matches the value specified by this parameter. + the VPC ID that matches the value specified by this parameter. This + constraint is only checked by the ec2 auth method as well as the iam + auth method only when inferring an ec2 instance.
      @@ -1339,31 +1352,34 @@ inferring an EC2 instance. bound_subnet_id optional If set, defines a constraint on the EC2 instance to be associated with - the subnet ID that matches the value specified by this parameter. + the subnet ID that matches the value specified by this parameter. This + constraint is only checked by the ec2 auth method as well as the iam + auth method only when inferring an ec2 instance.
    • bound_iam_role_arn optional - If set, defines a constraint on the authenticating EC2 instance that it -must match the IAM role ARN specified by this parameter. The value is -prefix-matched (as though it were a glob ending in `*`). The configured -IAM user or EC2 instance role must be allowed to execute the -`iam:GetInstanceProfile` action if this is specified. This constraint is checked -by the ec2 auth method as well as the iam auth method only when inferring an EC2 -instance. + If set, defines a constraint on the authenticating EC2 instance that it must + match the IAM role ARN specified by this parameter. The value is + prefix-matched (as though it were a glob ending in `*`). The configured IAM + user or EC2 instance role must be allowed to execute the + `iam:GetInstanceProfile` action if this is specified. This constraint is + checked by the ec2 auth method as well as the iam auth method only when + inferring an EC2 instance.
    • bound_iam_instance_profile_arn optional -If set, defines a constraint on the EC2 instances to be associated with an IAM -instance profile ARN which has a prefix that matches the value specified by -this parameter. The value is prefix-matched (as though it were a glob ending -in `*`). This constraint is checked by the ec2 auth method as well as the iam -auth method only when inferring an ec2 instance. + If set, defines a constraint on the EC2 instances to be associated with + an IAM instance profile ARN which has a prefix that matches the value + specified by this parameter. The value is prefix-matched (as though it + were a glob ending in `*`). This constraint is checked by the ec2 auth + method as well as the iam auth method only when inferring an ec2 + instance.
      @@ -1374,8 +1390,8 @@ auth method only when inferring an ec2 instance. field should be the 'key' of the tag on the EC2 instance. The 'value' of the tag should be generated using `role//tag` endpoint. Defaults to an empty string, meaning that role tags are disabled. This - constraint is valid only with the ec2 auth method and is not checked - when using the iam auth method, even when inferencing is enabled. + constraint is valid only with the ec2 auth method and is not allowed + when an auth_type is iam.
      @@ -1695,11 +1711,10 @@ auth method only when inferring an ec2 instance. Fetch a token. This endpoint verifies the pkcs7 signature of the instance identity document or the signature of the signed GetCallerIdentity request. With the ec2 auth method, or when inferring an EC2 instance, verifies that - the instance is actually in a running state. - Cross checks the constraints defined on the role with which the login is being - performed. With the ec2 auth method, as an alternative to pkcs7 signature, - the identity document along - with its RSA digest can be supplied to this endpoint. + the instance is actually in a running state. Cross checks the constraints + defined on the role with which the login is being performed. With the ec2 + auth method, as an alternative to pkcs7 signature, the identity document + along with its RSA digest can be supplied to this endpoint.
    Method
    @@ -1782,9 +1797,9 @@ auth method only when inferring an ec2 instance. iam_request_url required 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. + `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.
      @@ -1792,9 +1807,9 @@ auth method only when inferring an ec2 instance. iam_request_body required Base64-encoded body of the signed request. Most likely - QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== + `QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ==` which is the base64 encoding of - Action=GetCallerIdentity&Version=2011-06-15. This is required + `Action=GetCallerIdentity&Version=2011-06-15`. This is required when using the iam auth method.
    @@ -1805,10 +1820,10 @@ auth method only when inferring an ec2 instance. Base64-encoded, JSON-serialized representation of the HTTP request headers. The JSON serialization assumes that each header key maps to an array of string values (though the length of that array will probably - only be one). If the iam_server_id_header_value is configured in Vault for - the aws auth mount, then the headers must include the - X-Vault-AWSIAM-Server-Id header, its value must match the value - configured, and the header must be included in the signed headers. This + only be one). If the `iam_server_id_header_value` is configured in Vault + for the aws auth mount, then the headers must include the + X-Vault-AWS-IAM-Server-ID header, its value must match the value + configured, and the header must be included in the signed headers. This is required when using the iam auth method. From c98898c863380befd8f90c589432c816368b23be Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Wed, 19 Apr 2017 00:04:56 -0400 Subject: [PATCH 17/20] Rename aws-ec2.html.md to aws.html.md Per PR feedback, to go along with new backend name. --- website/source/docs/auth/{aws-ec2.html.md => aws.html.md} | 0 website/source/layouts/docs.erb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename website/source/docs/auth/{aws-ec2.html.md => aws.html.md} (100%) diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws.html.md similarity index 100% rename from website/source/docs/auth/aws-ec2.html.md rename to website/source/docs/auth/aws.html.md diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8cf20f20a6f4..f6f07b86a511 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -269,7 +269,7 @@ > - AWS + AWS > From 91a1ad7327b5e784cafc4295701b5bdde1b39bad Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Wed, 19 Apr 2017 23:43:28 -0400 Subject: [PATCH 18/20] Add MountType to logical.Request --- logical/request.go | 5 +++++ vault/core_test.go | 7 +++++-- vault/router.go | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/logical/request.go b/logical/request.go index a3f67151892c..c41b1dc09116 100644 --- a/logical/request.go +++ b/logical/request.go @@ -82,6 +82,11 @@ type Request struct { // request path with the MountPoint trimmed off. MountPoint string `json:"mount_point" structs:"mount_point" mapstructure:"mount_point"` + // MountType is provided so that a logical backend can make decisions + // based on the specific mount type (e.g., if a mount type has different + // aliases, generating different defaults depending on the alias) + MountType string `json:"mount_type" structs:"mount_type" mapstructure:"mount_type"` + // WrapInfo contains requested response wrapping parameters WrapInfo *RequestWrapInfo `json:"wrap_info" structs:"wrap_info" mapstructure:"wrap_info"` diff --git a/vault/core_test.go b/vault/core_test.go index 09f1ae976704..ced18cdf70bb 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -1956,7 +1956,7 @@ path "secret/*" { } } -func TestCore_HandleRequest_MountPoint(t *testing.T) { +func TestCore_HandleRequest_MountPointType(t *testing.T) { noop := &NoopBackend{ Response: &logical.Response{}, } @@ -1986,13 +1986,16 @@ func TestCore_HandleRequest_MountPoint(t *testing.T) { t.Fatalf("err: %v", err) } - // Verify Path and MountPoint + // Verify Path, MountPoint, and MountType if noop.Requests[0].Path != "test" { t.Fatalf("bad: %#v", noop.Requests) } if noop.Requests[0].MountPoint != "foo/" { t.Fatalf("bad: %#v", noop.Requests) } + if noop.Requests[0].MountType != "noop" { + t.Fatalf("bad: %#v", noop.Requests) + } } func TestCore_Standby_Rotate(t *testing.T) { diff --git a/vault/router.go b/vault/router.go index dd2d23220b3f..c1cb5c75d011 100644 --- a/vault/router.go +++ b/vault/router.go @@ -257,6 +257,7 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica originalPath := req.Path req.Path = strings.TrimPrefix(req.Path, mount) req.MountPoint = mount + req.MountType = re.mountEntry.Type if req.Path == "/" { req.Path = "" } @@ -304,6 +305,7 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica defer func() { req.Path = originalPath req.MountPoint = "" + req.MountType = "" req.Connection = originalConn req.ID = originalReqID req.Storage = nil From 09057461a951217ed380357b17b4e314987fdac8 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 20 Apr 2017 00:06:54 -0400 Subject: [PATCH 19/20] Make default aws auth_type dependent upon MountType When MountType is aws-ec2, default to ec2 auth_type for backwards compatibility with legacy roles. Otherwise, default to iam. --- builtin/credential/aws/path_role.go | 29 +++++++++++++++------------- website/source/docs/auth/aws.html.md | 7 ++++--- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 1809f336acff..f6c19f23790f 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -21,8 +21,7 @@ func pathRole(b *backend) *framework.Path { Description: "Name of the role.", }, "auth_type": { - Type: framework.TypeString, - Default: iamAuthType, + Type: framework.TypeString, Description: `The auth_type permitted to authenticate to this role. Must be one of iam or ec2 and cannot be changed after role creation.`, }, @@ -201,9 +200,10 @@ func (b *backend) lockedAWSRole(s logical.Storage, roleName string) (*awsRoleEnt } b.roleMutex.RLock() - defer b.roleMutex.RUnlock() - roleEntry, err := b.nonLockedAWSRole(s, roleName) + // we manually unlock rather than defer the unlock because we might need to grab + // a read/write lock in the upgrade path + b.roleMutex.RUnlock() if err != nil { return nil, err } @@ -215,15 +215,7 @@ func (b *backend) lockedAWSRole(s logical.Storage, roleName string) (*awsRoleEnt return nil, fmt.Errorf("error upgrading roleEntry: %v", err) } if needUpgrade { - // we need to get a write lock to upgrade the role entry, so we first need to release our read lock - // to prevent the write lock from deadlocking on it - b.roleMutex.RUnlock() - // now we need to ensure that we re-acquire the read lock so the above deferred RUnlock() doesn't - // cause a crash trying to unlock the already-unlocked mutex - defer b.roleMutex.RLock() - // Now grab a write lock on the mutex b.roleMutex.Lock() - // and ensure we eventually unlock it defer b.roleMutex.Unlock() // Now that we have a R/W lock, we need to re-read the role entry in case it was // written to between releasing the read lock and acquiring the write lock @@ -475,6 +467,8 @@ func (b *backend) pathRoleCreateUpdate( // auth_type is a special case as it's immutable and can't be changed once a role is created if authTypeRaw, ok := data.GetOk("auth_type"); ok { + // roleEntry.AuthType should only be "" when it's a new role; existing roles without an + // auth_type should have already been upgraded to have one before we get here if roleEntry.AuthType == "" { switch authTypeRaw.(string) { case ec2AuthType, iamAuthType: @@ -486,7 +480,16 @@ func (b *backend) pathRoleCreateUpdate( return logical.ErrorResponse("changing auth_type on a role is not allowed"), nil } } else if req.Operation == logical.CreateOperation { - roleEntry.AuthType = data.Get("auth_type").(string) + switch req.MountType { + // maintain backwards compatibility for old aws-ec2 auth types + case "aws-ec2": + roleEntry.AuthType = ec2AuthType + // but default to iamAuth for new mounts going forward + case "aws": + roleEntry.AuthType = iamAuthType + default: + roleEntry.AuthType = iamAuthType + } } allowEc2Binds := roleEntry.AuthType == ec2AuthType diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index a28df7f23995..107ad2e89311 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -1302,9 +1302,10 @@ The response will be in JSON. For example: auth_type optional The auth type permitted for this role. Valid choices are "ec2" or "iam". - If no value is specified, then it will default to "iam". Only those - bindings applicable to the auth type chosen by clients will be checked - by Vault upon login. + If no value is specified, then it will default to "iam" (except for + legacy `aws-ec2` auth types, for which it will default to "ec2"). Only + those bindings applicable to the auth type chosen will be allowed to be + configured on the role.
      From 28680fe1579e461162d4c128ddfe8644649ccc45 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 20 Apr 2017 22:02:36 -0400 Subject: [PATCH 20/20] Pass MountPoint and MountType back up to the core Previously the request router reset the MountPoint and MountType back to the empty string before returning to the core. This ensures they get set back to the correct values. --- vault/router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/router.go b/vault/router.go index c1cb5c75d011..5a90dfa0e2db 100644 --- a/vault/router.go +++ b/vault/router.go @@ -304,8 +304,8 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica // Reset the request before returning defer func() { req.Path = originalPath - req.MountPoint = "" - req.MountType = "" + req.MountPoint = mount + req.MountType = re.mountEntry.Type req.Connection = originalConn req.ID = originalReqID req.Storage = nil