diff --git a/CHANGELOG.md b/CHANGELOG.md index fc09767ec378..4546a9e9ef2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,21 +8,43 @@ DEPRECATIONS/CHANGES: FEATURES: + * **AWS IAM Authentication**: IAM principals can get Vault tokens + 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] + * **Lease Listing and Lookup**: You can now introspect a lease to get its + creation and expiration properties via `sys/leases/lookup`; with `sudo` + capability you can also list leases for lookup, renewal, or revocation via + that endpoint. Various lease functions (renew, revoke, revoke-prefix, + revoke-force) have also been relocated to `sys/leases/`, but they also work + at the old paths for compatibility. Reading (but not listing) leases via + `sys/leases/lookup` is now a part of the current `default` policy. [GH-2650] IMPROVEMENTS: + * auth/cert: Support for constraints on subject Common Name and DNS/email + Subject Alternate Names in certificates [GH-2595] * auth/ldap: Use the binding credentials to search group membership rather than the user credentials [GH-2534] * cli/revoke: Add `-self` option to allow revoking the currently active token [GH-2596] + * core: Randomizing x coordinate in Shamir shares [GH-2621] * secret/pki: Add `no_store` option that allows certificates to be issued without being stored. This removes the ability to look up and/or add to a CRL but helps with scaling to very large numbers of certificates. [GH-2565] * secret/pki: If used with a role parameter, the `sign-verbatim/` endpoint honors the values of `generate_lease`, `no_store`, `ttl` and `max_ttl` from the given role [GH-2593] + * secret/pki: Add role parameter `allow_glob_domains` that enables defining + names in `allowed_domains` containing `*` glob patterns [GH-2517] + * secret/pki: Update certificate storage to not use characters that are not + supported on some filesystems [GH-2575] * storage/etcd3: Add `discovery_srv` option to query for SRV records to find servers [GH-2521] * storage/s3: Support `max_parallel` option to limit concurrent outstanding @@ -35,6 +57,8 @@ BUG FIXES: * api: Respect a configured path in Vault's address [GH-2588] * auth/aws-ec2: New bounds added as criteria to allow role creation [GH-2600] * auth/ldap: Don't lowercase groups attached to users [GH-2613] + * cli: Don't panic if `vault write` is used with the `force` flag but no path + [GH-2674] * secret/mssql: Update mssql driver to support queries with colons [GH-2610] * secret/pki: Don't lowercase O/OU values in certs [GH-2555] * secret/pki: Don't attempt to validate IP SANs if none are provided [GH-2574] diff --git a/builtin/credential/approle/path_role_test.go b/builtin/credential/approle/path_role_test.go index c1933854991f..a40cbe10d698 100644 --- a/builtin/credential/approle/path_role_test.go +++ b/builtin/credential/approle/path_role_test.go @@ -104,7 +104,7 @@ func TestAppRole_RoleConstraints(t *testing.T) { roleData["bind_secret_id"] = false resp, err = b.HandleRequest(roleReq) if resp != nil && resp.IsError() { - t.Fatalf("resp:%#v", err, resp) + t.Fatalf("err:%v, resp:%#v", err, resp) } if err == nil { t.Fatalf("expected an error") @@ -433,7 +433,7 @@ func TestAppRole_RoleSecretIDAccessorReadDelete(t *testing.T) { hmacReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(hmacReq) if resp != nil && resp.IsError() { - t.Fatalf("error response:%#v", err, resp) + t.Fatalf("err:%v resp:%#v", err, resp) } if err == nil { t.Fatalf("expected an error") diff --git a/builtin/credential/approle/validation.go b/builtin/credential/approle/validation.go index 15b50bbaf7b8..db668a8257b9 100644 --- a/builtin/credential/approle/validation.go +++ b/builtin/credential/approle/validation.go @@ -352,7 +352,7 @@ func (b *backend) nonLockedSecretIDStorageEntry(s logical.Storage, roleNameHMAC, if persistNeeded { if err := b.nonLockedSetSecretIDStorageEntry(s, roleNameHMAC, secretIDHMAC, &result); err != nil { - return nil, fmt.Errorf("failed to upgrade role storage entry", err) + return nil, fmt.Errorf("failed to upgrade role storage entry %s", err) } } 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..52ff435744af --- /dev/null +++ b/builtin/credential/aws/path_role_test.go @@ -0,0 +1,556 @@ +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" + secondRole := &logical.Request{ + Operation: logical.CreateOperation, + Path: "role/MyOtherRoleName", + Data: data, + Storage: storage, + } + resp, err = b.HandleRequest(secondRole) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf("failed to create additional role: %v", *secondRole) + } + + 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/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index 766c095f8d79..f96c9cb868c8 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -348,9 +348,9 @@ func TestBackend_CertWrites(t *testing.T) { tc := logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "aaa", ca1, "foo", false), - testAccStepCert(t, "bbb", ca2, "foo", false), - testAccStepCert(t, "ccc", ca3, "foo", true), + testAccStepCert(t, "aaa", ca1, "foo", "", false), + testAccStepCert(t, "bbb", ca2, "foo", "", false), + testAccStepCert(t, "ccc", ca3, "foo", "", true), }, } tc.Steps = append(tc.Steps, testAccStepListCerts(t, []string{"aaa", "bbb"})...) @@ -368,13 +368,17 @@ func TestBackend_basic_CA(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", false), + testAccStepCert(t, "web", ca, "foo", "", false), testAccStepLogin(t, connState), testAccStepCertLease(t, "web", ca, "foo"), testAccStepCertTTL(t, "web", ca, "foo"), testAccStepLogin(t, connState), testAccStepCertNoLease(t, "web", ca, "foo"), testAccStepLoginDefaultLease(t, connState), + testAccStepCert(t, "web", ca, "foo", "*.example.com", false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", "*.invalid.com", false), + testAccStepLoginInvalid(t, connState), }, }) } @@ -405,8 +409,29 @@ func TestBackend_Basic_CRLs(t *testing.T) { }) } -// Test a self-signed client that is trusted +// Test a self-signed client (root CA) that is trusted func TestBackend_basic_singleCert(t *testing.T) { + connState := testConnState(t, "test-fixtures/root/rootcacert.pem", + "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") + ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("err: %v", err) + } + logicaltest.Test(t, logicaltest.TestCase{ + Backend: testFactory(t), + Steps: []logicaltest.TestStep{ + testAccStepCert(t, "web", ca, "foo", "", false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", "example.com", false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", "invalid", false), + testAccStepLoginInvalid(t, connState), + }, + }) +} + +// Test against a collection of matching and non-matching rules +func TestBackend_mixed_constraints(t *testing.T) { connState := testConnState(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") @@ -416,13 +441,18 @@ func TestBackend_basic_singleCert(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", false), + testAccStepCert(t, "1unconstrained", ca, "foo", "", false), + testAccStepCert(t, "2matching", ca, "foo", "*.example.com,whatever", false), + testAccStepCert(t, "3invalid", ca, "foo", "invalid", false), testAccStepLogin(t, connState), + // Assumes CertEntries are processed in alphabetical order (due to store.List), so we only match 2matching if 1unconstrained doesn't match + testAccStepLoginWithName(t, connState, "2matching"), + testAccStepLoginWithNameInvalid(t, connState, "3invalid"), }, }) } -// Test an untrusted self-signed client +// Test an untrusted client func TestBackend_untrusted(t *testing.T) { connState := testConnState(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") @@ -476,6 +506,10 @@ func testAccStepDeleteCRL(t *testing.T, connState tls.ConnectionState) logicalte } func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { + return testAccStepLoginWithName(t, connState, "") +} + +func testAccStepLoginWithName(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "login", @@ -486,9 +520,16 @@ func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.T t.Fatalf("bad lease length: %#v", resp.Auth) } + if certName != "" && resp.Auth.DisplayName != ("mnt-"+certName) { + t.Fatalf("matched the wrong cert: %#v", resp.Auth.DisplayName) + } + fn := logicaltest.TestCheckAuth([]string{"default", "foo"}) return fn(resp) }, + Data: map[string]interface{}{ + "name": certName, + }, } } @@ -510,6 +551,10 @@ func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) l } func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { + return testAccStepLoginWithNameInvalid(t, connState, "") +} + +func testAccStepLoginWithNameInvalid(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "login", @@ -521,6 +566,9 @@ func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logica } return nil }, + Data: map[string]interface{}{ + "name": certName, + }, ErrorOk: true, } } @@ -572,16 +620,17 @@ func testAccStepListCerts( } func testAccStepCert( - t *testing.T, name string, cert []byte, policies string, expectError bool) logicaltest.TestStep { + t *testing.T, name string, cert []byte, policies string, allowedNames string, expectError bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "certs/" + name, ErrorOk: expectError, Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "lease": 1000, + "certificate": string(cert), + "policies": policies, + "display_name": name, + "allowed_names": allowedNames, + "lease": 1000, }, Check: func(resp *logical.Response) error { if resp == nil && expectError { @@ -730,10 +779,17 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLogin(req, nil) + empty_login_fd := &framework.FieldData{ + Raw: map[string]interface{}{}, + Schema: pathLogin(b).Fields, + } + resp, err = b.pathLogin(req, empty_login_fd) if err != nil { t.Fatal(err) } + if resp.IsError() { + t.Fatalf("got error: %#v", *resp) + } req.Auth.InternalData = resp.Auth.InternalData req.Auth.Metadata = resp.Auth.Metadata req.Auth.LeaseOptions = resp.Auth.LeaseOptions @@ -741,7 +797,7 @@ func Test_Renew(t *testing.T) { req.Auth.IssueTime = time.Now() // Normal renewal - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err != nil { t.Fatal(err) } @@ -759,7 +815,7 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err == nil { t.Fatal("expected error") } @@ -771,7 +827,7 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err != nil { t.Fatal(err) } @@ -788,7 +844,7 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index 4afc1eadc567..66809c2e3a8f 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -13,6 +13,7 @@ type CLIHandler struct{} func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { var data struct { Mount string `mapstructure:"mount"` + Name string `mapstructure:"name"` } if err := mapstructure.WeakDecode(m, &data); err != nil { return "", err @@ -22,8 +23,11 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { data.Mount = "cert" } + options := map[string]interface{}{ + "name": data.Name, + } path := fmt.Sprintf("auth/%s/login", data.Mount) - secret, err := c.Logical().Write(path, nil) + secret, err := c.Logical().Write(path, options) if err != nil { return "", err } @@ -38,10 +42,13 @@ func (h *CLIHandler) Help() string { help := ` The "cert" credential provider allows you to authenticate with a client certificate. No other authentication materials are needed. +Optionally, you may specify the specific certificate role to +authenticate against with the "name" parameter. Example: vault auth -method=cert \ -client-cert=/path/to/cert.pem \ -client-key=/path/to/key.pem + name=cert1 ` diff --git a/builtin/credential/cert/path_certs.go b/builtin/credential/cert/path_certs.go index d842bb841de6..2c002f6e3f3f 100644 --- a/builtin/credential/cert/path_certs.go +++ b/builtin/credential/cert/path_certs.go @@ -39,6 +39,12 @@ func pathCerts(b *backend) *framework.Path { Must be x509 PEM encoded.`, }, + "allowed_names": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `A comma-separated list of names. +At least one must exist in either the Common Name or SANs. Supports globbing.`, + }, + "display_name": &framework.FieldSchema{ Type: framework.TypeString, Description: `The display name to use for clients using this @@ -139,6 +145,7 @@ func (b *backend) pathCertWrite( certificate := d.Get("certificate").(string) displayName := d.Get("display_name").(string) policies := policyutil.ParsePolicies(d.Get("policies").(string)) + allowedNames := d.Get("allowed_names").([]string) // Default the display name to the certificate name if not given if displayName == "" { @@ -165,10 +172,11 @@ func (b *backend) pathCertWrite( } certEntry := &CertEntry{ - Name: name, - Certificate: certificate, - DisplayName: displayName, - Policies: policies, + Name: name, + Certificate: certificate, + DisplayName: displayName, + Policies: policies, + AllowedNames: allowedNames, } // Parse the lease duration or default to backend/system default @@ -196,11 +204,12 @@ func (b *backend) pathCertWrite( } type CertEntry struct { - Name string - Certificate string - DisplayName string - Policies []string - TTL time.Duration + Name string + Certificate string + DisplayName string + Policies []string + TTL time.Duration + AllowedNames []string } const pathCertHelpSyn = ` diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index d9fcbe8fd5f1..164bbe7de7b6 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + + "github.com/ryanuber/go-glob" ) // ParsedCert is a certificate that has been configured as trusted @@ -25,7 +27,12 @@ type ParsedCert struct { func pathLogin(b *backend) *framework.Path { return &framework.Path{ Pattern: "login", - Fields: map[string]*framework.FieldSchema{}, + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The name of the certificate role to authenticate against.", + }, + }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathLogin, }, @@ -36,7 +43,7 @@ func (b *backend) pathLogin( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { var matched *ParsedCert - if verifyResp, resp, err := b.verifyCredentials(req); err != nil { + if verifyResp, resp, err := b.verifyCredentials(req, data); err != nil { return nil, err } else if resp != nil { return resp, nil @@ -93,7 +100,7 @@ func (b *backend) pathLoginRenew( if !config.DisableBinding { var matched *ParsedCert - if verifyResp, resp, err := b.verifyCredentials(req); err != nil { + if verifyResp, resp, err := b.verifyCredentials(req, d); err != nil { return nil, err } else if resp != nil { return resp, nil @@ -136,7 +143,7 @@ func (b *backend) pathLoginRenew( return framework.LeaseExtend(cert.TTL, 0, b.System())(req, d) } -func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical.Response, error) { +func (b *backend) verifyCredentials(req *logical.Request, d *framework.FieldData) (*ParsedCert, *logical.Response, error) { // Get the connection state if req.Connection == nil || req.Connection.ConnState == nil { return nil, logical.ErrorResponse("tls connection required"), nil @@ -146,20 +153,29 @@ func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical if connState.PeerCertificates == nil || len(connState.PeerCertificates) == 0 { return nil, logical.ErrorResponse("client certificate must be supplied"), nil } + clientCert := connState.PeerCertificates[0] + + // Allow constraining the login request to a single CertEntry + certName := d.Get("name").(string) // Load the trusted certificates - roots, trusted, trustedNonCAs := b.loadTrustedCerts(req.Storage) + roots, trusted, trustedNonCAs := b.loadTrustedCerts(req.Storage, certName) // If trustedNonCAs is not empty it means that client had registered a non-CA cert // with the backend. if len(trustedNonCAs) != 0 { - policy := b.matchNonCAPolicy(connState.PeerCertificates[0], trustedNonCAs) - if policy != nil && !b.checkForChainInCRLs(policy.Certificates) { - return policy, nil, nil + for _, trustedNonCA := range trustedNonCAs { + tCert := trustedNonCA.Certificates[0] + // Check for client cert being explicitly listed in the config (and matching other constraints) + if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 && + bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) && + b.matchesConstraints(clientCert, trustedNonCA.Certificates, trustedNonCA) { + return trustedNonCA, nil, nil + } } } - // Validate the connection state is trusted + // Get the list of full chains matching the connection trustedChains, err := validateConnState(roots, connState) if err != nil { return nil, nil, err @@ -170,49 +186,58 @@ func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical return nil, logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil } - validChain := b.checkForValidChain(trustedChains) - if !validChain { - return nil, logical.ErrorResponse( - "no chain containing non-revoked certificates could be found for this login certificate", - ), nil + // Search for a ParsedCert that intersects with the validated chains and any additional constraints + matches := make([]*ParsedCert, 0) + for _, trust := range trusted { // For each ParsedCert in the config + for _, tCert := range trust.Certificates { // For each certificate in the entry + for _, chain := range trustedChains { // For each root chain that we matched + for _, cCert := range chain { // For each cert in the matched chain + if tCert.Equal(cCert) && // ParsedCert intersects with matched chain + b.matchesConstraints(clientCert, chain, trust) { // validate client cert + matched chain against the config + // Add the match to the list + matches = append(matches, trust) + } + } + } + } + } + + // Fail on no matches + if len(matches) == 0 { + return nil, logical.ErrorResponse("no chain matching all constraints could be found for this login certificate"), nil } - // Match the trusted chain with the policy - return b.matchPolicy(trustedChains, trusted), nil, nil + // Return the first matching entry (for backwards compatibility, we continue to just pick one if multiple match) + return matches[0], nil, nil } -// matchNonCAPolicy is used to match the client cert with the registered non-CA -// policies to establish client identity. -func (b *backend) matchNonCAPolicy(clientCert *x509.Certificate, trustedNonCAs []*ParsedCert) *ParsedCert { - for _, trustedNonCA := range trustedNonCAs { - tCert := trustedNonCA.Certificates[0] - if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 && bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) { - return trustedNonCA +func (b *backend) matchesConstraints(clientCert *x509.Certificate, trustedChain []*x509.Certificate, config *ParsedCert) bool { + // Default behavior (no names) is to allow all names + nameMatched := len(config.Entry.AllowedNames) == 0 + // At least one pattern must match at least one name if any patterns are specified + for _, allowedName := range config.Entry.AllowedNames { + if glob.Glob(allowedName, clientCert.Subject.CommonName) { + nameMatched = true } - } - return nil -} -// matchPolicy is used to match the associated policy with the certificate that -// was used to establish the client identity. -func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCert) *ParsedCert { - // There is probably a better way to do this... - for _, chain := range chains { - for _, trust := range trusted { - for _, tCert := range trust.Certificates { - for _, cCert := range chain { - if tCert.Equal(cCert) { - return trust - } - } + for _, name := range clientCert.DNSNames { + if glob.Glob(allowedName, name) { + nameMatched = true + } + } + + for _, name := range clientCert.EmailAddresses { + if glob.Glob(allowedName, name) { + nameMatched = true } } } - return nil + + return !b.checkForChainInCRLs(trustedChain) && nameMatched } // loadTrustedCerts is used to load all the trusted certificates from the backend -func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) { +func (b *backend) loadTrustedCerts(store logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) { pool = x509.NewCertPool() trusted = make([]*ParsedCert, 0) trustedNonCAs = make([]*ParsedCert, 0) @@ -222,6 +247,10 @@ func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, return } for _, name := range names { + // If we are trying to select a single CertEntry and this isn't it + if certName != "" && name != certName { + continue + } entry, err := b.Cert(store, strings.TrimPrefix(name, "cert/")) if err != nil { b.Logger().Error("cert: failed to load trusted cert", "name", name, "error", err) diff --git a/builtin/credential/ldap/path_config.go b/builtin/credential/ldap/path_config.go index b32aa10875ce..4fc772eaa3b0 100644 --- a/builtin/credential/ldap/path_config.go +++ b/builtin/credential/ldap/path_config.go @@ -400,7 +400,7 @@ func (c *ConfigEntry) DialLDAP() (*ldap.Conn, error) { } conn, err = ldap.DialTLS("tcp", net.JoinHostPort(host, port), tlsConfig) default: - retErr = multierror.Append(retErr, fmt.Errorf("invalid LDAP scheme in url %q")) + retErr = multierror.Append(retErr, fmt.Errorf("invalid LDAP scheme in url %q", net.JoinHostPort(host, port))) continue } if err == nil { diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 6bb1b99c29fa..4fefc95c13b1 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -1549,6 +1549,7 @@ func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { Wildcard bool `structs:"*.example.com"` SubSubdomain bool `structs:"foo.bar.example.com"` SubSubdomainWildcard bool `structs:"*.bar.example.com"` + GlobDomain bool `structs:"fooexample.com"` NonHostname bool `structs:"daɪˈɛrɨsɨs"` AnyHost bool `structs:"porkslap.beer"` } @@ -1755,6 +1756,11 @@ func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { commonNames.BareDomain = true addCnTests() + roleVals.AllowedDomains = "foobar.com,*example.com" + roleVals.AllowGlobDomains = true + commonNames.GlobDomain = true + addCnTests() + roleVals.AllowAnyName = true roleVals.EnforceHostnames = true commonNames.AnyHost = true diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index b688f7caa450..1796d98a012e 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + "github.com/ryanuber/go-glob" ) type certExtKeyUsage int @@ -185,33 +186,65 @@ func fetchCAInfo(req *logical.Request) (*caInfoBundle, error) { // Allows fetching certificates from the backend; it handles the slightly // separate pathing for CA, CRL, and revoked certificates. func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) { - var path string + var path, legacyPath string + var err error + var certEntry *logical.StorageEntry + + hyphenSerial := normalizeSerial(serial) + colonSerial := strings.Replace(strings.ToLower(serial), "-", ":", -1) 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) + legacyPath = "revoked/" + colonSerial + path = "revoked/" + hyphenSerial case serial == "ca": path = "ca" case serial == "crl": path = "crl" default: - path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1) + legacyPath = "certs/" + colonSerial + path = "certs/" + hyphenSerial } - 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 { + 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 + } + + // If legacyPath is unset, it's going to be a CA or CRL; return immediately + if legacyPath == "" { return nil, nil } + // Retrieve the old-style path + certEntry, err = req.Storage.Get(legacyPath) + 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)} } + // Update old-style paths to new-style paths + certEntry.Key = path + 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(legacyPath); err != nil { + return nil, errutil.InternalError{Err: fmt.Sprintf("error deleting certificate with serial %s from old location", serial)} + } + return certEntry, nil } @@ -362,6 +395,13 @@ func validateNames(req *logical.Request, names []string, role *roleEntry) string break } } + + if role.AllowGlobDomains && + strings.Contains(currDomain, "*") && + glob.Glob(currDomain, name) { + valid = true + break + } } if valid { continue @@ -466,7 +506,7 @@ func signCert(b *backend, } csr, err := x509.ParseCertificateRequest(pemBlock.Bytes) if err != nil { - return nil, errutil.UserError{Err: "certificate request could not be parsed"} + return nil, errutil.UserError{Err: fmt.Sprintf("certificate request could not be parsed: %v", err)} } switch role.KeyType { @@ -970,7 +1010,7 @@ func createCSR(creationInfo *creationBundle) (*certutil.ParsedCSRBundle, error) result.CSRBytes = csr result.CSR, err = x509.ParseCertificateRequest(csr) if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %s", err)} + return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %v", err)} } return result, nil diff --git a/builtin/logical/pki/cert_util_test.go b/builtin/logical/pki/cert_util_test.go new file mode 100644 index 000000000000..068a0a69a757 --- /dev/null +++ b/builtin/logical/pki/cert_util_test.go @@ -0,0 +1,124 @@ +package pki + +import ( + "fmt" + "testing" + + "strings" + + "github.com/hashicorp/vault/logical" +) + +func TestPki_FetchCertBySerial(t *testing.T) { + storage := &logical.InmemStorage{} + + cases := map[string]struct { + Req *logical.Request + Prefix string + Serial string + }{ + "valid cert": { + &logical.Request{ + Storage: storage, + }, + "certs/", + "00:00:00:00:00:00:00:00", + }, + "revoked cert": { + &logical.Request{ + Storage: storage, + }, + "revoked/", + "11:11:11:11:11:11:11:11", + }, + } + + // Test for colon-based paths in storage + for name, tc := range cases { + storageKey := fmt.Sprintf("%s%s", tc.Prefix, tc.Serial) + err := storage.Put(&logical.StorageEntry{ + Key: storageKey, + Value: []byte("some data"), + }) + if err != nil { + t.Fatalf("error writing to storage on %s colon-based storage path: %s", name, err) + } + + certEntry, err := fetchCertBySerial(tc.Req, tc.Prefix, tc.Serial) + if err != nil { + t.Fatalf("error on %s for colon-based storage path: %s", name, err) + } + + // Check for non-nil on valid/revoked certs + if certEntry == nil { + t.Fatalf("nil on %s for colon-based storage path", name) + } + + // Ensure that cert serials are converted/updated after fetch + expectedKey := tc.Prefix + normalizeSerial(tc.Serial) + se, err := storage.Get(expectedKey) + if err != nil { + t.Fatalf("error on %s for colon-based storage path:%s", name, err) + } + if strings.Compare(expectedKey, se.Key) != 0 { + t.Fatalf("expected: %s, got: %s", expectedKey, certEntry.Key) + } + } + + // Reset storage + storage = &logical.InmemStorage{} + + // Test for hyphen-base paths in storage + for name, tc := range cases { + storageKey := tc.Prefix + normalizeSerial(tc.Serial) + err := storage.Put(&logical.StorageEntry{ + Key: storageKey, + Value: []byte("some data"), + }) + if err != nil { + t.Fatalf("error writing to storage on %s hyphen-based storage path: %s", name, err) + } + + certEntry, err := fetchCertBySerial(tc.Req, tc.Prefix, tc.Serial) + if err != nil || certEntry == nil { + t.Fatalf("error on %s for hyphen-based storage path: err: %v, entry: %v", name, err, certEntry) + } + } + + noConvCases := map[string]struct { + Req *logical.Request + Prefix string + Serial string + }{ + "ca": { + &logical.Request{ + Storage: storage, + }, + "", + "ca", + }, + "crl": { + &logical.Request{ + Storage: storage, + }, + "", + "crl", + }, + } + + // Test for ca and crl case + for name, tc := range noConvCases { + err := storage.Put(&logical.StorageEntry{ + Key: tc.Serial, + Value: []byte("some data"), + }) + if err != nil { + t.Fatalf("error writing to storage on %s: %s", name, err) + } + + certEntry, err := fetchCertBySerial(tc.Req, tc.Prefix, tc.Serial) + if err != nil || certEntry == nil { + t.Fatalf("error on %s: err: %v, entry: %v", name, err, certEntry) + } + } +} diff --git a/builtin/logical/pki/crl_util.go b/builtin/logical/pki/crl_util.go index aa15f6cc617f..c40e759aab0f 100644 --- a/builtin/logical/pki/crl_util.go +++ b/builtin/logical/pki/crl_util.go @@ -65,7 +65,7 @@ func revokeCert(b *backend, req *logical.Request, serial string, fromLease bool) cert, err := x509.ParseCertificate(certEntry.Value) if err != nil { - return nil, fmt.Errorf("Error parsing certificate") + return nil, fmt.Errorf("Error parsing certificate: %s", err) } if cert == nil { return nil, fmt.Errorf("Got a nil certificate") @@ -86,7 +86,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/"+normalizeSerial(serial), 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..71a04555ebf5 100644 --- a/builtin/logical/pki/path_intermediate.go +++ b/builtin/logical/pki/path_intermediate.go @@ -196,7 +196,7 @@ func (b *backend) pathSetSignedIntermediate( return nil, err } - entry.Key = "certs/" + cb.SerialNumber + entry.Key = "certs/" + normalizeSerial(cb.SerialNumber) 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 f615f98d1dab..26f74210464b 100644 --- a/builtin/logical/pki/path_issue_sign.go +++ b/builtin/logical/pki/path_issue_sign.go @@ -269,11 +269,11 @@ func (b *backend) pathIssueSignCert( if !role.NoStore { err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + cb.SerialNumber, + Key: "certs/" + normalizeSerial(cb.SerialNumber), 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_roles.go b/builtin/logical/pki/path_roles.go index 2095fbde3c55..4d9e11567d98 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -84,6 +84,14 @@ including wildcard subdomains. See the documentation for more information.`, }, + "allow_glob_domains": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: `If set, domains specified in "allowed_domains" +can include glob patterns, e.g. "ftp*.example.com". See +the documentation for more information.`, + }, + "allow_any_name": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, @@ -380,6 +388,7 @@ func (b *backend) pathRoleCreate( AllowedDomains: data.Get("allowed_domains").(string), AllowBareDomains: data.Get("allow_bare_domains").(bool), AllowSubdomains: data.Get("allow_subdomains").(bool), + AllowGlobDomains: data.Get("allow_glob_domains").(bool), AllowAnyName: data.Get("allow_any_name").(bool), EnforceHostnames: data.Get("enforce_hostnames").(bool), AllowIPSANs: data.Get("allow_ip_sans").(bool), @@ -505,6 +514,7 @@ type roleEntry struct { AllowBareDomains bool `json:"allow_bare_domains" structs:"allow_bare_domains" mapstructure:"allow_bare_domains"` AllowTokenDisplayName bool `json:"allow_token_displayname" structs:"allow_token_displayname" mapstructure:"allow_token_displayname"` AllowSubdomains bool `json:"allow_subdomains" structs:"allow_subdomains" mapstructure:"allow_subdomains"` + AllowGlobDomains bool `json:"allow_glob_domains" structs:"allow_glob_domains" mapstructure:"allow_glob_domains"` AllowAnyName bool `json:"allow_any_name" structs:"allow_any_name" mapstructure:"allow_any_name"` EnforceHostnames bool `json:"enforce_hostnames" structs:"enforce_hostnames" mapstructure:"enforce_hostnames"` AllowIPSANs bool `json:"allow_ip_sans" structs:"allow_ip_sans" mapstructure:"allow_ip_sans"` diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index c9a8cf297e78..d02953133cb5 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -145,7 +145,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/" + normalizeSerial(cb.SerialNumber), Value: parsedBundle.CertificateBytes, }) if err != nil { @@ -277,7 +277,7 @@ func (b *backend) pathCASignIntermediate( } err = req.Storage.Put(&logical.StorageEntry{ - Key: "certs/" + cb.SerialNumber, + Key: "certs/" + normalizeSerial(cb.SerialNumber), 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) } diff --git a/builtin/logical/pki/util.go b/builtin/logical/pki/util.go new file mode 100644 index 000000000000..3dffb536bd85 --- /dev/null +++ b/builtin/logical/pki/util.go @@ -0,0 +1,7 @@ +package pki + +import "strings" + +func normalizeSerial(serial string) string { + return strings.Replace(strings.ToLower(serial), ":", "-", -1) +} diff --git a/cli/commands.go b/cli/commands.go index 13f7c8b25aad..6075d36933d1 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" @@ -72,7 +72,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, @@ -120,6 +120,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/command/server.go b/command/server.go index 9697c1dc853b..c5b952ce879e 100644 --- a/command/server.go +++ b/command/server.go @@ -84,6 +84,7 @@ func (c *ServerCommand) Run(args []string) int { // start logging too early. logGate := &gatedwriter.Writer{Writer: colorable.NewColorable(os.Stderr)} var level int + logLevel = strings.ToLower(strings.TrimSpace(logLevel)) switch logLevel { case "trace": level = log.LevelTrace diff --git a/command/write.go b/command/write.go index 0614f9b1986a..21478f9f00f5 100644 --- a/command/write.go +++ b/command/write.go @@ -32,6 +32,12 @@ func (c *WriteCommand) Run(args []string) int { } args = flags.Args() + if len(args) < 1 { + c.Ui.Error("write requires a path") + flags.Usage() + return 1 + } + if len(args) < 2 && !force { c.Ui.Error("write expects at least two arguments; use -f to perform the write anyways") flags.Usage() diff --git a/helper/awsutil/generate_credentials.go b/helper/awsutil/generate_credentials.go index 7605c84bbb56..7399a5ca367c 100644 --- a/helper/awsutil/generate_credentials.go +++ b/helper/awsutil/generate_credentials.go @@ -48,7 +48,7 @@ func (c *CredentialsConfig) GenerateCredentialChain() (*credentials.Credentials, SecretAccessKey: c.SecretKey, SessionToken: c.SessionToken, }}) - case c.AccessKey == "" && c.AccessKey == "": + case c.AccessKey == "" && c.SecretKey == "": // Attempt to get credentials from the IAM instance role below default: // Have one or the other but not both and not neither diff --git a/helper/certutil/types.go b/helper/certutil/types.go index aa8d115f0fa1..35b7317a5eaa 100644 --- a/helper/certutil/types.go +++ b/helper/certutil/types.go @@ -444,7 +444,7 @@ func (c *CSRBundle) ToParsedCSRBundle() (*ParsedCSRBundle, error) { result.CSRBytes = pemBlock.Bytes result.CSR, err = x509.ParseCertificateRequest(result.CSRBytes) if err != nil { - return nil, errutil.UserError{"Error encountered parsing certificate bytes from raw bundle"} + return nil, errutil.UserError{fmt.Sprintf("Error encountered parsing certificate bytes from raw bundle: %v", err)} } } diff --git a/helper/keysutil/lock_manager.go b/helper/keysutil/lock_manager.go index 380c39af11eb..e0bdd648a284 100644 --- a/helper/keysutil/lock_manager.go +++ b/helper/keysutil/lock_manager.go @@ -248,7 +248,7 @@ func (lm *LockManager) getPolicyCommon(req PolicyRequest, lockType bool) (*Polic case KeyType_ECDSA_P256: if req.Derived || req.Convergent { - return nil, nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %s", KeyType_ECDSA_P256) + return nil, nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %v", KeyType_ECDSA_P256) } default: diff --git a/http/logical_test.go b/http/logical_test.go index 1a2cdd065fba..bbbd89296654 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -152,6 +152,7 @@ func TestLogical_StandbyRedirect(t *testing.T) { "ttl": json.Number("0"), "creation_ttl": json.Number("0"), "explicit_max_ttl": json.Number("0"), + "expire_time": nil, }, "warnings": nilWarnings, "wrap_info": nil, diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index ea9f8d98ad86..41cb2a540c51 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -312,6 +312,7 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) { "ttl": json.Number("0"), "path": "auth/token/root", "explicit_max_ttl": json.Number("0"), + "expire_time": nil, } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") @@ -401,6 +402,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) { "ttl": json.Number("0"), "path": "auth/token/root", "explicit_max_ttl": json.Number("0"), + "expire_time": nil, } resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") 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/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 diff --git a/shamir/shamir.go b/shamir/shamir.go index 96d48361aced..d6f5137e5b35 100644 --- a/shamir/shamir.go +++ b/shamir/shamir.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "crypto/subtle" "fmt" + mathrand "math/rand" + "time" ) const ( @@ -166,13 +168,17 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { return nil, fmt.Errorf("cannot split an empty secret") } + // Generate random list of x coordinates + mathrand.Seed(time.Now().UnixNano()) + xCoordinates := mathrand.Perm(255) + // Allocate the output array, initialize the final byte // of the output with the offset. The representation of each // output is {y1, y2, .., yN, x}. out := make([][]byte, parts) for idx := range out { out[idx] = make([]byte, len(secret)+1) - out[idx][len(secret)] = uint8(idx) + 1 + out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1 } // Construct a random polynomial for each byte of the secret. @@ -189,7 +195,7 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { // We cheat by encoding the x value once as the final index, // so that it only needs to be stored once. for i := 0; i < parts; i++ { - x := uint8(i) + 1 + x := uint8(xCoordinates[i]) + 1 y := p.evaluate(x) out[i][idx] = y } 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/expiration.go b/vault/expiration.go index 87fceaba6755..f0f885edbd81 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -395,7 +395,7 @@ func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*log } // Check if the lease is renewable - if err := le.renewable(); err != nil { + if _, err := le.renewable(); err != nil { return nil, err } @@ -450,7 +450,7 @@ func (m *ExpirationManager) RenewToken(req *logical.Request, source string, toke // Check if the lease is renewable. Note that this also checks for a nil // lease and errors in that case as well. - if err := le.renewable(); err != nil { + if _, err := le.renewable(); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } @@ -841,29 +841,34 @@ type leaseEntry struct { } // encode is used to JSON encode the lease entry -func (l *leaseEntry) encode() ([]byte, error) { - return json.Marshal(l) +func (le *leaseEntry) encode() ([]byte, error) { + return json.Marshal(le) } -func (le *leaseEntry) renewable() error { +func (le *leaseEntry) renewable() (bool, error) { + var err error + switch { // If there is no entry, cannot review - if le == nil || le.ExpireTime.IsZero() { - return fmt.Errorf("lease not found or lease is not renewable") - } - + case le == nil || le.ExpireTime.IsZero(): + err = fmt.Errorf("lease not found or lease is not renewable") // Determine if the lease is expired - if le.ExpireTime.Before(time.Now()) { - return fmt.Errorf("lease expired") - } - + case le.ExpireTime.Before(time.Now()): + err = fmt.Errorf("lease expired") // Determine if the lease is renewable - if le.Secret != nil && !le.Secret.Renewable { - return fmt.Errorf("lease is not renewable") + case le.Secret != nil && !le.Secret.Renewable: + err = fmt.Errorf("lease is not renewable") + case le.Auth != nil && !le.Auth.Renewable: + err = fmt.Errorf("lease is not renewable") } - if le.Auth != nil && !le.Auth.Renewable { - return fmt.Errorf("lease is not renewable") + + if err != nil { + return false, err } - return nil + return true, nil +} + +func (le *leaseEntry) ttl() int64 { + return int64(le.ExpireTime.Sub(time.Now().Round(time.Second)).Seconds()) } // decodeLeaseEntry is used to reverse encode and return a new entry diff --git a/vault/expiration_test.go b/vault/expiration_test.go index b8255b3e9d6b..ced6b42318f7 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -1075,7 +1075,8 @@ func TestLeaseEntry(t *testing.T) { }, Secret: &logical.Secret{ LeaseOptions: logical.LeaseOptions{ - TTL: time.Minute, + TTL: time.Minute, + Renewable: true, }, }, IssueTime: time.Now(), @@ -1095,6 +1096,37 @@ func TestLeaseEntry(t *testing.T) { if !reflect.DeepEqual(out.Data, le.Data) { t.Fatalf("got: %#v, expect %#v", out, le) } + + // Test renewability + le.ExpireTime = time.Time{} + if r, _ := le.renewable(); r { + t.Fatal("lease with zero expire time is not renewable") + } + le.ExpireTime = time.Now().Add(-1 * time.Hour) + if r, _ := le.renewable(); r { + t.Fatal("lease with expire time in the past is not renewable") + } + le.ExpireTime = time.Now().Add(1 * time.Hour) + if r, err := le.renewable(); !r { + t.Fatalf("lease with future expire time is renewable, err: %v", err) + } + le.Secret.LeaseOptions.Renewable = false + if r, _ := le.renewable(); r { + t.Fatal("secret is set to not be renewable but returns as renewable") + } + le.Secret = nil + le.Auth = &logical.Auth{ + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + }, + } + if r, err := le.renewable(); !r { + t.Fatalf("auth is renewable but is set to not be, err: %v", err) + } + le.Auth.LeaseOptions.Renewable = false + if r, _ := le.renewable(); r { + t.Fatal("auth is set to not be renewable but returns as renewable") + } } func TestExpiration_RevokeForce(t *testing.T) { diff --git a/vault/logical_system.go b/vault/logical_system.go index 4dd66f8147e6..4a7f28fb793a 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -56,7 +56,6 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen Root: []string{ "auth/*", "remount", - "revoke-prefix/*", "audit", "audit/*", "raw/*", @@ -65,6 +64,10 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen "rotate", "config/auditing/*", "plugins/catalog/*", + "revoke-prefix/*", + "leases/revoke-prefix/*", + "leases/revoke-force/*", + "leases/lookup/*", }, Unauthenticated: []string{ @@ -301,7 +304,43 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "renew" + framework.OptionalParamRegex("url_lease_id"), + Pattern: "leases/lookup/(?P.+?)?", + + Fields: map[string]*framework.FieldSchema{ + "prefix": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["leases-list-prefix"][0]), + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.handleLeaseLookupList, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["leases"][0]), + HelpDescription: strings.TrimSpace(sysHelp["leases"][1]), + }, + + &framework.Path{ + Pattern: "leases/lookup", + + Fields: map[string]*framework.FieldSchema{ + "lease_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["lease_id"][0]), + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.handleLeaseLookup, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["leases"][0]), + HelpDescription: strings.TrimSpace(sysHelp["leases"][1]), + }, + + &framework.Path{ + Pattern: "(leases/)?renew" + framework.OptionalParamRegex("url_lease_id"), Fields: map[string]*framework.FieldSchema{ "url_lease_id": &framework.FieldSchema{ @@ -327,9 +366,13 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "revoke/(?P.+)", + Pattern: "(leases/)?revoke" + framework.OptionalParamRegex("url_lease_id"), Fields: map[string]*framework.FieldSchema{ + "url_lease_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["lease_id"][0]), + }, "lease_id": &framework.FieldSchema{ Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["lease_id"][0]), @@ -345,7 +388,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "revoke-force/(?P.+)", + Pattern: "(leases/)?revoke-force/(?P.+)", Fields: map[string]*framework.FieldSchema{ "prefix": &framework.FieldSchema{ @@ -363,7 +406,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, &framework.Path{ - Pattern: "revoke-prefix/(?P.+)", + Pattern: "(leases/)?revoke-prefix/(?P.+)", Fields: map[string]*framework.FieldSchema{ "prefix": &framework.FieldSchema{ @@ -684,6 +727,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen HelpSynopsis: strings.TrimSpace(sysHelp["audited-headers-name"][0]), HelpDescription: strings.TrimSpace(sysHelp["audited-headers-name"][1]), }, + &framework.Path{ Pattern: "config/auditing/request-headers$", @@ -1385,6 +1429,61 @@ func (b *SystemBackend) handleTuneWriteCommon( return nil, nil } +// handleLease is use to view the metadata for a given LeaseID +func (b *SystemBackend) handleLeaseLookup( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + leaseID := data.Get("lease_id").(string) + if leaseID == "" { + return logical.ErrorResponse("lease_id must be specified"), + logical.ErrInvalidRequest + } + + leaseTimes, err := b.Core.expiration.FetchLeaseTimes(leaseID) + if err != nil { + b.Backend.Logger().Error("sys: error retrieving lease", "lease_id", leaseID, "error", err) + return handleError(err) + } + if leaseTimes == nil { + return logical.ErrorResponse("invalid lease"), logical.ErrInvalidRequest + } + + resp := &logical.Response{ + Data: map[string]interface{}{ + "id": leaseID, + "issue_time": leaseTimes.IssueTime, + "expire_time": nil, + "last_renewal": nil, + "ttl": int64(0), + }, + } + renewable, _ := leaseTimes.renewable() + resp.Data["renewable"] = renewable + + if !leaseTimes.LastRenewalTime.IsZero() { + resp.Data["last_renewal"] = leaseTimes.LastRenewalTime + } + if !leaseTimes.ExpireTime.IsZero() { + resp.Data["expire_time"] = leaseTimes.ExpireTime + resp.Data["ttl"] = leaseTimes.ttl() + } + return resp, nil +} + +func (b *SystemBackend) handleLeaseLookupList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + prefix := data.Get("prefix").(string) + if prefix != "" && !strings.HasSuffix(prefix, "/") { + prefix = prefix + "/" + } + + keys, err := b.Core.expiration.idView.List(prefix) + if err != nil { + b.Backend.Logger().Error("sys: error listing leases", "prefix", prefix, "error", err) + return handleError(err) + } + return logical.ListResponse(keys), nil +} + // handleRenew is used to renew a lease with a given LeaseID func (b *SystemBackend) handleRenew( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -1393,6 +1492,10 @@ func (b *SystemBackend) handleRenew( if leaseID == "" { leaseID = data.Get("url_lease_id").(string) } + if leaseID == "" { + return logical.ErrorResponse("lease_id must be specified"), + logical.ErrInvalidRequest + } incrementRaw := data.Get("increment").(int) // Convert the increment @@ -1412,6 +1515,13 @@ func (b *SystemBackend) handleRevoke( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // Get all the options leaseID := data.Get("lease_id").(string) + if leaseID == "" { + leaseID = data.Get("url_lease_id").(string) + } + if leaseID == "" { + return logical.ErrorResponse("lease_id must be specified"), + logical.ErrInvalidRequest + } // Invoke the expiration manager directly if err := b.Core.expiration.Revoke(leaseID); err != nil { @@ -2546,4 +2656,21 @@ This path responds to the following HTTP methods. Delete the plugin with the given name. `, }, + "leases": { + `View or list lease metadata.`, + ` +This path responds to the following HTTP methods. + + PUT / + Retrieve the metadata for the provided lease id. + + LIST / + Lists the leases for the named prefix. + `, + }, + + "leases-list-prefix": { + `The path to list leases under. Example: "aws/creds/deploy"`, + "", + }, } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index aa2ce449a1ca..0a84c04af717 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -18,13 +18,13 @@ import ( "github.com/hashicorp/vault/helper/pluginutil" "github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/logical" + "github.com/mitchellh/mapstructure" ) func TestSystemBackend_RootPaths(t *testing.T) { expected := []string{ "auth/*", "remount", - "revoke-prefix/*", "audit", "audit/*", "raw/*", @@ -33,6 +33,10 @@ func TestSystemBackend_RootPaths(t *testing.T) { "rotate", "config/auditing/*", "plugins/catalog/*", + "revoke-prefix/*", + "leases/revoke-prefix/*", + "leases/revoke-force/*", + "leases/lookup/*", } b := testSystemBackend(t) @@ -323,6 +327,196 @@ func TestSystemBackend_remount_system(t *testing.T) { } } +func TestSystemBackend_leases(t *testing.T) { + core, b, root := testCoreSystemBackend(t) + + // Create a key with a lease + req := logical.TestRequest(t, logical.UpdateOperation, "secret/foo") + req.Data["foo"] = "bar" + req.ClientToken = root + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // Read lease + req = logical.TestRequest(t, logical.UpdateOperation, "leases/lookup") + req.Data["lease_id"] = resp.Secret.LeaseID + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Data["renewable"] == nil || resp.Data["renewable"].(bool) { + t.Fatal("generic leases are not renewable") + } + + // Invalid lease + req = logical.TestRequest(t, logical.UpdateOperation, "leases/lookup") + req.Data["lease_id"] = "invalid" + resp, err = b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("expected invalid request, got err: %v", err) + } +} + +func TestSystemBackend_leases_list(t *testing.T) { + core, b, root := testCoreSystemBackend(t) + + // Create a key with a lease + req := logical.TestRequest(t, logical.UpdateOperation, "secret/foo") + req.Data["foo"] = "bar" + req.ClientToken = root + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // List top level + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + var keys []string + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 1 { + t.Fatalf("Expected 1 subkey lease, got %d: %#v", len(keys), keys) + } + if keys[0] != "secret/" { + t.Fatal("Expected only secret subkey") + } + + // List lease + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/secret/foo") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + keys = []string{} + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 1 { + t.Fatalf("Expected 1 secret lease, got %d: %#v", len(keys), keys) + } + + // Generate multiple leases + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/secret/foo") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + keys = []string{} + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 3 { + t.Fatalf("Expected 3 secret lease, got %d: %#v", len(keys), keys) + } + + // Listing subkeys + req = logical.TestRequest(t, logical.UpdateOperation, "secret/bar") + req.Data["foo"] = "bar" + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/bar") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + req = logical.TestRequest(t, logical.ListOperation, "leases/lookup/secret") + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("bad: %#v", resp) + } + keys = []string{} + if err := mapstructure.WeakDecode(resp.Data["keys"], &keys); err != nil { + t.Fatalf("err: %v", err) + } + if len(keys) != 2 { + t.Fatalf("Expected 2 secret lease, got %d: %#v", len(keys), keys) + } + expected := []string{"bar/", "foo/"} + if !reflect.DeepEqual(expected, keys) { + t.Fatalf("exp: %#v, act: %#v", expected, keys) + } +} + func TestSystemBackend_renew(t *testing.T) { core, b, root := testCoreSystemBackend(t) @@ -350,7 +544,7 @@ func TestSystemBackend_renew(t *testing.T) { } // Attempt renew - req2 := logical.TestRequest(t, logical.UpdateOperation, "renew/"+resp.Secret.LeaseID) + req2 := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID) resp2, err := b.HandleRequest(req2) if err != logical.ErrInvalidRequest { t.Fatalf("err: %v", err) @@ -386,7 +580,7 @@ func TestSystemBackend_renew(t *testing.T) { } // Attempt renew - req2 = logical.TestRequest(t, logical.UpdateOperation, "renew/"+resp.Secret.LeaseID) + req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID) resp2, err = b.HandleRequest(req2) if err != nil { t.Fatalf("err: %v", err) @@ -402,6 +596,23 @@ func TestSystemBackend_renew(t *testing.T) { } // Test the other route path + req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/renew") + req2.Data["lease_id"] = resp.Secret.LeaseID + resp2, err = b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp2.IsError() { + t.Fatalf("got an error") + } + if resp2.Data == nil { + t.Fatal("nil data") + } + if resp.Secret.TTL != 180*time.Second { + t.Fatalf("bad lease duration: %v", resp.Secret.TTL) + } + + // Test orig path req2 = logical.TestRequest(t, logical.UpdateOperation, "renew") req2.Data["lease_id"] = resp.Secret.LeaseID resp2, err = b.HandleRequest(req2) @@ -422,6 +633,31 @@ func TestSystemBackend_renew(t *testing.T) { func TestSystemBackend_renew_invalidID(t *testing.T) { b := testSystemBackend(t) + // Attempt renew + req := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/foobarbaz") + resp, err := b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } + + // Attempt renew with other method + req = logical.TestRequest(t, logical.UpdateOperation, "leases/renew") + req.Data["lease_id"] = "foobarbaz" + resp, err = b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } +} + +func TestSystemBackend_renew_invalidID_origUrl(t *testing.T) { + b := testSystemBackend(t) + // Attempt renew req := logical.TestRequest(t, logical.UpdateOperation, "renew/foobarbaz") resp, err := b.HandleRequest(req) @@ -431,6 +667,17 @@ func TestSystemBackend_renew_invalidID(t *testing.T) { if resp.Data["error"] != "lease not found or lease is not renewable" { t.Fatalf("bad: %v", resp) } + + // Attempt renew with other method + req = logical.TestRequest(t, logical.UpdateOperation, "renew") + req.Data["lease_id"] = "foobarbaz" + resp, err = b.HandleRequest(req) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } } func TestSystemBackend_revoke(t *testing.T) { @@ -479,12 +726,81 @@ func TestSystemBackend_revoke(t *testing.T) { if resp3.Data["error"] != "lease not found or lease is not renewable" { t.Fatalf("bad: %v", resp) } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // Test the other route path + req2 = logical.TestRequest(t, logical.UpdateOperation, "revoke") + req2.Data["lease_id"] = resp.Secret.LeaseID + resp2, err = b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v %#v", err, resp2) + } + if resp2 != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + + // Test the other route path + req2 = logical.TestRequest(t, logical.UpdateOperation, "leases/revoke") + req2.Data["lease_id"] = resp.Secret.LeaseID + resp2, err = b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v %#v", err, resp2) + } + if resp2 != nil { + t.Fatalf("bad: %#v", resp) + } } func TestSystemBackend_revoke_invalidID(t *testing.T) { b := testSystemBackend(t) - // Attempt renew + // Attempt revoke + req := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke/foobarbaz") + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } + + // Attempt revoke with other method + req = logical.TestRequest(t, logical.UpdateOperation, "leases/revoke") + req.Data["lease_id"] = "foobarbaz" + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } +} + +func TestSystemBackend_revoke_invalidID_origUrl(t *testing.T) { + b := testSystemBackend(t) + + // Attempt revoke req := logical.TestRequest(t, logical.UpdateOperation, "revoke/foobarbaz") resp, err := b.HandleRequest(req) if err != nil { @@ -493,6 +809,17 @@ func TestSystemBackend_revoke_invalidID(t *testing.T) { if resp != nil { t.Fatalf("bad: %v", resp) } + + // Attempt revoke with other method + req = logical.TestRequest(t, logical.UpdateOperation, "revoke") + req.Data["lease_id"] = "foobarbaz" + resp, err = b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } } func TestSystemBackend_revokePrefix(t *testing.T) { @@ -522,6 +849,54 @@ func TestSystemBackend_revokePrefix(t *testing.T) { t.Fatalf("bad: %#v", resp) } + // Attempt revoke + req2 := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke-prefix/secret/") + resp2, err := b.HandleRequest(req2) + if err != nil { + t.Fatalf("err: %v %#v", err, resp2) + } + if resp2 != nil { + t.Fatalf("bad: %#v", resp) + } + + // Attempt renew + req3 := logical.TestRequest(t, logical.UpdateOperation, "leases/renew/"+resp.Secret.LeaseID) + resp3, err := b.HandleRequest(req3) + if err != logical.ErrInvalidRequest { + t.Fatalf("err: %v", err) + } + if resp3.Data["error"] != "lease not found or lease is not renewable" { + t.Fatalf("bad: %v", resp) + } +} + +func TestSystemBackend_revokePrefix_origUrl(t *testing.T) { + core, b, root := testCoreSystemBackend(t) + + // Create a key with a lease + req := logical.TestRequest(t, logical.UpdateOperation, "secret/foo") + req.Data["foo"] = "bar" + req.Data["lease"] = "1h" + req.ClientToken = root + resp, err := core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + // Read a key with a LeaseID + req = logical.TestRequest(t, logical.ReadOperation, "secret/foo") + req.ClientToken = root + resp, err = core.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil || resp.Secret == nil || resp.Secret.LeaseID == "" { + t.Fatalf("bad: %#v", resp) + } + // Attempt revoke req2 := logical.TestRequest(t, logical.UpdateOperation, "revoke-prefix/secret/") resp2, err := b.HandleRequest(req2) @@ -588,6 +963,69 @@ func TestSystemBackend_revokePrefixAuth(t *testing.T) { t.Fatalf("err: %v", err) } + req := logical.TestRequest(t, logical.UpdateOperation, "leases/revoke-prefix/auth/github/") + resp, err := b.HandleRequest(req) + if err != nil { + t.Fatalf("err: %v %v", err, resp) + } + if resp != nil { + t.Fatalf("bad: %#v", resp) + } + + te, err = ts.Lookup(te.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if te != nil { + t.Fatalf("bad: %v", te) + } +} + +func TestSystemBackend_revokePrefixAuth_origUrl(t *testing.T) { + core, ts, _, _ := TestCoreWithTokenStore(t) + bc := &logical.BackendConfig{ + Logger: core.logger, + System: logical.StaticSystemView{ + DefaultLeaseTTLVal: time.Hour * 24, + MaxLeaseTTLVal: time.Hour * 24 * 32, + }, + } + b, err := NewSystemBackend(core, bc) + if err != nil { + t.Fatal(err) + } + + exp := ts.expiration + + te := &TokenEntry{ + ID: "foo", + Path: "auth/github/login/bar", + } + err = ts.create(te) + if err != nil { + t.Fatal(err) + } + + te, err = ts.Lookup("foo") + if err != nil { + t.Fatal(err) + } + if te == nil { + t.Fatal("token entry was nil") + } + + // Create a new token + auth := &logical.Auth{ + ClientToken: te.ID, + LeaseOptions: logical.LeaseOptions{ + TTL: time.Hour, + }, + } + err = exp.RegisterAuth(te.Path, auth) + if err != nil { + t.Fatalf("err: %v", err) + } + req := logical.TestRequest(t, logical.UpdateOperation, "revoke-prefix/auth/github/") resp, err := b.HandleRequest(req) if err != nil { diff --git a/vault/policy_store.go b/vault/policy_store.go index 27a5f7ec4e0b..0768f76f788a 100644 --- a/vault/policy_store.go +++ b/vault/policy_store.go @@ -60,10 +60,20 @@ path "sys/capabilities-self" { capabilities = ["update"] } -# Allow a token to renew a lease via lease_id in the request body +# Allow a token to renew a lease via lease_id in the request body; old path for +# old clients, new path for newer path "sys/renew" { capabilities = ["update"] } +path "sys/leases/renew" { + capabilities = ["update"] +} + +# Allow looking up lease properties. This requires knowing the lease ID ahead +# of time and does not divulge any sensitive information. +path "sys/leases/lookup" { + capabilities = ["update"] +} # Allow a token to manage its own cubbyhole path "cubbyhole/*" { 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/vault/router_test.go b/vault/router_test.go index 5ba274bc8e90..e5de72e9773b 100644 --- a/vault/router_test.go +++ b/vault/router_test.go @@ -95,7 +95,7 @@ func TestRouter_Mount(t *testing.T) { } if v := r.MatchingStorageView("prod/aws/foo"); v != view { - t.Fatalf("bad: %s", v) + t.Fatalf("bad: %v", v) } if path := r.MatchingMount("stage/aws/foo"); path != "" { @@ -103,7 +103,7 @@ func TestRouter_Mount(t *testing.T) { } if v := r.MatchingStorageView("stage/aws/foo"); v != nil { - t.Fatalf("bad: %s", v) + t.Fatalf("bad: %v", v) } mount, prefix, ok := r.MatchingStoragePrefix("logical/foo") diff --git a/vault/token_store.go b/vault/token_store.go index 055b43905cf9..46614ed622b9 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -1010,7 +1010,7 @@ func (ts *TokenStore) RevokeTree(id string) error { defer metrics.MeasureSince([]string{"token", "revoke-tree"}, time.Now()) // Verify the token is not blank if id == "" { - return fmt.Errorf("cannot revoke blank token") + return fmt.Errorf("cannot tree-revoke blank token") } // Get the salted ID @@ -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 @@ -1858,6 +1858,7 @@ func (ts *TokenStore) handleLookup( "orphan": false, "creation_time": int64(out.CreationTime), "creation_ttl": int64(out.TTL.Seconds()), + "expire_time": nil, "ttl": int64(0), "explicit_max_ttl": int64(out.ExplicitMaxTTL.Seconds()), }, @@ -1882,15 +1883,15 @@ func (ts *TokenStore) handleLookup( if leaseTimes != nil { if !leaseTimes.LastRenewalTime.IsZero() { resp.Data["last_renewal_time"] = leaseTimes.LastRenewalTime.Unix() + resp.Data["last_renewal"] = leaseTimes.LastRenewalTime } if !leaseTimes.ExpireTime.IsZero() { - resp.Data["ttl"] = int64(leaseTimes.ExpireTime.Sub(time.Now().Round(time.Second)).Seconds()) - } - if err := leaseTimes.renewable(); err == nil { - resp.Data["renewable"] = true - } else { - resp.Data["renewable"] = false + resp.Data["expire_time"] = leaseTimes.ExpireTime + resp.Data["ttl"] = leaseTimes.ttl() } + renewable, _ := leaseTimes.renewable() + resp.Data["renewable"] = renewable + resp.Data["issue_time"] = leaseTimes.IssueTime } if urltoken { @@ -2173,7 +2174,7 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate( } resp.AddWarning(fmt.Sprintf( "Given explicit max TTL of %d is greater than system/mount allowed value of %d seconds; until this is fixed attempting to create tokens against this role will result in an error", - entry.ExplicitMaxTTL.Seconds(), sysView.MaxLeaseTTL().Seconds())) + int64(entry.ExplicitMaxTTL.Seconds()), int64(sysView.MaxLeaseTTL().Seconds()))) } } diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 69e5b7ca4512..7a84fe737f2c 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -701,7 +701,7 @@ func TestTokenStore_RevokeTree(t *testing.T) { } err := ts.RevokeTree("") - if err.Error() != "cannot revoke blank token" { + if err.Error() != "cannot tree-revoke blank token" { t.Fatalf("err: %v", err) } err = ts.RevokeTree(ent1.ID) @@ -1358,6 +1358,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { "creation_ttl": int64(0), "ttl": int64(0), "explicit_max_ttl": int64(0), + "expire_time": nil, } if resp.Data["creation_time"].(int64) == 0 { @@ -1403,6 +1404,14 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { t.Fatalf("creation time was zero") } delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatal("issue time is default time") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatal("expire time is default time") + } + delete(resp.Data, "expire_time") // Depending on timing of the test this may have ticked down, so accept 3599 if resp.Data["ttl"].(int64) == 3599 { @@ -1445,6 +1454,14 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { t.Fatalf("creation time was zero") } delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatal("issue time is default time") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatal("expire time is default time") + } + delete(resp.Data, "expire_time") // Depending on timing of the test this may have ticked down, so accept 3599 if resp.Data["ttl"].(int64) == 3599 { @@ -1486,9 +1503,11 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) { } func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { - _, ts, _, root := TestCoreWithTokenStore(t) + c, ts, _, root := TestCoreWithTokenStore(t) + testCoreMakeToken(t, c, root, "client", "3600s", []string{"foo"}) + req := logical.TestRequest(t, logical.ReadOperation, "lookup-self") - req.ClientToken = root + req.ClientToken = "client" resp, err := ts.HandleRequest(req) if err != nil { t.Fatalf("err: %v %v", err, resp) @@ -1498,16 +1517,17 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { } exp := map[string]interface{}{ - "id": root, + "id": "client", "accessor": resp.Data["accessor"], - "policies": []string{"root"}, - "path": "auth/token/root", + "policies": []string{"default", "foo"}, + "path": "auth/token/create", "meta": map[string]string(nil), - "display_name": "root", - "orphan": true, + "display_name": "token", + "orphan": false, + "renewable": true, "num_uses": 0, - "creation_ttl": int64(0), - "ttl": int64(0), + "creation_ttl": int64(3600), + "ttl": int64(3600), "explicit_max_ttl": int64(0), } @@ -1515,6 +1535,19 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) { t.Fatalf("creation time was zero") } delete(resp.Data, "creation_time") + if resp.Data["issue_time"].(time.Time).IsZero() { + t.Fatalf("creation time was zero") + } + delete(resp.Data, "issue_time") + if resp.Data["expire_time"].(time.Time).IsZero() { + t.Fatalf("expire time was zero") + } + delete(resp.Data, "expire_time") + + // Depending on timing of the test this may have ticked down, so accept 3599 + if resp.Data["ttl"].(int64) == 3599 { + resp.Data["ttl"] = int64(3600) + } if !reflect.DeepEqual(resp.Data, exp) { t.Fatalf("bad: expected:%#v\nactual:%#v", exp, resp.Data) diff --git a/vendor/github.com/ryanuber/go-glob/LICENSE b/vendor/github.com/ryanuber/go-glob/LICENSE new file mode 100644 index 000000000000..bdfbd9514976 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan Uber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ryanuber/go-glob/README.md b/vendor/github.com/ryanuber/go-glob/README.md new file mode 100644 index 000000000000..48f7fcb05a44 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/README.md @@ -0,0 +1,29 @@ +# String globbing in golang [![Build Status](https://travis-ci.org/ryanuber/go-glob.svg)](https://travis-ci.org/ryanuber/go-glob) + +`go-glob` is a single-function library implementing basic string glob support. + +Globs are an extremely user-friendly way of supporting string matching without +requiring knowledge of regular expressions or Go's particular regex engine. Most +people understand that if you put a `*` character somewhere in a string, it is +treated as a wildcard. Surprisingly, this functionality isn't found in Go's +standard library, except for `path.Match`, which is intended to be used while +comparing paths (not arbitrary strings), and contains specialized logic for this +use case. A better solution might be a POSIX basic (non-ERE) regular expression +engine for Go, which doesn't exist currently. + +Example +======= + +``` +package main + +import "github.com/ryanuber/go-glob" + +func main() { + glob.Glob("*World!", "Hello, World!") // true + glob.Glob("Hello,*", "Hello, World!") // true + glob.Glob("*ello,*", "Hello, World!") // true + glob.Glob("World!", "Hello, World!") // false + glob.Glob("/home/*", "/home/ryanuber/.bashrc") // true +} +``` diff --git a/vendor/github.com/ryanuber/go-glob/glob.go b/vendor/github.com/ryanuber/go-glob/glob.go new file mode 100644 index 000000000000..e67db3be183f --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/glob.go @@ -0,0 +1,56 @@ +package glob + +import "strings" + +// The character which is treated like a glob +const GLOB = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// subject string. The result is a simple true/false, determining whether or +// not the glob pattern matched the subject text. +func Glob(pattern, subj string) bool { + // Empty pattern can only match empty subject + if pattern == "" { + return subj == pattern + } + + // If the pattern _is_ a glob, it matches everything + if pattern == GLOB { + return true + } + + parts := strings.Split(pattern, GLOB) + + if len(parts) == 1 { + // No globs in pattern, so test for equality + return subj == pattern + } + + leadingGlob := strings.HasPrefix(pattern, GLOB) + trailingGlob := strings.HasSuffix(pattern, GLOB) + end := len(parts) - 1 + + // Go over the leading parts and ensure they match. + for i := 0; i < end; i++ { + idx := strings.Index(subj, parts[i]) + + switch i { + case 0: + // Check the first section. Requires special handling. + if !leadingGlob && idx != 0 { + return false + } + default: + // Check that the middle parts match. + if idx < 0 { + return false + } + } + + // Trim evaluated text from subj as we loop over the pattern. + subj = subj[idx+len(parts[i]):] + } + + // Reached the last section. Requires special handling. + return trailingGlob || strings.HasSuffix(subj, parts[end]) +} diff --git a/vendor/github.com/sstarcher/go-okta/api.go b/vendor/github.com/sstarcher/go-okta/api.go index 546bfb450d2f..1236f07445c0 100644 --- a/vendor/github.com/sstarcher/go-okta/api.go +++ b/vendor/github.com/sstarcher/go-okta/api.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net/http" ) @@ -84,7 +83,7 @@ func (c *Client) call(endpoint, method string, request, response interface{}) er var url = "https://" + c.org + "." + c.Url + "/api/v1/" + endpoint req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) if err != nil { - log.Fatal(err) + return err } req.Header.Add("Accept", `application/json`) @@ -95,19 +94,19 @@ func (c *Client) call(endpoint, method string, request, response interface{}) er resp, err := c.client.Do(req) if err != nil { - log.Fatal(err) + return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Fatal(err) + return err } if resp.StatusCode == http.StatusOK { err := json.Unmarshal(body, &response) if err != nil { - log.Fatal(err) + return err } } else { var errors ErrorResponse diff --git a/vendor/vendor.json b/vendor/vendor.json index e26053f4572f..d4908d1e3de7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1164,6 +1164,12 @@ "revision": "ddeb643de91b4ee0d9d87172c931a4ea3d81d49a", "revisionTime": "2017-02-08T17:17:27Z" }, + { + "checksumSHA1": "6JP37UqrI0H80Gpk0Y2P+KXgn5M=", + "path": "github.com/ryanuber/go-glob", + "revision": "256dc444b735e061061cf46c809487313d5b0065", + "revisionTime": "2017-01-28T01:21:29Z" + }, { "checksumSHA1": "5SYLEhADhdBVZAGPVHWggQl7H8k=", "path": "github.com/samuel/go-zookeeper/zk", @@ -1177,10 +1183,10 @@ "revisionTime": "2017-04-08T21:24:09Z" }, { - "checksumSHA1": "reJ+wO9qzH/7r2vXQE5MiTvg8+w=", + "checksumSHA1": "7b7psq20O8IOCr885W2Ld6a3KTc=", "path": "github.com/sstarcher/go-okta", - "revision": "388b6aef4eed400621bd3e3a98d831ef1368582d", - "revisionTime": "2016-10-03T17:19:47Z" + "revision": "64b3cb9e3a7b6d0c4e4432576c873e492d152666", + "revisionTime": "2017-04-28T20:44:25Z" }, { "checksumSHA1": "MxLnUmfrP+r5HfCZM29+WPKebn8=", diff --git a/version/version.go b/version/version.go index b7985ae0f572..0f8193335712 100644 --- a/version/version.go +++ b/version/version.go @@ -15,6 +15,7 @@ var ( Version = "unknown" VersionPrerelease = "unknown" + VersionMetadata = "" ) // VersionInfo @@ -22,11 +23,13 @@ type VersionInfo struct { Revision string Version string VersionPrerelease string + VersionMetadata string } func GetVersion() *VersionInfo { ver := Version rel := VersionPrerelease + md := VersionMetadata if GitDescribe != "" { ver = GitDescribe } @@ -38,6 +41,7 @@ func GetVersion() *VersionInfo { Revision: GitCommit, Version: ver, VersionPrerelease: rel, + VersionMetadata: md, } } @@ -52,6 +56,10 @@ func (c *VersionInfo) VersionNumber() string { version = fmt.Sprintf("%s-%s", version, c.VersionPrerelease) } + if c.VersionMetadata != "" { + version = fmt.Sprintf("%s+%s", version, c.VersionMetadata) + } + return version } @@ -66,6 +74,11 @@ func (c *VersionInfo) FullVersionNumber(rev bool) string { if c.VersionPrerelease != "" { fmt.Fprintf(&versionString, "-%s", c.VersionPrerelease) } + + if c.VersionMetadata != "" { + fmt.Fprintf(&versionString, "+%s", c.VersionMetadata) + } + if rev && c.Revision != "" { fmt.Fprintf(&versionString, " (%s)", c.Revision) } diff --git a/website/source/api/secret/aws/index.html.md b/website/source/api/secret/aws/index.html.md index ece445fcc080..25dc2682998c 100644 --- a/website/source/api/secret/aws/index.html.md +++ b/website/source/api/secret/aws/index.html.md @@ -336,7 +336,7 @@ role. ```json { - "ttl": "5m" + "ttl": "15m" } ``` diff --git a/website/source/api/secret/pki/index.html.md b/website/source/api/secret/pki/index.html.md index 8f95a4d5bdca..37eadb0b82cd 100644 --- a/website/source/api/secret/pki/index.html.md +++ b/website/source/api/secret/pki/index.html.md @@ -490,7 +490,7 @@ based on the role named in the endpoint. The issuing CA certificate is returned as well, so that only the root CA need be in a client's trust store. **The private key is _not_ stored. If you do not save the private key, you will -**need to request a new certificate.** +need to request a new certificate.** | Method | Path | Produces | | :------- | :--------------------------- | :--------------------- | @@ -613,12 +613,13 @@ $ curl \ ## Create/Update Role -This endpoint ceates or updates the role definition. Note that the -`allowed_domains`, `allow_subdomains`, and `allow_any_name` attributes are -additive; between them nearly and across multiple roles nearly any issuing -policy can be accommodated. `server_flag`, `client_flag`, and -`code_signing_flag` are additive as well. If a client requests a certificate -that is not allowed by the CN policy in the role, the request is denied. +This endpoint creates or updates the role definition. Note that the +`allowed_domains`, `allow_subdomains`, `allow_glob_domains`, and +`allow_any_name` attributes are additive; between them nearly and across +multiple roles nearly any issuing policy can be accommodated. `server_flag`, +`client_flag`, and `code_signing_flag` are additive as well. If a client +requests a certificate that is not allowed by the CN policy in the role, the +request is denied. | Method | Path | Produces | | :------- | :--------------------------- | :--------------------- | @@ -659,6 +660,11 @@ that is not allowed by the CN policy in the role, the request is denied. allow `foo.example.com` and `bar.example.com` as well as `*.example.com`. This is redundant when using the `allow_any_name` option. +- `allow_glob_domains` `(bool: false)` - Allows names specified in + `allowed_domains` to contain glob patterns (e.g. `ftp*.example.com`). Clients + will be allowed to request certificates with names matching the glob + patterns. + - `allow_any_name` `(bool: false)` – Specifies if clients can request any CN. Useful in some circumstances, but make sure you understand whether it is appropriate for your installation before enabling it. 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. diff --git a/website/source/api/system/audit.html.md b/website/source/api/system/audit.html.md index 794497c36979..060756bceb1b 100644 --- a/website/source/api/system/audit.html.md +++ b/website/source/api/system/audit.html.md @@ -71,6 +71,13 @@ single word name or a more complex, nested path. - `type` `(string: )` – Specifies the type of the audit backend. +Additionally, the following options are allowed in Vault open-source, but +relevant functionality is only supported in Vault Enterprise: + +- `local` `(bool: false)` – Specifies if the audit backend is a local mount + only. Local mounts are not replicated nor (if a secondary) removed by + replication. + ### Sample Payload ```json diff --git a/website/source/api/system/auth.html.md b/website/source/api/system/auth.html.md index e5c4d0cea906..eb9a61d79a8a 100644 --- a/website/source/api/system/auth.html.md +++ b/website/source/api/system/auth.html.md @@ -74,6 +74,13 @@ For example, mounting the "foo" auth backend will make it accessible at - `type` `(string: )` – Specifies the name of the authentication backend type, such as "github" or "token". +Additionally, the following options are allowed in Vault open-source, but +relevant functionality is only supported in Vault Enterprise: + +- `local` `(bool: false)` – Specifies if the auth backend is a local mount + only. Local mounts are not replicated nor (if a secondary) removed by + replication. + ### Sample Payload ```json diff --git a/website/source/api/system/leases.html.md b/website/source/api/system/leases.html.md new file mode 100644 index 000000000000..ec5166479a74 --- /dev/null +++ b/website/source/api/system/leases.html.md @@ -0,0 +1,222 @@ +--- +layout: "api" +page_title: "/sys/leases - HTTP API" +sidebar_current: "docs-http-system-leases" +description: |- + The `/sys/leases` endpoints are used to view and manage leases. +--- + +# `/sys/leases` + +The `/sys/leases` endpoints are used to view and manage leases in Vault. + +## Read Lease + +This endpoint retrieve lease metadata. + +| Method | Path | Produces | +| :------- | :---------------------------- | :--------------------- | +| `PUT` | `/sys/leases/lookup` | `200 application/json` | + +### Parameters + +- `lease_id` `(string: )` – Specifies the ID of the lease to lookup. + +### Sample Payload + +```json +{ + "lease_id": "aws/creds/deploy/abcd-1234..." +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/leases/lookup +``` + +### Sample Response + +```json +{ + "id": "auth/token/create/25c75065466dfc5f920525feafe47502c4c9915c", + "issue_time": "2017-04-30T10:18:11.228946471-04:00", + "expire_time": "2017-04-30T11:18:11.228946708-04:00", + "last_renewal_time": null, + "renewable": true, + "ttl": 3558 +} +``` + +## List Leases + +This endpoint returns a list of lease ids. + +**This endpoint requires 'sudo' capability.** + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `LIST` | `/sys/leases/lookup/:prefix` | `200 application/json` | + + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + https://vault.rocks/v1/sys/leases/lookup/aws/creds/deploy/ +``` + +### Sample Response + +```json +{ + "data":{ + "keys":[ + "abcd-1234...", + "efgh-1234...", + "ijkl-1234..." + ] + } +} +``` + +## Renew Lease + +This endpoint renews a lease, requesting to extend the lease. + +| Method | Path | Produces | +| :------- | :---------------------------- | :--------------------- | +| `PUT` | `/sys/leases/renew` | `200 application/json` | + +### Parameters + +- `lease_id` `(string: )` – Specifies the ID of the lease to extend. + This can be specified as part of the URL or as part of the request body. + +- `increment` `(int: 0)` – Specifies the requested amount of time (in seconds) + to extend the lease. + +### Sample Payload + +```json +{ + "lease_id": "aws/creds/deploy/abcd-1234...", + "increment": 1800 +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/leases/renew +``` + +### Sample Response + +```json +{ + "lease_id": "aws/creds/deploy/abcd-1234...", + "renewable": true, + "lease_duration": 2764790 +} +``` + +## Revoke Lease + +This endpoint revokes a lease immediately. + +| Method | Path | Produces | +| :------- | :---------------------------- | :--------------------- | +| `PUT` | `/sys/leases/revoke` | `204 (empty body)` | + +### Parameters + +- `lease_id` `(string: )` – Specifies the ID of the lease to revoke. + +### Sample Payload + +```json +{ + "lease_id": "postgresql/creds/readonly/abcd-1234..." +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + --data @payload.json \ + https://vault.rocks/v1/sys/leases/revoke +``` + +## Revoke Force + +This endpoint revokes all secrets or tokens generated under a given prefix +immediately. Unlike `/sys/leases/revoke-prefix`, this path ignores backend errors +encountered during revocation. This is _potentially very dangerous_ and should +only be used in specific emergency situations where errors in the backend or the +connected backend service prevent normal revocation. + +By ignoring these errors, Vault abdicates responsibility for ensuring that the +issued credentials or secrets are properly revoked and/or cleaned up. Access to +this endpoint should be tightly controlled. + +**This endpoint requires 'sudo' capability.** + +| Method | Path | Produces | +| :------- | :---------------------------------- | :--------------------- | +| `PUT` | `/sys/leases/revoke-force/:prefix` | `204 (empty body)` | + +### Parameters + +- `prefix` `(string: )` – Specifies the prefix to revoke. This is + specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + https://vault.rocks/v1/sys/leases/revoke-force/aws/creds +``` + +## Revoke Prefix + +This endpoint revokes all secrets (via a lease ID prefix) or tokens (via the +tokens' path property) generated under a given prefix immediately. This requires +`sudo` capability and access to it should be tightly controlled as it can be +used to revoke very large numbers of secrets/tokens at once. + +**This endpoint requires 'sudo' capability.** + +| Method | Path | Produces | +| :------- | :---------------------------------- | :--------------------- | +| `PUT` | `/sys/leases/revoke-prefix/:prefix` | `204 (empty body)` | + +### Parameters + +- `prefix` `(string: )` – Specifies the prefix to revoke. This is + specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request PUT \ + https://vault.rocks/v1/sys/leases/revoke-prefix/aws/creds +``` diff --git a/website/source/api/system/mounts.html.md b/website/source/api/system/mounts.html.md index 67713f6fb151..8b485f5ee996 100644 --- a/website/source/api/system/mounts.html.md +++ b/website/source/api/system/mounts.html.md @@ -84,6 +84,13 @@ This endpoint mounts a new secret backend at the given path. disabling backend caching respectively. If set on a specific mount, this overrides the global defaults. +Additionally, the following options are allowed in Vault open-source, but +relevant functionality is only supported in Vault Enterprise: + +- `local` `(bool: false)` – Specifies if the secret backend is a local mount + only. Local mounts are not replicated nor (if a secondary) removed by + replication. + ### Sample Payload ```json diff --git a/website/source/api/system/renew.html.md b/website/source/api/system/renew.html.md deleted file mode 100644 index a288916e6e66..000000000000 --- a/website/source/api/system/renew.html.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -layout: "api" -page_title: "/sys/renew - HTTP API" -sidebar_current: "docs-http-system-renew" -description: |- - The `/sys/renew` endpoint is used to renew secrets. ---- - -# `/sys/renew` - -The `/sys/renew` endpoint is used to renew secrets. - -## Renew Secret - -This endpoint renews a secret, requesting to extend the lease. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/renew/(:lease_id)` | `200 application/json` | - -### Parameters - -- `lease_id` `(string: )` – Specifies the ID of the lease to extend. - This can be specified as part of the URL or as part of the request body. - -- `increment` `(int: 0)` – Specifies the requested amount of time (in seconds) - to extend the lease. - -### Sample Payload - -```json -{ - "lease_id": "postgresql/creds/readonly/abcd-1234...", - "increment": 1800 -} -``` - -### Sample Request - -With the `lease_id` as part of the URL: - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - --data @payload.json \ - https://vault.rocks/v1/sys/renew/postgresql/creds/readonly/abcd-1234 -``` - -With the `lease_id` in the request body: - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - --data @payload.json \ - https://vault.rocks/v1/sys/renew -``` - -### Sample Response - -```json -{ - "lease_id": "aws/creds/deploy/e31b1145-ff27-e62c-cba2-934e9f0d1dbc", - "renewable": true, - "lease_duration": 2764790 -} -``` diff --git a/website/source/api/system/revoke-force.html.md b/website/source/api/system/revoke-force.html.md deleted file mode 100644 index 5ae915127224..000000000000 --- a/website/source/api/system/revoke-force.html.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -layout: "api" -page_title: "/sys/revoke-force - HTTP API" -sidebar_current: "docs-http-system-revoke-force" -description: |- - The `/sys/revoke-force` endpoint is used to revoke secrets or tokens based on - prefix while ignoring backend errors. ---- - -# `/sys/revoke-force` - -The `/sys/revoke-force` endpoint is used to revoke secrets or tokens based on -prefix while ignoring backend errors. - -## Revoke Force - -This endpoint revokes all secrets or tokens generated under a given prefix -immediately. Unlike `/sys/revoke-prefix`, this path ignores backend errors -encountered during revocation. This is _potentially very dangerous_ and should -only be used in specific emergency situations where errors in the backend or the -connected backend service prevent normal revocation. - -By ignoring these errors, Vault abdicates responsibility for ensuring that the -issued credentials or secrets are properly revoked and/or cleaned up. Access to -this endpoint should be tightly controlled. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/revoke-force/:prefix` | `204 (empty body)` | - -### Parameters - -- `prefix` `(string: )` – Specifies the prefix to revoke. This is - specified as part of the URL. - -### Sample Request - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - https://vault.rocks/v1/sys/revoke-force/aws/creds -``` diff --git a/website/source/api/system/revoke-prefix.html.md b/website/source/api/system/revoke-prefix.html.md deleted file mode 100644 index 8ebc81c3e275..000000000000 --- a/website/source/api/system/revoke-prefix.html.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -layout: "api" -page_title: "/sys/revoke-prefix - HTTP API" -sidebar_current: "docs-http-system-revoke-prefix" -description: |- - The `/sys/revoke-prefix` endpoint is used to revoke secrets or tokens based on - prefix. ---- - -# `/sys/revoke-prefix` - -The `/sys/revoke-prefix` endpoint is used to revoke secrets or tokens based on -prefix. - -## Revoke Prefix - -This endpoint revokes all secrets (via a lease ID prefix) or tokens (via the -tokens' path property) generated under a given prefix immediately. This requires -`sudo` capability and access to it should be tightly controlled as it can be -used to revoke very large numbers of secrets/tokens at once. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/revoke-prefix/:prefix` | `204 (empty body)` | - -### Parameters - -- `prefix` `(string: )` – Specifies the prefix to revoke. This is - specified as part of the URL. - -### Sample Request - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - https://vault.rocks/v1/sys/revoke-prefix/aws/creds -``` diff --git a/website/source/api/system/revoke.html.md b/website/source/api/system/revoke.html.md deleted file mode 100644 index 9bcbb9a34680..000000000000 --- a/website/source/api/system/revoke.html.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -layout: "api" -page_title: "/sys/revoke - HTTP API" -sidebar_current: "docs-http-system-revoke/" -description: |- - The `/sys/revoke` endpoint is used to revoke secrets. ---- - -# `/sys/revoke` - -The `/sys/revoke` endpoint is used to revoke secrets. - -## Revoke Secret - -This endpoint revokes a secret immediately. - -| Method | Path | Produces | -| :------- | :--------------------------- | :--------------------- | -| `PUT` | `/sys/revoke/:lease_id` | `204 (empty body)` | - -### Parameters - -- `lease_id` `(string: )` – Specifies the ID of the lease to revoke. - -### Sample Request - -``` -$ curl \ - --header "X-Vault-Token: ..." \ - --request PUT \ - https://vault.rocks/v1/sys/revoke/aws/creds/readonly-acbd1234 -``` diff --git a/website/source/api/system/wrapping-wrap.html.md b/website/source/api/system/wrapping-wrap.html.md index c10cdbbaf062..c3c4065d588c 100644 --- a/website/source/api/system/wrapping-wrap.html.md +++ b/website/source/api/system/wrapping-wrap.html.md @@ -41,6 +41,7 @@ token. ``` $ curl \ --header "X-Vault-Token: ..." \ + --header "X-Vault-Wrap-TTL: 60" \ --request POST \ --data @payload.json \ https://vault.rocks/v1/sys/wrapping/wrap 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/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/docs/auth/cert.html.md b/website/source/docs/auth/cert.html.md index a9e50c6c030d..4f094053e29d 100644 --- a/website/source/docs/auth/cert.html.md +++ b/website/source/docs/auth/cert.html.md @@ -60,18 +60,25 @@ it is up to the administrator to remove it from the backend. ## Authentication ### Via the CLI +The below requires Vault to present a certificate signed by `ca.pem` and +presents `cert.pem` (using `key.pem`) to authenticate against the `web` cert +role. If a certificate role name is not specified, the auth backend will try to +authenticate against all trusted certificates. + ``` $ vault auth -method=cert \ - -ca-cert=ca.pem -client-cert=cert.pem -client-key=key.pem + -ca-cert=ca.pem -client-cert=cert.pem -client-key=key.pem \ + name=web ``` ### Via the API The endpoint for the login is `/login`. The client simply connects with their TLS certificate and when the login endpoint is hit, the auth backend will determine -if there is a matching trusted certificate to authenticate the client. +if there is a matching trusted certificate to authenticate the client. Optionally, +you may specify a single certificate role to authenticate against. ``` -$ curl --cacert ca.pem --cert cert.pem --key key.pem \ +$ curl --cacert ca.pem --cert cert.pem --key key.pem -d name=web \ $VAULT_ADDR/v1/auth/cert/login -XPOST ``` @@ -175,6 +182,7 @@ of the header should be "X-Vault-Token" and the value should be the token. "certificate": "-----BEGIN CERTIFICATE-----\nMIIEtzCCA5+.......ZRtAfQ6r\nwlW975rYa1ZqEdA=\n-----END CERTIFICATE-----", "display_name": "test", "policies": "", + "allowed_names": "", "ttl": 2764800 }, "warnings": null, @@ -245,6 +253,15 @@ of the header should be "X-Vault-Token" and the value should be the token. required The PEM-format CA certificate. +
  • + allowed_names + optional + Constrain the Common and Alternative Names in the client certificate + with a [globbed pattern](https://github.com/ryanuber/go-glob/blob/master/README.md#example). + Value is a comma-separated list of patterns. + Authentication requires at least one Name matching at least one pattern. + If not set, defaults to allowing all names. +
  • policies optional @@ -382,8 +399,8 @@ of the header should be "X-Vault-Token" and the value should be the token.
    Description
    - Log in and fetch a token. If there is a valid chain to a CA configured in the backend, - a token will be issued. + Log in and fetch a token. If there is a valid chain to a CA configured in + the backend and all role constraints are matched, a token will be issued.
    Method
    @@ -394,7 +411,15 @@ of the header should be "X-Vault-Token" and the value should be the token.
    Parameters
    - None. +
      +
    • + name + optional + Authenticate against only the named certificate role, returning its + policy list if successful. If not set, defaults to trying all + certificate roles and returning any one that matches. +
    • +
    Returns
    diff --git a/website/source/docs/configuration/index.html.md b/website/source/docs/configuration/index.html.md index a18474b73e6a..7c61eb75f626 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. @@ -97,9 +97,10 @@ to specify where the configuration is. duration for tokens and secrets. This is specified using a label suffix like `"30s"` or `"1h"`. -- `ui` `(bool: false, Enterprise-only)` – Enables the built-in web UI. Once - enabled, the UI will be available to browsers at the standard Vault address. - This can also be provided via the environment variable `VAULT_UI`. +- `ui` `(bool: false, Enterprise-only)` – Enables the built-in web UI, which is + available on all listeners (address + port) at the `/ui` path. Browsers accessing + the standard Vault API address will automatically redirect there. This can also + be provided via the environment variable `VAULT_UI`. [storage-backend]: /docs/configuration/storage/index.html [listener]: /docs/configuration/listener/index.html diff --git a/website/source/docs/configuration/listener/tcp.html.md b/website/source/docs/configuration/listener/tcp.html.md index 5286e3a22cb5..bab3a45a8055 100644 --- a/website/source/docs/configuration/listener/tcp.html.md +++ b/website/source/docs/configuration/listener/tcp.html.md @@ -29,7 +29,7 @@ listener "tcp" { they need to hop through a TCP load balancer or some other scheme in order to talk. -- `tls_disable` `(bool: false)` – Specifies if TLS will be disabled. Vault +- `tls_disable` `(string: "false")` – Specifies if TLS will be disabled. Vault assumes TLS by default, so you must explicitly disable TLS to opt-in to insecure communication. @@ -51,10 +51,10 @@ listener "tcp" { ciphersuites as a comma-separated-list. The list of all available ciphersuites is available in the [Golang TLS documentation][golang-tls]. -- `tls_prefer_server_cipher_suites` `(bool: false)` – Specifies to prefer the +- `tls_prefer_server_cipher_suites` `(string: "false")` – Specifies to prefer the server's ciphersuite over the client ciphersuites. -- `tls_require_and_verify_client_cert` `(bool: false)` – Turns on client +- `tls_require_and_verify_client_cert` `(string: "false")` – Turns on client authentication for this listener; the listener will require a presented client cert that successfully validates against system CAs. diff --git a/website/source/docs/configuration/storage/consul.html.md b/website/source/docs/configuration/storage/consul.html.md index 49c73afcb6ec..821ea5e7cb33 100644 --- a/website/source/docs/configuration/storage/consul.html.md +++ b/website/source/docs/configuration/storage/consul.html.md @@ -193,6 +193,6 @@ storage "consul" { ``` [consul]: https://www.consul.io/ "Consul by HashiCorp" -[consul-acl]: https://www.consul.io/docs/internals/acl.html "Consul ACLs" -[consul-consistency]: https://www.consul.io/docs/agent/http.html#consistency-modes "Consul Consistency Modes" +[consul-acl]: https://www.consul.io/docs/guides/acl.html "Consul ACLs" +[consul-consistency]: https://www.consul.io/api/index.html#consistency-modes "Consul Consistency Modes" [consul-encryption]: https://www.consul.io/docs/agent/encryption.html "Consul Encryption" diff --git a/website/source/docs/vault-enterprise/ui/index.html.md b/website/source/docs/vault-enterprise/ui/index.html.md new file mode 100644 index 000000000000..a628b8ce51e6 --- /dev/null +++ b/website/source/docs/vault-enterprise/ui/index.html.md @@ -0,0 +1,78 @@ +--- +layout: "docs" +page_title: "Vault Enterprise UI" +sidebar_current: "docs-vault-enterprise-ui" +description: |- + Vault Enterprise features a user interface for interacting with Vault. Easily + create, read, update, and delete secrets, authenticate, unseal, and more with + the Vault Enterprise UI. +--- + +# Vault UI + +Vault Enterprise features a user interface for interacting with Vault. Easily +create, read, update, and delete secrets, authenticate, unseal, and more with +the Vault Enterprise UI. + +To use the UI, you must have an active or trial license for Vault Enterprise or +Vault Pro. To start a trial, contact [HashiCorp sales](mailto:sales@hashicorp.com). + +## Activating the Vault UI + +The Vault Enterprise UI is not activated by default. To activate the UI, set the +`ui` configuration option in the Vault server configuration. Vault clients do +not need to set this option, since they will not be serving the UI. + +```hcl +ui = true + +listener "tcp" { + address = "10.0.1.35:8200" +} + +storage "consul" { + # ... +} +``` + +For more information, please see the +[Vault configuration options](/docs/configuration/index.html). + +## Accessing the Vault UI + +The UI runs on the same port as the Vault listener. As such, you must configure +at least one `listener` stanza in order to access the UI. + +```hcl +listener "tcp" { + address = "10.0.1.35:8200" + + # If bound to localhost, the Vault UI is only + # accessible from the local machine! + # address = "127.0.0.1:8200" +} +``` + +In this case, the UI is accessible the following URL from any machine on the +subnet (provided no network firewalls are in place): + +```text +https://10.0.1.35:8200/ui +``` + +It is also accessible at any DNS entry that resolves to that IP address, such as +the Consul service address (if using Consul): + +```text +https://vault.service.consul:8200/ui +``` + +### Note on TLS + +When using TLS (recommended), the certificate must be valid for all DNS entries +you will be accessing the Vault UI on, and any IP addresses on the SAN. If you +are running Vault with a self-signed certificate, any browsers that access the +Vault UI will need to have the root CA installed. Failure to do so may result in +the browser displaying a warning that the site is "untrusted". It is highly +recommended that client browsers accessing the Vault UI install the proper CA +root for validation to reduce the chance of a MITM attack. diff --git a/website/source/layouts/api.erb b/website/source/layouts/api.erb index c6e92d026eb3..43eefa464cd0 100644 --- a/website/source/layouts/api.erb +++ b/website/source/layouts/api.erb @@ -117,6 +117,9 @@ > /sys/leader
  • + > + /sys/leases + > /sys/mounts @@ -135,21 +138,9 @@ > /sys/remount - > - /sys/renew - > /sys/replication - > - /sys/revoke - - > - /sys/revoke-force - - > - /sys/revoke-prefix - > /sys/rotate diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 95787e8e199e..70a7bc440734 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -293,8 +293,8 @@ AppRole - > - AWS EC2 + > + AWS > @@ -381,6 +381,9 @@ + > + UI (Web Interface) + 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 %>
    -