From dba2de57de42a8b5175d7aa1388965566e8d08c9 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 6 Apr 2017 17:00:50 -0400 Subject: [PATCH 01/53] Change storage of entries from colons to hyphens and add a lookup/migration path Still TODO: tests on migration path Fixes #2552 --- builtin/logical/pki/cert_util.go | 42 ++++++++++++++++++++++-- builtin/logical/pki/crl_util.go | 3 +- builtin/logical/pki/path_intermediate.go | 3 +- builtin/logical/pki/path_issue_sign.go | 5 +-- builtin/logical/pki/path_root.go | 5 +-- builtin/logical/pki/secret_certs.go | 5 +-- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index d9425fc5a0aa..15188ced9708 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -185,32 +185,68 @@ func fetchCAInfo(req *logical.Request) (*caInfoBundle, error) { // separate pathing for CA, CRL, and revoked certificates. func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) { var path string + var err error + var certEntry *logical.StorageEntry switch { // Revoked goes first as otherwise ca/crl get hardcoded paths which fail if // we actually want revocation info case strings.HasPrefix(prefix, "revoked/"): - path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) + path = "revoked/" + strings.Replace(strings.ToLower(serial), ":", "-", -1) case serial == "ca": path = "ca" case serial == "crl": path = "crl" + default: + path = "certs/" + strings.Replace(strings.ToLower(serial), ":", "-", -1) + } + + certEntry, err = req.Storage.Get(path) + if err != nil { + return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)} + } + if certEntry != nil { + if certEntry.Value == nil || len(certEntry.Value) == 0 { + return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} + } + return certEntry, nil + } + + // No point checking these, no old/new style colons/hyphens + if path == "ca" || path == "crl" { + return nil, nil + } + + // Save the desired path + desiredPath := path + + // If we get here we need to check for old-style paths using colons + switch { + case strings.HasPrefix(prefix, "revoked/"): + path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) default: path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) } - certEntry, err := req.Storage.Get(path) + certEntry, err = req.Storage.Get(path) if err != nil { return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)} } if certEntry == nil { return nil, nil } - if certEntry.Value == nil || len(certEntry.Value) == 0 { return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} } + certEntry.Key = desiredPath + if err = req.Storage.Put(certEntry); err != nil { + return nil, errutil.InternalError{Err: fmt.Sprintf("error saving certificate with serial %s to new location", serial)} + } + if err = req.Storage.Delete(path); err != nil { + return nil, errutil.InternalError{Err: fmt.Sprintf("error deleting certificate with serial %s from old location", serial)} + } + return certEntry, nil } diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index aa15f6cc617f..4519a0805918 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "fmt" + "strings" "time" "github.com/hashicorp/vault/helper/errutil" @@ -86,7 +87,7 @@ func revokeCert(b *backend, req *logical.Request, serial string, fromLease bool) revInfo.RevocationTime = currTime.Unix() revInfo.RevocationTimeUTC = currTime.UTC() - revEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo) + revEntry, err = logical.StorageEntryJSON("revoked/"+strings.ToLower(strings.Replace(serial, ":", "-", -1)), revInfo) if err != nil { return nil, fmt.Errorf("Error creating revocation entry") } diff --git a/builtin/logical/pki/path_intermediate.go b/builtin/logical/pki/path_intermediate.go index 6bac720240ec..6887e97ae104 100644 --- a/builtin/logical/pki/path_intermediate.go +++ b/builtin/logical/pki/path_intermediate.go @@ -3,6 +3,7 @@ package pki import ( "encoding/base64" "fmt" + "strings" "github.com/hashicorp/vault/helper/certutil" "github.com/hashicorp/vault/helper/errutil" @@ -196,7 +197,7 @@ func (b *backend) pathSetSignedIntermediate( return nil, err } - entry.Key = "certs/" + cb.SerialNumber + entry.Key = "certs/" + strings.ToLower(strings.Replace(cb.SerialNumber, ":", "-", -1)) entry.Value = inputBundle.CertificateBytes err = req.Storage.Put(entry) if err != nil { diff --git a/builtin/logical/pki/path_issue_sign.go b/builtin/logical/pki/path_issue_sign.go index 429d96315b7b..3759d4d99c90 100644 --- a/builtin/logical/pki/path_issue_sign.go +++ b/builtin/logical/pki/path_issue_sign.go @@ -3,6 +3,7 @@ package pki import ( "encoding/base64" "fmt" + "strings" "time" "github.com/hashicorp/vault/helper/certutil" @@ -242,11 +243,11 @@ func (b *backend) pathIssueSignCert( if !role.NoStore { err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + cb.SerialNumber, + Key: "certs/" + strings.ToLower(strings.Replace(cb.SerialNumber, ":", "-", -1)), Value: parsedBundle.CertificateBytes, }) if err != nil { - return nil, fmt.Errorf("Unable to store certificate locally: %v", err) + return nil, fmt.Errorf("unable to store certificate locally: %v", err) } } diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index c9a8cf297e78..c10d462b0751 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -3,6 +3,7 @@ package pki import ( "encoding/base64" "fmt" + "strings" "github.com/hashicorp/vault/helper/errutil" "github.com/hashicorp/vault/logical" @@ -145,7 +146,7 @@ func (b *backend) pathCAGenerateRoot( // Also store it as just the certificate identified by serial number, so it // can be revoked err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + cb.SerialNumber, + Key: "certs/" + strings.ToLower(strings.Replace(cb.SerialNumber, ":", "-", -1)), Value: parsedBundle.CertificateBytes, }) if err != nil { @@ -277,7 +278,7 @@ func (b *backend) pathCASignIntermediate( } err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + cb.SerialNumber, + Key: "certs/" + strings.ToLower(strings.Replace(cb.SerialNumber, ":", "-", -1)), Value: parsedBundle.CertificateBytes, }) if err != nil { diff --git a/builtin/logical/pki/secret_certs.go b/builtin/logical/pki/secret_certs.go index fbc653d1daab..32f6f4296cc0 100644 --- a/builtin/logical/pki/secret_certs.go +++ b/builtin/logical/pki/secret_certs.go @@ -2,7 +2,6 @@ package pki import ( "fmt" - "strings" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -46,10 +45,8 @@ func (b *backend) secretCredsRevoke( return nil, fmt.Errorf("could not find serial in internal secret data") } - serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1) - b.revokeStorageLock.Lock() defer b.revokeStorageLock.Unlock() - return revokeCert(b, req, serial, true) + return revokeCert(b, req, serialInt.(string), true) } From ab03ffb047eec24f155592a19860809c4fcc4ef0 Mon Sep 17 00:00:00 2001 From: Cameron Stokes Date: Thu, 20 Apr 2017 15:18:55 -0700 Subject: [PATCH 02/53] [docs] Fix typo in Transit API docs. --- website/source/api/secret/transit/index.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/api/secret/transit/index.html.md b/website/source/api/secret/transit/index.html.md index ef991556d946..37c7a7ac26b1 100644 --- a/website/source/api/secret/transit/index.html.md +++ b/website/source/api/secret/transit/index.html.md @@ -39,7 +39,7 @@ values set here cannot be changed after key creation. be unique** or it will compromise the security of your key, and the key space for nonces is 96 bit -- not as large as the AES key itself. -- `derived` `(bool: false)` – Specifies if key derivation kist be used. If +- `derived` `(bool: false)` – Specifies if key derivation is to be used. If enabled, all encrypt/decrypt requests to this named key must provide a context which is used for key derivation. From 028ea4cb0fdd3078851eee1408a93531f7b0f7e3 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 21 Apr 2017 15:37:43 -0400 Subject: [PATCH 03/53] List should use a trailing slash --- vault/token_store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/token_store.go b/vault/token_store.go index 055b43905cf9..8e020077bce8 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -1120,7 +1120,7 @@ func (ts *TokenStore) handleTidy(req *logical.Request, data *framework.FieldData // Scan through the secondary index entries; if there is an entry // with the token's salt ID at the end, remove it for _, parent := range parentList { - children, err := ts.view.List(parentPrefix + parent) + children, err := ts.view.List(parentPrefix + parent + "/") if err != nil { tidyErrors = multierror.Append(tidyErrors, fmt.Errorf("failed to read child index entry: %v", err)) continue From b20afce73fb5232f9696c9936ba7a4c2a49b3cb5 Mon Sep 17 00:00:00 2001 From: Matthew Gallagher Date: Sat, 22 Apr 2017 16:56:01 -0400 Subject: [PATCH 04/53] Remove mention of Darwin mlock support from docs. (#2624) --- website/source/docs/configuration/index.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/configuration/index.html.md b/website/source/docs/configuration/index.html.md index a18474b73e6a..75a2b02ffc03 100644 --- a/website/source/docs/configuration/index.html.md +++ b/website/source/docs/configuration/index.html.md @@ -74,8 +74,8 @@ to specify where the configuration is. Disabling `mlock` is not recommended unless the systems running Vault only use encrypted swap or do not use swap at all. Vault only supports memory - locking on UNIX-like systems (Linux, FreeBSD, Darwin, etc). Non-UNIX like - systems (e.g. Windows, NaCL, Android) lack the primitives to keep a + locking on UNIX-like systems that support the mlock() syscall (Linux, FreeBSD, etc). + Non UNIX-like systems (e.g. Windows, NaCL, Android) lack the primitives to keep a process's entire memory address space from spilling to disk and is therefore automatically disabled on unsupported platforms. From 7bce60290f2103365c4fedd9e28fbb28c0930fa1 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 24 Apr 2017 09:35:36 -0400 Subject: [PATCH 05/53] Add linux/arm64 build. Fixes #2630 --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index e390dea0f149..6a1cb51f6d1f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -21,7 +21,7 @@ GIT_DIRTY="$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)" # Determine the arch/os combos we're building for XC_ARCH=${XC_ARCH:-"386 amd64"} XC_OS=${XC_OS:-linux darwin windows freebsd openbsd netbsd solaris} -XC_OSARCH=${XC_OSARCH:-"linux/386 linux/amd64 linux/arm darwin/386 darwin/amd64 windows/386 windows/amd64 freebsd/386 freebsd/amd64 freebsd/arm openbsd/386 openbsd/amd64 openbsd/arm netbsd/386 netbsd/amd64 netbsd/arm solaris/amd64"} +XC_OSARCH=${XC_OSARCH:-"linux/386 linux/amd64 linux/arm linux/arm64 darwin/386 darwin/amd64 windows/386 windows/amd64 freebsd/386 freebsd/amd64 freebsd/arm openbsd/386 openbsd/amd64 openbsd/arm netbsd/386 netbsd/amd64 netbsd/arm solaris/amd64"} GOPATH=${GOPATH:-$(go env GOPATH)} case $(uname) in From 5a934e6b2fe000554c8483ff106c35462b0756d8 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Mon, 24 Apr 2017 15:15:50 -0400 Subject: [PATCH 06/53] Create unified aws auth backend (#2441) * 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. * 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. * Add missing aws auth handler to CLI This was omitted from the previous commit * 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. * Update docs for the aws auth backend * Refactor aws bind validation * 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. * 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. * Fix typo in aws auth cli help * Respond to PR feedback * More PR feedback * Respond to additional PR feedback * Address more feedback on aws auth PR * Make aws auth_type immutable per role * Address more aws auth PR feedback * Address more iam auth PR feedback * Rename aws-ec2.html.md to aws.html.md Per PR feedback, to go along with new backend name. * Add MountType to logical.Request * 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. * 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. --- builtin/credential/aws-ec2/path_role_test.go | 179 ---- .../credential/{aws-ec2 => aws}/backend.go | 2 +- .../{aws-ec2 => aws}/backend_test.go | 381 +++++--- builtin/credential/aws/cli.go | 129 +++ builtin/credential/{aws-ec2 => aws}/client.go | 61 +- .../path_config_certificate.go | 2 +- .../{aws-ec2 => aws}/path_config_client.go | 70 +- .../credential/aws/path_config_client_test.go | 76 ++ .../{aws-ec2 => aws}/path_config_sts.go | 2 +- .../path_config_tidy_identity_whitelist.go | 2 +- .../path_config_tidy_roletag_blacklist.go | 2 +- .../path_identity_whitelist.go | 2 +- .../credential/{aws-ec2 => aws}/path_login.go | 849 ++++++++++++++---- builtin/credential/aws/path_login_test.go | 140 +++ .../credential/{aws-ec2 => aws}/path_role.go | 307 ++++++- .../{aws-ec2 => aws}/path_role_tag.go | 2 +- builtin/credential/aws/path_role_test.go | 555 ++++++++++++ .../path_roletag_blacklist.go | 2 +- .../path_tidy_identity_whitelist.go | 2 +- .../path_tidy_roletag_blacklist.go | 2 +- cli/commands.go | 5 +- logical/request.go | 5 + vault/auth.go | 7 + vault/core_test.go | 7 +- vault/router.go | 4 +- .../auth/{aws-ec2.html.md => aws.html.md} | 656 +++++++++++--- website/source/layouts/docs.erb | 4 +- 27 files changed, 2772 insertions(+), 683 deletions(-) delete mode 100644 builtin/credential/aws-ec2/path_role_test.go rename builtin/credential/{aws-ec2 => aws}/backend.go (99%) rename builtin/credential/{aws-ec2 => aws}/backend_test.go (80%) create mode 100644 builtin/credential/aws/cli.go rename builtin/credential/{aws-ec2 => aws}/client.go (77%) rename builtin/credential/{aws-ec2 => aws}/path_config_certificate.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_config_client.go (67%) create mode 100644 builtin/credential/aws/path_config_client_test.go rename builtin/credential/{aws-ec2 => aws}/path_config_sts.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_config_tidy_identity_whitelist.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_config_tidy_roletag_blacklist.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_identity_whitelist.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_login.go (55%) create mode 100644 builtin/credential/aws/path_login_test.go rename builtin/credential/{aws-ec2 => aws}/path_role.go (62%) rename builtin/credential/{aws-ec2 => aws}/path_role_tag.go (99%) create mode 100644 builtin/credential/aws/path_role_test.go rename builtin/credential/{aws-ec2 => aws}/path_roletag_blacklist.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_tidy_identity_whitelist.go (99%) rename builtin/credential/{aws-ec2 => aws}/path_tidy_roletag_blacklist.go (99%) rename website/source/docs/auth/{aws-ec2.html.md => aws.html.md} (60%) diff --git a/builtin/credential/aws-ec2/path_role_test.go b/builtin/credential/aws-ec2/path_role_test.go deleted file mode 100644 index 1ef87c28449c..000000000000 --- a/builtin/credential/aws-ec2/path_role_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package awsec2 - -import ( - "reflect" - "testing" - "time" - - "github.com/hashicorp/vault/logical" -) - -func TestAwsEc2_RoleCrud(t *testing.T) { - var err error - var resp *logical.Response - 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) - } - - role1Data := map[string]interface{}{ - "bound_vpc_id": "testvpcid", - "allow_instance_migration": true, - "policies": "testpolicy1,testpolicy2", - } - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/role1", - Data: role1Data, - } - - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - roleData := map[string]interface{}{ - "bound_ami_id": "testamiid", - "bound_account_id": "testaccountid", - "bound_region": "testregion", - "bound_iam_role_arn": "testiamrolearn", - "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", - "bound_subnet_id": "testsubnetid", - "bound_vpc_id": "testvpcid", - "role_tag": "testtag", - "allow_instance_migration": true, - "ttl": "10m", - "max_ttl": "20m", - "policies": "testpolicy1,testpolicy2", - "disallow_reauthentication": true, - "hmac_key": "testhmackey", - "period": "1m", - } - - roleReq.Path = "role/testrole" - roleReq.Data = roleData - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - roleReq.Operation = logical.ReadOperation - - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - expected := map[string]interface{}{ - "bound_ami_id": "testamiid", - "bound_account_id": "testaccountid", - "bound_region": "testregion", - "bound_iam_role_arn": "testiamrolearn", - "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", - "bound_subnet_id": "testsubnetid", - "bound_vpc_id": "testvpcid", - "role_tag": "testtag", - "allow_instance_migration": true, - "ttl": time.Duration(600), - "max_ttl": time.Duration(1200), - "policies": []string{"default", "testpolicy1", "testpolicy2"}, - "disallow_reauthentication": true, - "period": time.Duration(60), - } - - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data) - } - - roleData["bound_vpc_id"] = "newvpcid" - roleReq.Operation = logical.UpdateOperation - - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - roleReq.Operation = logical.ReadOperation - - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - expected["bound_vpc_id"] = "newvpcid" - - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data) - } - - roleReq.Operation = logical.DeleteOperation - - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - if resp != nil { - t.Fatalf("failed to delete role entry") - } -} - -func TestAwsEc2_RoleDurationSeconds(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) - } - - roleData := map[string]interface{}{ - "bound_iam_instance_profile_arn": "testarn", - "ttl": "10s", - "max_ttl": "20s", - "period": "30s", - } - - roleReq := &logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "role/testrole", - Data: roleData, - } - - resp, err := b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - roleReq.Operation = logical.ReadOperation - - resp, err = b.HandleRequest(roleReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("resp: %#v, err: %v", resp, err) - } - - if int64(resp.Data["ttl"].(time.Duration)) != 10 { - t.Fatalf("bad: period; expected: 10, actual: %d", resp.Data["ttl"]) - } - if int64(resp.Data["max_ttl"].(time.Duration)) != 20 { - t.Fatalf("bad: period; expected: 20, actual: %d", resp.Data["max_ttl"]) - } - if int64(resp.Data["period"].(time.Duration)) != 30 { - t.Fatalf("bad: period; expected: 30, actual: %d", resp.Data["period"]) - } -} diff --git a/builtin/credential/aws-ec2/backend.go b/builtin/credential/aws/backend.go similarity index 99% rename from builtin/credential/aws-ec2/backend.go rename to builtin/credential/aws/backend.go index 33ef4a80ce06..fbd62c72a056 100644 --- a/builtin/credential/aws-ec2/backend.go +++ b/builtin/credential/aws/backend.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "sync" diff --git a/builtin/credential/aws-ec2/backend_test.go b/builtin/credential/aws/backend_test.go similarity index 80% rename from builtin/credential/aws-ec2/backend_test.go rename to builtin/credential/aws/backend_test.go index 3125656d25b7..a539fbac18be 100644 --- a/builtin/credential/aws-ec2/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" @@ -29,11 +34,12 @@ 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", } resp, err := b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/abcd-123", Storage: storage, Data: data, @@ -100,7 +106,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, @@ -683,132 +689,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() @@ -825,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", @@ -901,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", @@ -966,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", @@ -1068,7 +951,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) == "" { @@ -1156,6 +1039,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { // 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", @@ -1164,7 +1048,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { } roleReq := &logical.Request{ - Operation: logical.UpdateOperation, + Operation: logical.CreateOperation, Path: "role/" + roleName, Storage: storage, Data: data, @@ -1183,6 +1067,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { } // 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) @@ -1210,7 +1095,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatalf("bad: expected error response: resp:%#v\nerr:%v", resp, err) } - // Place the correct IAM Role ARN + // place the correct IAM role ARN data["bound_iam_role_arn"] = iamARN resp, err = b.HandleRequest(roleReq) if err != nil || (resp != nil && resp.IsError()) { @@ -1280,7 +1165,6 @@ func TestBackend_pathStsConfig(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Backend(config) if err != nil { t.Fatal(err) @@ -1289,7 +1173,6 @@ func TestBackend_pathStsConfig(t *testing.T) { if err != nil { t.Fatal(err) } - stsReq := &logical.Request{ Operation: logical.CreateOperation, Storage: storage, @@ -1389,3 +1272,233 @@ func TestBackend_pathStsConfig(t *testing.T) { t.Fatalf("no entries should be present") } } + +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{}{ + "iam_http_request_method": request.Method, + "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, + }, 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 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) { + // 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", "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() + if err != nil { + fmt.Println("failed to create session,", err) + return + } + + stsService := sts.New(awsSession) + 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{}{ + "iam_server_id_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", + "auth_type": iamAuthType, + } + roleRequest := &logical.Request{ + Operation: logical.CreateOperation, + 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) + } + + // 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", + } + roleRequestEc2 := &logical.Request{ + Operation: logical.CreateOperation, + 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) + } + + // 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: didn't fail 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(iamServerIdHeader, "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(iamServerIdHeader, 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"] = "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 { + 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..c69187f5672b --- /dev/null +++ b/builtin/credential/aws/cli.go @@ -0,0 +1,129 @@ +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 = "" + } + + // 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"], + 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") + } + + // Use the credentials we've found to construct an STS session + 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) + + // Inject the required auth header value, if suplied, and then sign the request including that header + if 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 + } + requestBody, err := ioutil.ReadAll(stsRequest.HTTPRequest.Body) + if err != nil { + return "", err + } + method := stsRequest.HTTPRequest.Method + targetUrl := base64.StdEncoding.EncodeToString([]byte(stsRequest.HTTPRequest.URL.String())) + 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{}{ + "iam_http_request_method": method, + "iam_request_url": targetUrl, + "iam_request_headers": headers, + "iam_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 +} + +func (h *CLIHandler) Help() string { + help := ` +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 +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" + 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-AWS-IAM-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-ec2/client.go b/builtin/credential/aws/client.go similarity index 77% rename from builtin/credential/aws-ec2/client.go rename to builtin/credential/aws/client.go index 59c33f2b1cfb..1647f4527b7f 100644 --- a/builtin/credential/aws-ec2/client.go +++ b/builtin/credential/aws/client.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" @@ -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 string) (*aws.Config, error) { +func (b *backend) getRawClientConfig(s logical.Storage, region, clientType string) (*aws.Config, error) { credsConfig := &awsutil.CredentialsConfig{ Region: region, } @@ -34,8 +34,13 @@ 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 != "" { + switch { + case clientType == "ec2" && config.Endpoint != "": endpoint = aws.String(config.Endpoint) + case clientType == "iam" && config.IAMEndpoint != "": + endpoint = aws.String(config.IAMEndpoint) + case clientType == "sts" && config.STSEndpoint != "": + endpoint = aws.String(config.STSEndpoint) } credsConfig.AccessKey = config.AccessKey @@ -61,25 +66,35 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config }, 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 string, stsRole string) (*aws.Config, error) { - config, err := b.getClientConfig(s, region) +// 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.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(config), 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 } @@ -128,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) - } else { - awsConfig, err = b.getStsClientConfig(s, region, stsRole) - } + awsConfig, err = b.getClientConfig(s, region, stsRole, "ec2") if err != nil { return nil, err @@ -179,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) - } else { - awsConfig, err = b.getStsClientConfig(s, region, stsRole) - } + awsConfig, err = b.getClientConfig(s, region, stsRole, "iam") if err != nil { return nil, err diff --git a/builtin/credential/aws-ec2/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go similarity index 99% rename from builtin/credential/aws-ec2/path_config_certificate.go rename to builtin/credential/aws/path_config_certificate.go index 4613b212112a..0c026ed45339 100644 --- a/builtin/credential/aws-ec2/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-ec2/path_config_client.go b/builtin/credential/aws/path_config_client.go similarity index 67% rename from builtin/credential/aws-ec2/path_config_client.go rename to builtin/credential/aws/path_config_client.go index 93f20219de97..3787aed3b1a6 100644 --- a/builtin/credential/aws-ec2/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.", + }, + + "iam_server_id_header_value": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "Value to require in the X-Vault-AWS-IAM-Server-ID request header", + }, }, ExistenceCheck: b.pathConfigClientExistenceCheck, @@ -162,6 +180,41 @@ 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) { + // 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 { + configEntry.STSEndpoint = data.Get("sts_endpoint").(string) + } + + headerValStr, ok := data.GetOk("iam_server_id_header_value") + if ok { + if configEntry.IAMServerIdHeaderValue != headerValStr.(string) { + // NOT setting changedCreds here, since this isn't really cached + configEntry.IAMServerIdHeaderValue = headerValStr.(string) + } + } else if req.Operation == logical.CreateOperation { + configEntry.IAMServerIdHeaderValue = data.Get("iam_server_id_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. @@ -172,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 { @@ -187,9 +242,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"` + 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 new file mode 100644 index 000000000000..268571024041 --- /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 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["iam_server_id_header_value"] != nil { + t.Fatalf("returned endpoint or iam_server_id_header_value non-nil") + } + } + + data := map[string]interface{}{ + "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, + 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["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-ec2/path_config_sts.go b/builtin/credential/aws/path_config_sts.go similarity index 99% rename from builtin/credential/aws-ec2/path_config_sts.go rename to builtin/credential/aws/path_config_sts.go index aca4be80bfb2..4366feb16c99 100644 --- a/builtin/credential/aws-ec2/path_config_sts.go +++ b/builtin/credential/aws/path_config_sts.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" 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 99% rename from builtin/credential/aws-ec2/path_config_tidy_identity_whitelist.go rename to builtin/credential/aws/path_config_tidy_identity_whitelist.go index 8fac923dc3b2..43aafaacbeb7 100644 --- a/builtin/credential/aws-ec2/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-ec2/path_config_tidy_roletag_blacklist.go b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go similarity index 99% rename from builtin/credential/aws-ec2/path_config_tidy_roletag_blacklist.go rename to builtin/credential/aws/path_config_tidy_roletag_blacklist.go index 071ab9144687..c3059c68f1bb 100644 --- a/builtin/credential/aws-ec2/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-ec2/path_identity_whitelist.go b/builtin/credential/aws/path_identity_whitelist.go similarity index 99% rename from builtin/credential/aws-ec2/path_identity_whitelist.go rename to builtin/credential/aws/path_identity_whitelist.go index 84a649499b46..600fc7dd59d7 100644 --- a/builtin/credential/aws-ec2/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-ec2/path_login.go b/builtin/credential/aws/path_login.go similarity index 55% rename from builtin/credential/aws-ec2/path_login.go rename to builtin/credential/aws/path_login.go index 89b0b9848392..bf50898405c3 100644 --- a/builtin/credential/aws-ec2/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" @@ -22,6 +28,9 @@ import ( const ( reauthenticationDisabledNonce = "reauthentication-disabled-nonce" + iamAuthType = "iam" + ec2AuthType = "ec2" + ec2EntityType = "ec2_instance" ) func pathLogin(b *backend) *framework.Path { @@ -37,22 +46,49 @@ If a matching role is not found, login fails.`, }, "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 ec2.`, }, "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 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 +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.`, + }, + + "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`, + }, + + "iam_request_url": { + Type: framework.TypeString, + Description: `Base64-encoded full URL against which to make the AWS request +when using iam auth_type.`, + }, + + "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.`, + }, + "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 +which AWS has included a signature.`, }, "identity": { Type: framework.TypeString, @@ -79,27 +115,14 @@ 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, accountID string) (string, error) { +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") } - // Check if an STS configuration exists for the AWS account - sts, err := b.lockedAwsStsEntry(s, accountID) - if err != nil { - return "", fmt.Errorf("error fetching STS config for account ID %q: %q\n", accountID, err) - } - // An empty STS role signifies the master account - stsRole := "" - if sts != nil { - stsRole = sts.StsRole - } - - iamClient, err := b.clientIAM(s, region, stsRole) - if err != nil { - return "", err - } - profile, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{ InstanceProfileName: aws.String(instanceProfileName), }) @@ -127,7 +150,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, accountID string) (*ec2.DescribeInstancesOutput, error) { +func (b *backend) validateInstance(s logical.Storage, instanceID, region, accountID string) (*ec2.Instance, error) { // Check if an STS configuration exists for the AWS account sts, err := b.lockedAwsStsEntry(s, accountID) @@ -178,7 +201,7 @@ func (b *backend) validateInstance(s logical.Storage, instanceID, region, accoun 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 @@ -337,11 +360,169 @@ 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) { + 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) + 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) + default: + return logical.ErrorResponse("didn't supply required authentication values"), nil + } +} + +// 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) (error, error) { + + switch { + case instance == nil: + return nil, fmt.Errorf("nil instance") + case roleEntry == nil: + return nil, fmt.Errorf("nil roleEntry") + case identityDoc == nil: + 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 fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil + } + + // 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 ec2 or iam. + if roleEntry.BoundAmiID != "" { + if instance.ImageId == nil { + return nil, fmt.Errorf("AMI ID in the instance description is nil") + } + if roleEntry.BoundAmiID != *instance.ImageId { + 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, fmt.Errorf("subnet ID in the instance description is nil") + } + if roleEntry.BoundSubnetID != *instance.SubnetId { + 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, fmt.Errorf("VPC ID in the instance description is nil") + } + if roleEntry.BoundVpcID != *instance.VpcId { + return fmt.Errorf("VPC ID %q does not satisfy the constraint on role %q", *instance.VpcId, 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 fmt.Errorf("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") + } + + // 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 { + return nil, fmt.Errorf("could not fetch IAM client: %v", err) + } else if iamClient == nil { + return nil, fmt.Errorf("received a nil iamClient") + } + 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 fmt.Errorf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil + } + } + + return nil, nil +} + +// 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) pathLoginUpdate( +func (b *backend) pathLoginUpdateEc2( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { identityDocB64 := data.Get("identity").(string) var identityDocBytes []byte @@ -400,14 +581,6 @@ func (b *backend) pathLoginUpdate( 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. - instanceDesc, 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 { @@ -417,16 +590,16 @@ 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 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 + if roleEntry.AuthType != 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 @@ -435,82 +608,12 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse(fmt.Sprintf("Region %q does not satisfy the constraint on role %q", identityDocParsed.Region, roleName)), nil } - // Validate the SubnetID if corresponding bound was set on the role - if roleEntry.BoundSubnetID != "" { - subnetIDPtr := instanceDesc.Reservations[0].Instances[0].SubnetId - if subnetIDPtr == nil { - return nil, fmt.Errorf("Subnet ID in the instance description is nil") - } - if roleEntry.BoundSubnetID != *subnetIDPtr { - return logical.ErrorResponse(fmt.Sprintf("Subnet ID %q does not satisfy the constraint on role %q", *subnetIDPtr, roleName)), nil - } - } - - // Validate the VpcID if corresponding bound was set on the role - if roleEntry.BoundVpcID != "" { - vpcIDPtr := instanceDesc.Reservations[0].Instances[0].VpcId - if vpcIDPtr == nil { - return nil, fmt.Errorf("VPC ID in the instance description is nil") - } - if roleEntry.BoundVpcID != *vpcIDPtr { - return logical.ErrorResponse(fmt.Sprintf("VPC ID %q does not satisfy the constraint on role %q", *vpcIDPtr, 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 on role %q", iamInstanceProfileARN, roleName)), nil - } + validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDocParsed) + if err != nil { + return nil, err } - - // Check if the IAM role ARN of the instance trying to login, matches - // the IAM role ARN specified as a constraint on 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, identityDocParsed.AccountID) - 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 on role %q", iamRoleARN, roleName)), nil - } + if validationError != nil { + return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %v", validationError)), nil } // Get the entry from the identity whitelist, if there is one @@ -593,44 +696,45 @@ func (b *backend) pathLoginUpdate( policies := roleEntry.Policies rTagMaxTTL := time.Duration(0) - + var roleTagResp *roleTagLoginResponse if roleEntry.RoleTag != "" { - // - // 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) + roleTagResp, err := b.handleRoleTagLogin(req.Storage, roleName, roleEntry, instance) if err != nil { return nil, err } - if resp == nil { + 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. + // + // Overwrite the policies with the ones returned from processing the role tag // 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 } } @@ -717,20 +821,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) } @@ -764,7 +865,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.") } @@ -791,6 +892,69 @@ 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 = ec2AuthType + } + + if authType == ec2AuthType { + return b.pathLoginRenewEc2(req, data) + } else if authType == iamAuthType { + return b.pathLoginRenewIam(req, data) + } else { + return nil, fmt.Errorf("unrecognized auth_type: %q", authType) + } +} + +func (b *backend) pathLoginRenewIam( + 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["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") + } + 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) + } + } else { + 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 %q", canonicalArn) + } + + return framework.LeaseExtend(roleEntry.TTL, roleEntry.MaxTTL, b.System())(req, data) + +} + +func (b *backend) pathLoginRenewEc2( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { instanceID := req.Auth.Metadata["instance_id"] if instanceID == "" { @@ -888,6 +1052,381 @@ func (b *backend) pathLoginRenew( } } +func (b *backend) pathLoginUpdateIam( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + method := data.Get("iam_http_request_method").(string) + if method == "" { + return logical.ErrorResponse("missing iam_http_request_method"), nil + } + + // In the future, might consider supporting GET + if method != "POST" { + return logical.ErrorResponse("invalid iam_http_request_method; currently only 'POST' is supported"), nil + } + + rawUrlB64 := data.Get("iam_request_url").(string) + if rawUrlB64 == "" { + return logical.ErrorResponse("missing iam_request_url"), nil + } + 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 + } + + // TODO: There are two potentially valid cases we're not yet supporting that would + // necessitate this check being changed. First, if we support GET requests. + // Second if we support presigned POST requests + bodyB64 := data.Get("iam_request_body").(string) + if bodyB64 == "" { + return logical.ErrorResponse("missing iam_request_body"), nil + } + bodyRaw, err := base64.StdEncoding.DecodeString(bodyB64) + if err != nil { + return logical.ErrorResponse("failed to base64 decode iam_request_body"), nil + } + body := string(bodyRaw) + + headersB64 := data.Get("iam_request_headers").(string) + if headersB64 == "" { + return logical.ErrorResponse("missing iam_request_headers"), nil + } + headersJson, err := base64.StdEncoding.DecodeString(headersB64) + if err != 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("failed to JSON decode iam_request_headers %q: %v", headersJson, err)), nil + } + + 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.IAMServerIdHeaderValue != "" { + err = validateVaultHeaderValue(headers, parsedUrl, config.IAMServerIdHeaderValue) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %v", iamServerIdHeader, err)), nil + } + } + if config.STSEndpoint != "" { + endpoint = config.STSEndpoint + } + } + + clientArn, accountID, err := submitCallerIdentityRequest(method, endpoint, parsedUrl, body, headers) + if 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: %v", err)), 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 + } + + if roleEntry.AuthType != 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 %q does not belong to the role %q", clientArn, roleName)), nil + } + + policies := roleEntry.Policies + + inferredEntityType := "" + inferredEntityId := "" + 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 + } + + // 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), + } + + validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDoc) + if err != nil { + return nil, err + } + if validationError != nil { + return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %s", validationError)), nil + } + + inferredEntityType = ec2EntityType + inferredEntityId = sessionName + } + + resp := &logical.Response{ + Auth: &logical.Auth{ + Policies: policies, + Metadata: map[string]string{ + "client_arn": clientArn, + "canonical_arn": canonicalArn, + "auth_type": iamAuthType, + "inferred_entity_type": inferredEntityType, + "inferred_entity_id": inferredEntityId, + "inferred_aws_region": roleEntry.InferredAWSRegion, + "account_id": accountID, + }, + InternalData: map[string]interface{}{ + "role_name": roleName, + }, + DisplayName: principalName, + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + TTL: roleEntry.TTL, + }, + }, + } + + if roleEntry.Period > time.Duration(0) { + resp.Auth.TTL = roleEntry.Period + } else { + 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 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 +} + +// 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("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: %q", parts[0]) + } + return transformedArn, principalName, sessionName, nil +} + +func validateVaultHeaderValue(headers http.Header, requestUrl *url.URL, requiredHeaderValue string) error { + providedValue := "" + for k, v := range headers { + if strings.ToLower(iamServerIdHeader) == strings.ToLower(k) { + providedValue = strings.Join(v, ",") + break + } + } + if providedValue == "" { + 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 fmt.Errorf("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 fmt.Errorf("vault header wasn't signed") + } + if len(matches) > 2 { + return fmt.Errorf("found multiple SignedHeaders components") + } + signedHeaders := string(matches[1]) + return ensureHeaderIsSigned(signedHeaders, iamServerIdHeader) + } + // TODO: If we support GET requests, then we need to parse the X-Amz-SignedHeaders + // argument out of the query string and search in there for the header value + return fmt.Errorf("missing Authorization header") +} + +func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) *http.Request { + // 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) 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 nil + } + } + return fmt.Errorf("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, 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: %v", 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, callerIdentityResponse.GetCallerIdentityResult[0].Account, 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"` +} + // identityDocument represents the items of interest from the EC2 instance // identity document type identityDocument struct { @@ -907,11 +1446,21 @@ type roleTagLoginResponse struct { DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } +const iamServerIdHeader = "X-Vault-AWS-IAM-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..e96bed835034 --- /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_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: %v", 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-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-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + postHeadersUnsigned := 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, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + 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-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + + postHeadersSplit := http.Header{ + "Host": []string{"Foo"}, + iamServerIdHeader: []string{canaryHeaderValue}, + "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request", "SignedHeaders=content-type;host;x-amz-date;x-vault-aws-iam-server-id, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}, + } + + err = validateVaultHeaderValue(postHeadersMissing, requestUrl, canaryHeaderValue) + if err == nil { + t.Error("validated POST request with missing Vault header") + } + + err = validateVaultHeaderValue(postHeadersInvalid, requestUrl, canaryHeaderValue) + if err == nil { + t.Error("validated POST request with invalid Vault header value") + } + + err = validateVaultHeaderValue(postHeadersUnsigned, requestUrl, canaryHeaderValue) + if err == nil { + t.Error("validated POST request with unsigned Vault header") + } + + err = validateVaultHeaderValue(postHeadersValid, requestUrl, canaryHeaderValue) + if err != nil { + 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: %v", err) + } +} diff --git a/builtin/credential/aws-ec2/path_role.go b/builtin/credential/aws/path_role.go similarity index 62% rename from builtin/credential/aws-ec2/path_role.go rename to builtin/credential/aws/path_role.go index fc70d2bf7dfb..f6c19f23790f 100644 --- a/builtin/credential/aws-ec2/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -1,4 +1,4 @@ -package awsec2 +package awsauth import ( "fmt" @@ -20,6 +20,11 @@ func pathRole(b *backend) *framework.Path { Type: framework.TypeString, Description: "Name of the role.", }, + "auth_type": { + 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.`, + }, "bound_ami_id": { Type: framework.TypeString, Description: `If set, defines a constraint on the EC2 instances that they should be @@ -29,10 +34,17 @@ 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 iam.`, }, "bound_region": { - Type: framework.TypeString, - Description: `If set, defines a constraint on the EC2 instances that the region in its identity document to match the one specified by this parameter.`, + Type: framework.TypeString, + Description: `If set, defines a constraint on the EC2 instances that the region in +its identity document to match the one specified by this parameter. Only applicable when +auth_type is ec2.`, }, "bound_iam_role_arn": { Type: framework.TypeString, @@ -41,14 +53,34 @@ 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 +ec2.`, }, "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 ec2.`, + }, + "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 +value is ec2_instance, which will extract the EC2 instance ID from the +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, + Description: `When auth_type is iam and +inferred_entity_type is set, the region to assume the inferred entity exists in.`, }, "bound_vpc_id": { Type: framework.TypeString, @@ -63,9 +95,13 @@ If set, defines a constraint on the EC2 instance to be associated with the subnet ID that matches the value specified by this parameter.`, }, "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 allowed if auth_type is ec2.`, }, "period": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -90,9 +126,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 ec2.`, }, "disallow_reauthentication": { Type: framework.TypeBool, @@ -159,9 +200,44 @@ 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) + // 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 + } + if roleEntry == nil { + return nil, nil + } + needUpgrade, err := upgradeRoleEntry(roleEntry) + if err != nil { + return nil, fmt.Errorf("error upgrading roleEntry: %v", err) + } + if needUpgrade { + b.roleMutex.Lock() + 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 @@ -206,9 +282,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") @@ -227,20 +335,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 - 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") - } - } - return &result, nil } @@ -316,6 +410,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 @@ -348,16 +453,120 @@ func (b *backend) pathRoleCreateUpdate( roleEntry.BoundIamInstanceProfileARN = boundIamInstanceProfileARNRaw.(string) } - // Ensure that at least one bound is set on the role - switch { - case roleEntry.BoundAmiID != "": - case roleEntry.BoundAccountID != "": - case roleEntry.BoundRegion != "": - case roleEntry.BoundVpcID != "": - case roleEntry.BoundSubnetID != "": - case roleEntry.BoundIamRoleARN != "": - case roleEntry.BoundIamInstanceProfileARN != "": - default: + if boundIamPrincipalARNRaw, ok := data.GetOk("bound_iam_principal_arn"); ok { + roleEntry.BoundIamPrincipalARN = boundIamPrincipalARNRaw.(string) + } + + if inferRoleTypeRaw, ok := data.GetOk("inferred_entity_type"); ok { + roleEntry.InferredEntityType = inferRoleTypeRaw.(string) + } + + if inferredAWSRegionRaw, ok := data.GetOk("inferred_aws_region"); ok { + roleEntry.InferredAWSRegion = inferredAWSRegionRaw.(string) + } + + // 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: + 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("changing auth_type on a role is not allowed"), nil + } + } else if req.Operation == logical.CreateOperation { + 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 + + if roleEntry.InferredEntityType != "" { + switch { + 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 + case roleEntry.InferredAWSRegion == "": + 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 inferred_entity_type"), nil + } + + numBinds := 0 + + if roleEntry.BoundAccountID != "" { + 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 roleEntry.AuthType != ec2AuthType { + return logical.ErrorResponse("specified bound_region but not allowing ec2 auth_type"), nil + } + numBinds++ + } + + if roleEntry.BoundAmiID != "" { + if !allowEc2Binds { + 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(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(fmt.Sprintf("specified bound_iam_role_arn but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil + } + numBinds++ + } + + if roleEntry.BoundIamPrincipalARN != "" { + if roleEntry.AuthType != iamAuthType { + return logical.ErrorResponse("specified bound_iam_principal_arn but not allowing iam auth_type"), nil + } + numBinds++ + } + + if roleEntry.BoundVpcID != "" { + if !allowEc2Binds { + return logical.ErrorResponse(fmt.Sprintf("specified bound_vpc_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil + } + numBinds++ + } + + if roleEntry.BoundSubnetID != "" { + if !allowEc2Binds { + return logical.ErrorResponse(fmt.Sprintf("specified bound_subnet_id but not allowing ec2 auth_type or inferring %s", ec2EntityType)), nil + } + numBinds++ + } + + if numBinds == 0 { return logical.ErrorResponse("at least be one bound parameter should be specified on the role"), nil } @@ -370,15 +579,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) } @@ -430,13 +645,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) } @@ -460,13 +678,17 @@ func (b *backend) pathRoleCreateUpdate( // Struct to hold the information associated with an AMI ID in Vault. type awsRoleEntry struct { + 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"` - BoundRegion string `json:"bound_region" structs:"bound_region" mapstructure:"bound_region"` + 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"` + 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"` + 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"` TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` @@ -494,6 +716,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-ec2/path_role_tag.go b/builtin/credential/aws/path_role_tag.go similarity index 99% rename from builtin/credential/aws-ec2/path_role_tag.go rename to builtin/credential/aws/path_role_tag.go index 5544fd30177f..5c8a119b14b3 100644 --- a/builtin/credential/aws-ec2/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..a52d4fa7b5d6 --- /dev/null +++ b/builtin/credential/aws/path_role_test.go @@ -0,0 +1,555 @@ +package awsauth + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/logical" +) + +func TestBackend_pathRoleEc2(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{}{ + "auth_type": "ec2", + "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: %s", resp.Data["error"]) + } + 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 + data["bound_ami_id"] = "ami-abcd456" + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/ami-abcd456", + Data: data, + Storage: storage, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role: %s", resp.Data["error"]) + } + 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_pathIam(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{}{ + "auth_type": iamAuthType, + "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.Fatalf("failed to create the role entry; resp: %#v", resp) + } + + 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["inferred_entity_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 inferred_entity_type") + } + if err != nil { + t.Fatal(err) + } + + data["inferred_entity_type"] = ec2EntityType + 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", + "auth_type": "ec2,invalid", + } + + submitRequest := func(roleName string, op logical.Operation) (*logical.Response, error) { + return b.HandleRequest(&logical.Request{ + Operation: op, + Path: "role/" + roleName, + Data: data, + Storage: storage, + }) + } + + 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["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["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["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) + } + + data["inferred_entity_type"] = ec2EntityType + data["inferred_aws_region"] = "us-east-1" + resp, err = submitRequest("multipleTypesInferred", logical.CreateOperation) + if err != nil { + t.Fatal(err) + } + if resp.IsError() { + t.Fatalf("didn't allow creation of roles with only inferred bindings") + } +} + +func TestAwsEc2_RoleCrud(t *testing.T) { + var err error + var resp *logical.Response + 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) + } + + role1Data := map[string]interface{}{ + "auth_type": "ec2", + "bound_vpc_id": "testvpcid", + "allow_instance_migration": true, + "policies": "testpolicy1,testpolicy2", + } + roleReq := &logical.Request{ + Operation: logical.UpdateOperation, + Storage: storage, + Path: "role/role1", + Data: role1Data, + } + + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + roleData := map[string]interface{}{ + "auth_type": "ec2", + "bound_ami_id": "testamiid", + "bound_account_id": "testaccountid", + "bound_region": "testregion", + "bound_iam_role_arn": "testiamrolearn", + "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", + "bound_subnet_id": "testsubnetid", + "bound_vpc_id": "testvpcid", + "role_tag": "testtag", + "allow_instance_migration": true, + "ttl": "10m", + "max_ttl": "20m", + "policies": "testpolicy1,testpolicy2", + "disallow_reauthentication": true, + "hmac_key": "testhmackey", + "period": "1m", + } + + roleReq.Path = "role/testrole" + roleReq.Data = roleData + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + roleReq.Operation = logical.ReadOperation + + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + expected := map[string]interface{}{ + "auth_type": ec2AuthType, + "bound_ami_id": "testamiid", + "bound_account_id": "testaccountid", + "bound_region": "testregion", + "bound_iam_principal_arn": "", + "bound_iam_role_arn": "testiamrolearn", + "bound_iam_instance_profile_arn": "testiaminstanceprofilearn", + "bound_subnet_id": "testsubnetid", + "bound_vpc_id": "testvpcid", + "inferred_entity_type": "", + "inferred_aws_region": "", + "role_tag": "testtag", + "allow_instance_migration": true, + "ttl": time.Duration(600), + "max_ttl": time.Duration(1200), + "policies": []string{"default", "testpolicy1", "testpolicy2"}, + "disallow_reauthentication": true, + "period": time.Duration(60), + } + + if !reflect.DeepEqual(expected, resp.Data) { + t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data) + } + + roleData["bound_vpc_id"] = "newvpcid" + roleReq.Operation = logical.UpdateOperation + + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + roleReq.Operation = logical.ReadOperation + + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + expected["bound_vpc_id"] = "newvpcid" + + if !reflect.DeepEqual(expected, resp.Data) { + t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data) + } + + roleReq.Operation = logical.DeleteOperation + + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + if resp != nil { + t.Fatalf("failed to delete role entry") + } +} + +func TestAwsEc2_RoleDurationSeconds(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) + } + + roleData := map[string]interface{}{ + "auth_type": "ec2", + "bound_iam_instance_profile_arn": "testarn", + "ttl": "10s", + "max_ttl": "20s", + "period": "30s", + } + + roleReq := &logical.Request{ + Operation: logical.CreateOperation, + Storage: storage, + Path: "role/testrole", + Data: roleData, + } + + resp, err := b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + roleReq.Operation = logical.ReadOperation + + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("resp: %#v, err: %v", resp, err) + } + + if int64(resp.Data["ttl"].(time.Duration)) != 10 { + t.Fatalf("bad: period; expected: 10, actual: %d", resp.Data["ttl"]) + } + if int64(resp.Data["max_ttl"].(time.Duration)) != 20 { + t.Fatalf("bad: period; expected: 20, actual: %d", resp.Data["max_ttl"]) + } + if int64(resp.Data["period"].(time.Duration)) != 30 { + t.Fatalf("bad: period; expected: 30, actual: %d", resp.Data["period"]) + } +} diff --git a/builtin/credential/aws-ec2/path_roletag_blacklist.go b/builtin/credential/aws/path_roletag_blacklist.go similarity index 99% rename from builtin/credential/aws-ec2/path_roletag_blacklist.go rename to builtin/credential/aws/path_roletag_blacklist.go index c3faa4cba946..32fded87852e 100644 --- a/builtin/credential/aws-ec2/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-ec2/path_tidy_identity_whitelist.go b/builtin/credential/aws/path_tidy_identity_whitelist.go similarity index 99% rename from builtin/credential/aws-ec2/path_tidy_identity_whitelist.go rename to builtin/credential/aws/path_tidy_identity_whitelist.go index 266d4596f28f..c77687f6322d 100644 --- a/builtin/credential/aws-ec2/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-ec2/path_tidy_roletag_blacklist.go b/builtin/credential/aws/path_tidy_roletag_blacklist.go similarity index 99% rename from builtin/credential/aws-ec2/path_tidy_roletag_blacklist.go rename to builtin/credential/aws/path_tidy_roletag_blacklist.go index d163968ddb69..3970a1815d9a 100644 --- a/builtin/credential/aws-ec2/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 190111177953..7494c0676ee9 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -10,7 +10,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" + 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" @@ -71,7 +71,7 @@ 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, "app-id": credAppId.Factory, "github": credGitHub.Factory, "userpass": credUserpass.Factory, @@ -118,6 +118,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { "ldap": &credLdap.CLIHandler{}, "okta": &credOkta.CLIHandler{}, "cert": &credCert.CLIHandler{}, + "aws": &credAws.CLIHandler{}, "radius": &credUserpass.CLIHandler{DefaultMount: "radius"}, }, }, nil 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/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/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..5a90dfa0e2db 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 = "" } @@ -303,7 +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.MountPoint = mount + req.MountType = re.mountEntry.Type req.Connection = originalConn req.ID = originalReqID req.Storage = nil diff --git a/website/source/docs/auth/aws-ec2.html.md b/website/source/docs/auth/aws.html.md similarity index 60% rename from website/source/docs/auth/aws-ec2.html.md rename to website/source/docs/auth/aws.html.md index daf879b525a0..107ad2e89311 100644 --- a/website/source/docs/auth/aws-ec2.html.md +++ b/website/source/docs/auth/aws.html.md @@ -1,23 +1,38 @@ --- 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` 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 + 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,19 +53,73 @@ 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 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-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 +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 parameters 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 -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 -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 +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 +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. @@ -64,8 +133,130 @@ 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 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 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 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 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. + +## 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 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, 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. If you need to make use of role tags, then + you will need to use the ec2 auth method. + ## 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 +270,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 +313,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 +325,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 +346,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 +385,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 +408,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 +446,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 +455,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 @@ -284,7 +487,7 @@ for the IAM Role to be assumed. #### Enable AWS EC2 authentication in Vault. ``` -$ vault auth-enable aws-ec2 +$ vault auth-enable aws ``` #### Configure the credentials required to make AWS API calls @@ -298,50 +501,96 @@ 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 auth_type=iam \ + bound_iam_principal_arn=arn:aws:iam::123456789012:role/MyRole policies=prod,dev max_ttl=500h ``` +#### Configure a required X-Vault-AWS-IAM-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= +``` + +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 \ + iam_http_request_method=POST \ + iam_request_url=aHR0cHM6Ly9zdHMuYW1hem9uYXdzLmNvbS8= \ + iam_request_body=QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== \ + iam_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-ec2" -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 '{"auth_type":"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", "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: @@ -377,43 +626,52 @@ 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 backend is configured on an EC2 - instance with metadata querying capabilities, the credentials are fetched - automatically. + 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.
Method
POST
URL
-
`/auth/aws-ec2/config/client`
+
`/auth/aws/config/client`
Parameters
  • 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.
    @@ -423,6 +681,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_server_id_header_value + optional + 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-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 + to a production server. Consider setting this to the Vault server's DNS + name. +
  • +
Returns
@@ -442,7 +729,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/client`
+
`/auth/aws/config/client`
Parameters
@@ -460,6 +747,9 @@ The response will be in JSON. For example: "secret_key": "vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj", "access_key": "VKIAJBRHKH6EVTTNXDHA" "endpoint" "", + "iam_endpoint" "", + "sts_endpoint" "", + "iam_server_id_header_value" "", }, "lease_duration": 0, "renewable": false, @@ -482,7 +772,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws-ec2/config/client`
+
`/auth/aws/config/client`
Parameters
@@ -495,7 +785,7 @@ The response will be in JSON. For example:
-### /auth/aws-ec2/config/certificate/ +### /auth/aws/config/certificate/ #### POST
Description
@@ -511,7 +801,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/config/certificate/`
+
`/auth/aws/config/certificate/`
Parameters
@@ -559,7 +849,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/certificate/`
+
`/auth/aws/config/certificate/`
Parameters
@@ -596,7 +886,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
@@ -625,7 +915,7 @@ The response will be in JSON. For example:
-### /auth/aws-ec2/config/sts/ +### /auth/aws/config/sts/ #### POST
Description
@@ -640,7 +930,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/config/sts/`
+
`/auth/aws/config/sts/`
Parameters
@@ -679,7 +969,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/config/sts/`
+
`/auth/aws/config/sts/`
Parameters
@@ -716,7 +1006,7 @@ The response will be in JSON. For example:
LIST/GET
URL
-
`/auth/aws-ec2/config/sts` (LIST) or `/auth/aws-ec2/config/sts?list=true` (GET)
+
`/auth/aws/config/sts` (LIST) or `/auth/aws/config/sts?list=true` (GET)
Parameters
@@ -756,7 +1046,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws-ec2/config/sts/`
+
`/auth/aws/config/sts/`
Parameters
@@ -768,7 +1058,7 @@ The response will be in JSON. For example:
-### /auth/aws-ec2/config/tidy/identity-whitelist +### /auth/aws/config/tidy/identity-whitelist ##### POST
Description
@@ -780,7 +1070,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
@@ -820,7 +1110,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
@@ -858,7 +1148,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
@@ -872,7 +1162,7 @@ The response will be in JSON. For example: -### /auth/aws-ec2/config/tidy/roletag-blacklist +### /auth/aws/config/tidy/roletag-blacklist ##### POST
Description
@@ -884,7 +1174,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
@@ -924,7 +1214,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
@@ -962,7 +1252,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
@@ -976,23 +1266,27 @@ 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. 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
POST
URL
-
`/auth/aws-ec2/role/`
+
`/auth/aws/role/`
Parameters
@@ -1003,20 +1297,35 @@ The response will be in JSON. For example: Name of the role. +
    +
  • + 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" (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. +
  • +
  • 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. + 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. + 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.
    @@ -1024,7 +1333,9 @@ The response will be in JSON. For example: 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 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..
    @@ -1032,7 +1343,9 @@ The response will be in JSON. For example: 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.
    @@ -1040,18 +1353,22 @@ The response will be in JSON. For example: 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. + 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.
    @@ -1061,7 +1378,9 @@ The response will be in JSON. For example: 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 `*`). + 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.
    @@ -1070,8 +1389,40 @@ The response will be in JSON. For example: 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. - Defaults to an empty string, meaning that role tags are disabled. + 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 allowed + when an auth_type is iam. + +
+
    +
  • + 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. +
  • +
+
    +
  • + 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 + 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.
    @@ -1115,7 +1466,8 @@ The response will be in JSON. For example: 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. + previously-remembered time. Use with caution. This only applies to + authentications via the ec2 auth method.
    @@ -1125,8 +1477,8 @@ The response will be in JSON. For example: 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'. + 'auth/aws/identity-whitelist/' endpoint. Defaults to + 'false'. This only applies to authentications via the ec2 auth method.
@@ -1148,7 +1500,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws-ec2/role/`
+
`/auth/aws/role/`
Parameters
@@ -1195,7 +1547,7 @@ The response will be in JSON. For example:
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
@@ -1236,7 +1588,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws-ec2/role/`
+
`/auth/aws/role/`
Parameters
@@ -1249,7 +1601,7 @@ The response will be in JSON. For example:
-### /auth/aws-ec2/role/[role]/tag +### /auth/aws/role/[role]/tag #### POST
Description
@@ -1274,7 +1626,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws-ec2/role//tag`
+
`/auth/aws/role//tag`
Parameters
@@ -1314,7 +1666,7 @@ The response will be in JSON. For example: 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. + can be cleared with the auth/aws/identity-whitelist endpoint. Defaults to 'false'. @@ -1352,24 +1704,25 @@ The response will be in JSON. For example:
-### /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. 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 with its RSA digest can be supplied to this - endpoint. + 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.
Method
POST
URL
-
`/auth/aws-ec2/login`
+
`/auth/aws/login`
Parameters
@@ -1377,10 +1730,12 @@ The response will be in JSON. For example:
  • role 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. If a - matching role is not found, login fails. + 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 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.
    • @@ -1398,7 +1753,8 @@ The response will be in JSON. For example: signature required Base64 encoded SHA256 RSA signature of the instance identity document. - This needs to be supplied along with `identity` parameter. + This needs to be supplied along with `identity` parameter when using the + ec2 auth method.
      @@ -1407,7 +1763,7 @@ The response will be in JSON. For example: 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. + `signature` need to be set when using the ec2 auth method.
      @@ -1424,7 +1780,52 @@ The response will be in JSON. For example: 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. + role tag, the `nonce` holds no significance. This is ignored unless + using the ec2 auth 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 + required when using the iam auth method. +
    • +
    +
      +
    • + 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. +
    • +
    +
      +
    • + iam_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. +
    • +
    +
      +
    • + 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 + 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-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.
    @@ -1441,7 +1842,8 @@ The response will be in JSON. For example: "role_tag_max_ttl": "0", "instance_id": "i-de0f1344" "ami_id": "ami-fce36983" - "role": "dev-role" + "role": "dev-role", + "auth_type": "ec2" }, "policies": [ "default", @@ -1462,7 +1864,7 @@ The response will be in JSON. For example:
    -### /auth/aws-ec2/roletag-blacklist/ +### /auth/aws/roletag-blacklist/ #### POST
    Description
    @@ -1478,7 +1880,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws-ec2/roletag-blacklist/`
    +
    `/auth/aws/roletag-blacklist/`
    Parameters
    @@ -1509,7 +1911,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws-ec2/broletag-blacklist/`
    +
    `/auth/aws/broletag-blacklist/`
    Parameters
    @@ -1548,7 +1950,7 @@ The response will be in JSON. For example:
    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
    @@ -1588,7 +1990,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws-ec2/roletag-blacklist/`
    +
    `/auth/aws/roletag-blacklist/`
    Parameters
    @@ -1601,7 +2003,7 @@ The response will be in JSON. For example:
    -### /auth/aws-ec2/tidy/roletag-blacklist +### /auth/aws/tidy/roletag-blacklist #### POST
    Description
    @@ -1614,7 +2016,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws-ec2/tidy/roletag-blacklist`
    +
    `/auth/aws/tidy/roletag-blacklist`
    Parameters
    @@ -1635,7 +2037,7 @@ The response will be in JSON. For example:
    -### /auth/aws-ec2/identity-whitelist/ +### /auth/aws/identity-whitelist/ #### GET
    Description
    @@ -1648,7 +2050,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws-ec2/identity-whitelist/`
    +
    `/auth/aws/identity-whitelist/`
    Parameters
    @@ -1697,7 +2099,7 @@ The response will be in JSON. For example:
    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. @@ -1736,7 +2138,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws-ec2/identity-whitelist/`
    +
    `/auth/aws/identity-whitelist/`
    Parameters
    @@ -1749,7 +2151,7 @@ The response will be in JSON. For example:
    -### /auth/aws-ec2/tidy/identity-whitelist +### /auth/aws/tidy/identity-whitelist #### POST
    Description
    @@ -1761,7 +2163,7 @@ The response will be in JSON. For example:
    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 8f2686e64439..f6f07b86a511 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -268,8 +268,8 @@ AppRole - > - AWS EC2 + > + AWS > From 1f8f1465087e8db6650ddb6613369f457fcc33e6 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 24 Apr 2017 15:35:00 -0400 Subject: [PATCH 07/53] changelog++ --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc09767ec378..a071028214c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ DEPRECATIONS/CHANGES: FEATURES: + * **AWS IAM Authentication**: IAM principals can get Vault tokens + automatically using this new backend. Signed client identity information + retrieved using the AWS API `sts:GetCallerIdentity` is validated against AWS + STS service before issuing a Vault token. This backend is unified with the + `aws-ec2` authentication backend, which allows additional EC2 related + restrictions to be applied during the IAM authentication. [GH-2441] * **MSSQL Physical Backend**: You can now use Microsoft SQL Server as your Vault physical data store [GH-2546] From 9b8b78cf3d247b2f27374c6f56897a1f5e157881 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 24 Apr 2017 15:37:54 -0400 Subject: [PATCH 08/53] changelog++ --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a071028214c3..16b0fe02f2dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ DEPRECATIONS/CHANGES: FEATURES: * **AWS IAM Authentication**: IAM principals can get Vault tokens - automatically using this new backend. Signed client identity information - retrieved using the AWS API `sts:GetCallerIdentity` is validated against AWS - STS service before issuing a Vault token. This backend is unified with the - `aws-ec2` authentication backend, which allows additional EC2 related - restrictions to be applied during the IAM authentication. [GH-2441] + automatically, opening AWS-based authentication to users, ECS containers, + Lambda instances, and more. Signed client identity information retrieved + using the AWS API `sts:GetCallerIdentity` is validated against the AWS STS + service before issuing a Vault token. This backend is unified with the + `aws-ec2` authentication backend, and allows additional EC2-related + restrictions to be applied during the IAM authentication; the previous EC2 + behavior is also still available. [GH-2441] * **MSSQL Physical Backend**: You can now use Microsoft SQL Server as your Vault physical data store [GH-2546] From 97db6573971f1239c3a3eddefc896138f4eac5f4 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 24 Apr 2017 15:39:58 -0400 Subject: [PATCH 09/53] Make sidebar a bit wider on smaller screens (#2638) --- website/source/assets/stylesheets/_docs.scss | 16 ++++++++++++++++ website/source/layouts/inner.erb | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index ccbbf21351b6..475aa46d8904 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -1,6 +1,16 @@ #docs-sidebar { margin-bottom: 30px; margin-top: 50px; + overflow: hidden; + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 30px; + } ul.nav.docs-sidenav { display: block; @@ -34,6 +44,12 @@ opacity: 1; } } + + &.back { + &:before { + content: '\2039'; + } + } } // For forcing sub-navs to appear - in the long term, this should not diff --git a/website/source/layouts/inner.erb b/website/source/layouts/inner.erb index b1406f15faa0..5cb8b75d0d60 100644 --- a/website/source/layouts/inner.erb +++ b/website/source/layouts/inner.erb @@ -1,11 +1,11 @@ <% wrap_layout :layout do %>
    -