diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go new file mode 100644 index 0000000000..e0f56d3e8d --- /dev/null +++ b/builtin/credential/aws/backend.go @@ -0,0 +1,177 @@ +package aws + +import ( + "sync" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/vault/helper/salt" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func Factory(conf *logical.BackendConfig) (logical.Backend, error) { + b, err := Backend(conf) + if err != nil { + return nil, err + } + return b.Setup(conf) +} + +type backend struct { + *framework.Backend + Salt *salt.Salt + + // Lock to make changes to any of the backend's configuration endpoints. + configMutex sync.RWMutex + + // Lock to make changes to role entries + roleMutex sync.RWMutex + + // Lock to make changes to the blacklist entries + blacklistMutex sync.RWMutex + + // Guards the blacklist/whitelist tidy functions + tidyBlacklistCASGuard uint32 + tidyWhitelistCASGuard uint32 + + // Duration after which the periodic function of the backend needs to + // tidy the blacklist and whitelist entries. + tidyCooldownPeriod time.Duration + + // nextTidyTime holds the time at which the periodic func should initiatite + // the tidy operations. This is set by the periodicFunc based on the value + // of tidyCooldownPeriod. + nextTidyTime time.Time + + // Map to hold the EC2 client objects indexed by region. This avoids the + // overhead of creating a client object for every login request. When + // the credentials are modified or deleted, all the cached client objects + // will be flushed. + EC2ClientsMap map[string]*ec2.EC2 +} + +func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { + salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ + HashFunc: salt.SHA256Hash, + }) + if err != nil { + return nil, err + } + + b := &backend{ + // Setting the periodic func to be run once in an hour. + // If there is a real need, this can be made configurable. + tidyCooldownPeriod: time.Hour, + Salt: salt, + EC2ClientsMap: make(map[string]*ec2.EC2), + } + + b.Backend = &framework.Backend{ + PeriodicFunc: b.periodicFunc, + AuthRenew: b.pathLoginRenew, + Help: backendHelp, + PathsSpecial: &logical.Paths{ + Unauthenticated: []string{ + "login", + }, + }, + Paths: []*framework.Path{ + pathLogin(b), + pathListRole(b), + pathListRoles(b), + pathRole(b), + pathRoleTag(b), + pathConfigClient(b), + pathConfigCertificate(b), + pathConfigTidyRoletagBlacklist(b), + pathConfigTidyIdentityWhitelist(b), + pathListCertificates(b), + pathListRoletagBlacklist(b), + pathRoletagBlacklist(b), + pathTidyRoletagBlacklist(b), + pathListIdentityWhitelist(b), + pathIdentityWhitelist(b), + pathTidyIdentityWhitelist(b), + }, + } + + return b.Backend, nil +} + +// periodicFunc performs the tasks that the backend wishes to do periodically. +// Currently this will be triggered once in a minute by the RollbackManager. +// +// The tasks being done currently by this function are to cleanup the expired +// entries of both blacklist role tags and whitelist identities. Tidying is done +// not once in a minute, but once in an hour, controlled by 'tidyCooldownPeriod'. +// Tidying of blacklist and whitelist are by default enabled. This can be +// changed using `config/tidy/roletags` and `config/tidy/identities` endpoints. +func (b *backend) periodicFunc(req *logical.Request) error { + + // Run the tidy operations for the first time. Then run it when current + // time matches the nextTidyTime. + if b.nextTidyTime.IsZero() || !time.Now().UTC().Before(b.nextTidyTime) { + // safety_buffer defaults to 180 days for roletag blacklist + safety_buffer := 15552000 + tidyBlacklistConfigEntry, err := b.configTidyRoleTags(req.Storage) + if err != nil { + return err + } + skipBlacklistTidy := false + // check if tidying of role tags was configured + if tidyBlacklistConfigEntry != nil { + // check if periodic tidying of role tags was disabled + if tidyBlacklistConfigEntry.DisablePeriodicTidy { + skipBlacklistTidy = true + } + // overwrite the default safety_buffer with the configured value + safety_buffer = tidyBlacklistConfigEntry.SafetyBuffer + } + // tidy role tags if explicitly not disabled + if !skipBlacklistTidy { + b.tidyBlacklistRoleTag(req.Storage, safety_buffer) + } + + // reset the safety_buffer to 72h + safety_buffer = 259200 + tidyWhitelistConfigEntry, err := b.configTidyIdentities(req.Storage) + if err != nil { + return err + } + skipWhitelistTidy := false + // check if tidying of identities was configured + if tidyWhitelistConfigEntry != nil { + // check if periodic tidying of identities was disabled + if tidyWhitelistConfigEntry.DisablePeriodicTidy { + skipWhitelistTidy = true + } + // overwrite the default safety_buffer with the configured value + safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer + } + // tidy identities if explicitly not disabled + if !skipWhitelistTidy { + b.tidyWhitelistIdentity(req.Storage, safety_buffer) + } + + // Update the time at which to run the tidy functions again. + b.nextTidyTime = time.Now().UTC().Add(b.tidyCooldownPeriod) + } + return nil +} + +const backendHelp = ` +AWS auth backend takes in PKCS#7 signature of an AWS EC2 instance and a client +created nonce to authenticates the EC2 instance with Vault. + +Authentication is backed by a preconfigured role in the backend. The role +represents the authorization of resources by containing Vault's policies. +Role can be created using 'role/' endpoint. + +If there is need to further restrict the capabilities of the role on the instance +that is using the role, 'role_tag' option can be enabled on the role, and a tag +can be generated using 'role//tag' endpoint. This tag represents the +subset of capabilities set on the role. When the 'role_tag' option is enabled on +the role, the login operation requires that a respective role tag is attached to +the EC2 instance which performs the login. +` diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go new file mode 100644 index 0000000000..06a27a3084 --- /dev/null +++ b/builtin/credential/aws/backend_test.go @@ -0,0 +1,1273 @@ +package aws + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/helper/salt" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + logicaltest "github.com/hashicorp/vault/logical/testing" +) + +func createBackend(conf *logical.BackendConfig) (*backend, error) { + salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ + HashFunc: salt.SHA256Hash, + }) + if err != nil { + return nil, err + } + + b := &backend{ + // Setting the periodic func to be run once in an hour. + // If there is a real need, this can be made configurable. + tidyCooldownPeriod: time.Hour, + Salt: salt, + EC2ClientsMap: make(map[string]*ec2.EC2), + } + + b.Backend = &framework.Backend{ + PeriodicFunc: b.periodicFunc, + AuthRenew: b.pathLoginRenew, + Help: backendHelp, + PathsSpecial: &logical.Paths{ + Unauthenticated: []string{ + "login", + }, + }, + Paths: []*framework.Path{ + pathLogin(b), + pathListRole(b), + pathListRoles(b), + pathRole(b), + pathRoleTag(b), + pathConfigClient(b), + pathConfigCertificate(b), + pathConfigTidyRoletagBlacklist(b), + pathConfigTidyIdentityWhitelist(b), + pathListCertificates(b), + pathListRoletagBlacklist(b), + pathRoletagBlacklist(b), + pathTidyRoletagBlacklist(b), + pathListIdentityWhitelist(b), + pathIdentityWhitelist(b), + pathTidyIdentityWhitelist(b), + }, + } + + return b, nil +} + +func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { + // create a backend + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // create a role entry + data := map[string]interface{}{ + "policies": "p,q,r,s", + "bound_ami_id": "abcd-123", + } + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/abcd-123", + Storage: storage, + Data: data, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + // read the created role entry + roleEntry, err := b.awsRole(storage, "abcd-123") + if err != nil { + t.Fatal(err) + } + + // create a nonce for the role tag + nonce, err := createRoleTagNonce() + if err != nil { + t.Fatal(err) + } + rTag1 := &roleTag{ + Version: "v1", + Role: "abcd-123", + Nonce: nonce, + Policies: []string{"p", "q", "r"}, + MaxTTL: 200000000000, // 200s + } + + // create a role tag against the role entry + val, err := createRoleTagValue(rTag1, roleEntry) + if err != nil { + t.Fatal(err) + } + if val == "" { + t.Fatalf("failed to create role tag") + } + + // parse the created role tag + rTag2, err := b.parseAndVerifyRoleTagValue(storage, val) + if err != nil { + t.Fatal(err) + } + + // check the values in parsed role tag + if rTag2.Version != "v1" || + rTag2.Nonce != nonce || + rTag2.Role != "abcd-123" || + rTag2.MaxTTL != 200000000000 || // 200s + !policyutil.EquivalentPolicies(rTag2.Policies, []string{"p", "q", "r"}) || + len(rTag2.HMAC) == 0 { + t.Fatalf("parsed role tag is invalid") + } + + // verify the tag contents using role specific HMAC key + verified, err := verifyRoleTagValue(rTag2, roleEntry) + if err != nil { + t.Fatal(err) + } + if !verified { + t.Fatalf("failed to verify the role tag") + } + + // register a different role + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/ami-6789", + Storage: storage, + Data: data, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + // get the entry of the newly created role entry + roleEntry2, err := b.awsRole(storage, "ami-6789") + if err != nil { + t.Fatal(err) + } + + // try to verify the tag created with previous role's HMAC key + // with the newly registered entry's HMAC key + verified, err = verifyRoleTagValue(rTag2, roleEntry2) + if err != nil { + t.Fatal(err) + } + if verified { + t.Fatalf("verification of role tag should have failed") + } + + // modify any value in role tag and try to verify it + rTag2.Version = "v2" + verified, err = verifyRoleTagValue(rTag2, roleEntry) + if err != nil { + t.Fatal(err) + } + if verified { + t.Fatalf("verification of role tag should have failed: invalid Version") + } +} + +func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { + // create a nonce for the role tag + nonce, err := createRoleTagNonce() + if err != nil { + t.Fatal(err) + } + rTag := &roleTag{ + Version: "v1", + Nonce: nonce, + Role: "abcd-123", + } + + rTag.Version = "" + // try to create plaintext part of role tag + // without specifying version + val, err := prepareRoleTagPlaintextValue(rTag) + if err == nil { + t.Fatalf("expected error for missing version") + } + rTag.Version = "v1" + + rTag.Nonce = "" + // try to create plaintext part of role tag + // without specifying nonce + val, err = prepareRoleTagPlaintextValue(rTag) + if err == nil { + t.Fatalf("expected error for missing nonce") + } + rTag.Nonce = nonce + + rTag.Role = "" + // try to create plaintext part of role tag + // without specifying role + val, err = prepareRoleTagPlaintextValue(rTag) + if err == nil { + t.Fatalf("expected error for missing role") + } + rTag.Role = "abcd-123" + + // create the plaintext part of the tag + val, err = prepareRoleTagPlaintextValue(rTag) + if err != nil { + t.Fatal(err) + } + + // verify if it contains known fields + if !strings.Contains(val, "r=") || + !strings.Contains(val, "d=") || + !strings.Contains(val, "m=") || + !strings.HasPrefix(val, "v1") { + t.Fatalf("incorrect information in role tag plaintext value") + } + + rTag.InstanceID = "instance-123" + // create the role tag with instance_id specified + val, err = prepareRoleTagPlaintextValue(rTag) + if err != nil { + t.Fatal(err) + } + // verify it + if !strings.Contains(val, "i=") { + t.Fatalf("missing instance ID in role tag plaintext value") + } + + rTag.MaxTTL = 200000000000 + // create the role tag with max_ttl specified + val, err = prepareRoleTagPlaintextValue(rTag) + if err != nil { + t.Fatal(err) + } + // verify it + if !strings.Contains(val, "t=") { + t.Fatalf("missing max_ttl field in role tag plaintext value") + } +} + +func TestBackend_CreateRoleTagNonce(t *testing.T) { + // create a nonce for the role tag + nonce, err := createRoleTagNonce() + if err != nil { + t.Fatal(err) + } + if nonce == "" { + t.Fatalf("failed to create role tag nonce") + } + + // verify that the value returned is base64 encoded + nonceBytes, err := base64.StdEncoding.DecodeString(nonce) + if err != nil { + t.Fatal(err) + } + if len(nonceBytes) == 0 { + t.Fatalf("length of role tag nonce is zero") + } +} + +func TestBackend_ConfigTidyIdentities(t *testing.T) { + // create a backend + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // test update operation + tidyRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/tidy/identity-whitelist", + Storage: storage, + } + data := map[string]interface{}{ + "safety_buffer": "60", + "disable_periodic_tidy": true, + } + tidyRequest.Data = data + _, err = b.HandleRequest(tidyRequest) + if err != nil { + t.Fatal(err) + } + + // test read operation + tidyRequest.Operation = logical.ReadOperation + resp, err := b.HandleRequest(tidyRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatalf("failed to read config/tidy/identity-whitelist endpoint") + } + if resp.Data["safety_buffer"].(int) != 60 || !resp.Data["disable_periodic_tidy"].(bool) { + t.Fatalf("bad: expected: safety_buffer:60 disable_periodic_tidy:true actual: safety_buffer:%s disable_periodic_tidy:%t\n", resp.Data["safety_buffer"].(int), resp.Data["disable_periodic_tidy"].(bool)) + } + + // test delete operation + tidyRequest.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(tidyRequest) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatalf("failed to delete config/tidy/identity-whitelist") + } +} + +func TestBackend_ConfigTidyRoleTags(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // test update operation + tidyRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/tidy/roletag-blacklist", + Storage: storage, + } + data := map[string]interface{}{ + "safety_buffer": "60", + "disable_periodic_tidy": true, + } + tidyRequest.Data = data + _, err = b.HandleRequest(tidyRequest) + if err != nil { + t.Fatal(err) + } + + // test read operation + tidyRequest.Operation = logical.ReadOperation + resp, err := b.HandleRequest(tidyRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatalf("failed to read config/tidy/roletag-blacklist endpoint") + } + if resp.Data["safety_buffer"].(int) != 60 || !resp.Data["disable_periodic_tidy"].(bool) { + t.Fatalf("bad: expected: safety_buffer:60 disable_periodic_tidy:true actual: safety_buffer:%s disable_periodic_tidy:%t\n", resp.Data["safety_buffer"].(int), resp.Data["disable_periodic_tidy"].(bool)) + } + + // test delete operation + tidyRequest.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(tidyRequest) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatalf("failed to delete config/tidy/roletag-blacklist") + } +} + +func TestBackend_TidyIdentities(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // test update operation + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "tidy/identity-whitelist", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_TidyRoleTags(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // test update operation + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "tidy/roletag-blacklist", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_ConfigClient(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{"access_key": "AKIAJBRHKV6EVTTNXDHA", + "secret_key": "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", + } + + stepCreate := logicaltest.TestStep{ + Operation: logical.CreateOperation, + Path: "config/client", + Data: data, + } + + stepUpdate := logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data, + } + + data3 := map[string]interface{}{"access_key": "", + "secret_key": "mCtSM8ZUEQ3mOFVZYPBQkf2sO6F/W7a5TVzrl3Oj", + } + stepInvalidAccessKey := logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data3, + ErrorOk: true, + } + + data4 := map[string]interface{}{"access_key": "accesskey", + "secret_key": "", + } + stepInvalidSecretKey := logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data4, + ErrorOk: true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + AcceptanceTest: false, + Backend: b, + Steps: []logicaltest.TestStep{ + stepCreate, + stepInvalidAccessKey, + stepInvalidSecretKey, + stepUpdate, + }, + }) + + // test existence check returning false + checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{ + Operation: logical.CreateOperation, + Path: "config/client", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'config/client'") + } + if exists { + t.Fatal("existence check should have returned 'false' for 'config/client'") + } + + // create an entry + configClientCreateRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data, + Storage: storage, + } + _, err = b.HandleRequest(configClientCreateRequest) + if err != nil { + t.Fatal(err) + } + + //test existence check returning true + checkFound, exists, err = b.HandleExistenceCheck(&logical.Request{ + Operation: logical.CreateOperation, + Path: "config/client", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'config/client'") + } + if !exists { + t.Fatal("existence check should have returned 'true' for 'config/client'") + } + + endpointData := map[string]interface{}{ + "secret_key": "secretkey", + "access_key": "accesskey", + "endpoint": "endpointvalue", + } + + endpointReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/client", + Storage: storage, + Data: endpointData, + } + _, err = b.HandleRequest(endpointReq) + if err != nil { + t.Fatal(err) + } + + endpointReq.Operation = logical.ReadOperation + resp, err := b.HandleRequest(endpointReq) + if err != nil { + t.Fatal(err) + } + if resp == nil || + resp.IsError() { + t.Fatalf("") + } + actual := resp.Data["endpoint"].(string) + if actual != "endpointvalue" { + t.Fatalf("bad: endpoint: expected:endpointvalue actual:%s\n", actual) + } +} + +func TestBackend_pathConfigCertificate(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + certReq := &logical.Request{ + Operation: logical.CreateOperation, + Storage: storage, + Path: "config/certificate/cert1", + } + checkFound, exists, err := b.HandleExistenceCheck(certReq) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'config/certificate/cert1'") + } + if exists { + t.Fatal("existence check should have returned 'false' for 'config/certificate/cert1'") + } + + data := map[string]interface{}{ + "aws_public_cert": `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM3VENDQXEwQ0NRQ1d1a2paNVY0YVp6QUpC +Z2NxaGtqT09BUURNRnd4Q3pBSkJnTlZCQVlUQWxWVE1Sa3cKRndZRFZRUUlFeEJYWVhOb2FXNW5k +Rzl1SUZOMFlYUmxNUkF3RGdZRFZRUUhFd2RUWldGMGRHeGxNU0F3SGdZRApWUVFLRXhkQmJXRjZi +MjRnVjJWaUlGTmxjblpwWTJWeklFeE1RekFlRncweE1qQXhNRFV4TWpVMk1USmFGdzB6Ck9EQXhN +RFV4TWpVMk1USmFNRnd4Q3pBSkJnTlZCQVlUQWxWVE1Sa3dGd1lEVlFRSUV4QlhZWE5vYVc1bmRH +OXUKSUZOMFlYUmxNUkF3RGdZRFZRUUhFd2RUWldGMGRHeGxNU0F3SGdZRFZRUUtFeGRCYldGNmIy +NGdWMlZpSUZObApjblpwWTJWeklFeE1RekNDQWJjd2dnRXNCZ2NxaGtqT09BUUJNSUlCSHdLQmdR +Q2prdmNTMmJiMVZRNHl0LzVlCmloNU9PNmtLL24xTHpsbHI3RDhad3RRUDhmT0VwcDVFMm5nK0Q2 +VWQxWjFnWWlwcjU4S2ozbnNzU05wSTZiWDMKVnlJUXpLN3dMY2xuZC9Zb3pxTk5tZ0l5WmVjTjdF +Z2xLOUlUSEpMUCt4OEZ0VXB0M1FieVlYSmRtVk1lZ042UApodmlZdDVKSC9uWWw0aGgzUGExSEpk +c2tnUUlWQUxWSjNFUjExK0tvNHRQNm53dkh3aDYrRVJZUkFvR0JBSTFqCmsrdGtxTVZIdUFGY3ZB +R0tvY1Rnc2pKZW02LzVxb216SnVLRG1iSk51OVF4dzNyQW90WGF1OFFlK01CY0psL1UKaGh5MUtI +VnBDR2w5ZnVlUTJzNklMMENhTy9idXljVTFDaVlRazQwS05IQ2NIZk5pWmJkbHgxRTlycFVwN2Ju +RgpsUmEydjFudE1YM2NhUlZEZGJ0UEVXbWR4U0NZc1lGRGs0bVpyT0xCQTRHRUFBS0JnRWJtZXZl +NWY4TElFL0dmCk1ObVA5Q001ZW92UU9HeDVobzhXcUQrYVRlYnMrazJ0bjkyQkJQcWVacXBXUmE1 +UC8ranJkS21sMXF4NGxsSFcKTVhyczNJZ0liNitoVUlCK1M4ZHo4L21tTzBicHI3NlJvWlZDWFlh +YjJDWmVkRnV0N3FjM1dVSDkrRVVBSDVtdwp2U2VEQ09VTVlRUjdSOUxJTll3b3VISXppcVFZTUFr +R0J5cUdTTTQ0QkFNREx3QXdMQUlVV1hCbGs0MHhUd1N3CjdIWDMyTXhYWXJ1c2U5QUNGQk5HbWRY +MlpCclZOR3JOOU4yZjZST2swazlLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K +`, + } + + certReq.Data = data + // test create operation + _, err = b.HandleRequest(certReq) + if err != nil { + t.Fatal(err) + } + + certReq.Data = nil + // test existence check + checkFound, exists, err = b.HandleExistenceCheck(certReq) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'config/certificate/cert1'") + } + if !exists { + t.Fatal("existence check should have returned 'true' for 'config/certificate/cert1'") + } + + certReq.Operation = logical.ReadOperation + // test read operation + resp, err := b.HandleRequest(certReq) + expectedCert := `-----BEGIN CERTIFICATE----- +MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw +FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD +VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z +ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u +IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl +cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e +ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 +VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P +hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j +k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U +hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF +lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf +MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW +MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw +vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw +7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K +-----END CERTIFICATE----- +` + if resp.Data["aws_public_cert"].(string) != expectedCert { + t.Fatal("bad: expected:%s\n got:%s\n", expectedCert, resp.Data["aws_public_cert"].(string)) + } + + certReq.Operation = logical.CreateOperation + certReq.Path = "config/certificate/cert2" + certReq.Data = data + // create another entry to test the list operation + _, err = b.HandleRequest(certReq) + if err != nil { + t.Fatal(err) + } + + certReq.Operation = logical.ListOperation + certReq.Path = "config/certificates" + // test list operation + resp, err = b.HandleRequest(certReq) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatalf("failed to list config/certificates") + } + keys := resp.Data["keys"].([]string) + if len(keys) != 2 { + t.Fatalf("invalid keys listed: %#v\n", keys) + } + + certReq.Operation = logical.DeleteOperation + certReq.Path = "config/certificate/cert1" + _, err = b.HandleRequest(certReq) + if err != nil { + t.Fatal(err) + } + + certReq.Path = "config/certificate/cert2" + _, err = b.HandleRequest(certReq) + if err != nil { + t.Fatal(err) + } + + certReq.Operation = logical.ListOperation + certReq.Path = "config/certificates" + // test list operation + resp, err = b.HandleRequest(certReq) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.IsError() { + t.Fatalf("failed to list config/certificates") + } + if resp.Data["keys"] != nil { + t.Fatalf("no entries should be present") + } +} + +func TestBackend_pathRole(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.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() + storage := &logical.InmemStorage{} + config.StorageView = storage + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // create a role + data := map[string]interface{}{ + "policies": "p,q,r,s", + "max_ttl": "120s", + "role_tag": "VaultRole", + "bound_ami_id": "abcd-123", + } + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/abcd-123", + Storage: storage, + Data: data, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + // verify that the entry is created + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "role/abcd-123", + Storage: storage, + }) + if resp == nil { + t.Fatalf("expected an role entry for abcd-123") + } + + // create a role tag + data2 := map[string]interface{}{ + "policies": "p,q,r,s", + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/abcd-123/tag", + Storage: storage, + Data: data2, + }) + if err != nil { + t.Fatal(err) + } + if resp.Data["tag_key"].(string) == "" || + resp.Data["tag_value"].(string) == "" { + t.Fatalf("invalid tag response: %#v\n", resp) + } + tagValue := resp.Data["tag_value"].(string) + + // parse the value and check if the verifiable values match + rTag, err := b.parseAndVerifyRoleTagValue(storage, tagValue) + if err != nil { + t.Fatalf("err: %s", err) + } + if rTag == nil { + t.Fatalf("failed to parse role tag") + } + if rTag.Version != "v1" || + !policyutil.EquivalentPolicies(rTag.Policies, []string{"p", "q", "r", "s"}) || + rTag.Role != "abcd-123" { + t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag) + } +} + +func TestBackend_PathRoleTag(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "p,q,r,s", + "max_ttl": "120s", + "role_tag": "VaultRole", + "bound_ami_id": "abcd-123", + } + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/abcd-123", + Storage: storage, + Data: data, + }) + 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/abcd-123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatalf("failed to find a role entry for abcd-123") + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/abcd-123/tag", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("failed to create a tag on role: abcd-123") + } + if resp.IsError() { + t.Fatalf("failed to create a tag on role: abcd-123: %s\n", resp.Data["error"]) + } + if resp.Data["tag_value"].(string) == "" { + t.Fatalf("role tag not present in the response data: %#v\n", resp.Data) + } +} + +func TestBackend_PathBlacklistRoleTag(t *testing.T) { + // create the backend + storage := &logical.InmemStorage{} + config := logical.TestBackendConfig() + config.StorageView = storage + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + // create an role entry + data := map[string]interface{}{ + "policies": "p,q,r,s", + "role_tag": "VaultRole", + "bound_ami_id": "abcd-123", + } + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "role/abcd-123", + Storage: storage, + Data: data, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + // create a role tag against an role registered before + data2 := map[string]interface{}{ + "policies": "p,q,r,s", + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/abcd-123/tag", + Storage: storage, + Data: data2, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("failed to create a tag on role: abcd-123") + } + if resp.IsError() { + t.Fatalf("failed to create a tag on role: abcd-123: %s\n", resp.Data["error"]) + } + tag := resp.Data["tag_value"].(string) + if tag == "" { + t.Fatalf("role tag not present in the response data: %#v\n", resp.Data) + } + + // blacklist that role tag + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "roletag-blacklist/" + tag, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatalf("failed to blacklist the roletag: %s\n", tag) + } + + // read the blacklist entry + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "roletag-blacklist/" + tag, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil { + t.Fatalf("failed to read the blacklisted role tag: %s\n", tag) + } + if resp.IsError() { + t.Fatalf("failed to read the blacklisted role tag:%s. Err: %s\n", tag, resp.Data["error"]) + } + + // delete the blacklisted entry + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.DeleteOperation, + Path: "roletag-blacklist/" + tag, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + // try to read the deleted entry + tagEntry, err := b.blacklistRoleTagEntry(storage, tag) + if err != nil { + t.Fatal(err) + } + if tagEntry != nil { + t.Fatalf("role tag should not have been present: %s\n", tag) + } +} + +// This is an acceptance test. +// Requires TEST_AWS_EC2_PKCS7, TEST_AWS_EC2_AMI_ID to be set. +// If the test is not being run on an EC2 instance that has access to credentials using EC2RoleProvider, +// then TEST_AWS_SECRET_KEY and TEST_AWS_ACCESS_KEY env vars are also required. +func TestBackendAcc_LoginAndWhitelistIdentity(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 + } + + pkcs7 := os.Getenv("TEST_AWS_EC2_PKCS7") + if pkcs7 == "" { + t.Fatalf("env var TEST_AWS_EC2_PKCS7 not set") + } + + amiID := os.Getenv("TEST_AWS_EC2_AMI_ID") + if amiID == "" { + t.Fatalf("env var TEST_AWS_EC2_AMI_ID not set") + } + + roleName := amiID + + // create the backend + storage := &logical.InmemStorage{} + config := logical.TestBackendConfig() + config.StorageView = storage + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) + if err != nil { + t.Fatal(err) + } + + accessKey := os.Getenv("TEST_AWS_ACCESS_KEY") + secretKey := os.Getenv("TEST_AWS_SECRET_KEY") + + // In case of problems with making API calls using the credentials (2FA enabled, + // for instance), the keys need not be set if the test is running on an EC2 + // instance with permissions to get the credentials using EC2RoleProvider. + if accessKey != "" && secretKey != "" { + // get the API credentials from env vars + clientConfig := map[string]interface{}{ + "access_key": accessKey, + "secret_key": secretKey, + } + if clientConfig["access_key"] == "" || + clientConfig["secret_key"] == "" { + t.Fatalf("credentials not configured") + } + + // store the credentials + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Storage: storage, + Path: "config/client", + Data: clientConfig, + }) + if err != nil { + t.Fatal(err) + } + } + + // create an entry for the role. This is required for login to work. + data := map[string]interface{}{ + "policies": "root", + "max_ttl": "120s", + "bound_ami_id": amiID, + } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/" + roleName, + Storage: storage, + Data: data, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } + if err != nil { + t.Fatal(err) + } + + loginInput := map[string]interface{}{ + "pkcs7": pkcs7, + "nonce": "vault-client-nonce", + } + + // perform the login operation. + loginRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: loginInput, + } + resp, err = b.HandleRequest(loginRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Fatalf("first login attempt failed") + } + + // Attempt to login again and see if it succeeds + resp, err = b.HandleRequest(loginRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Fatalf("second login attempt failed") + } + + // verify the presence of instance_id in the response object. + instanceID := resp.Auth.Metadata["instance_id"] + if instanceID == "" { + t.Fatalf("instance ID not present in the response object") + } + + loginInput["nonce"] = "changed-vault-client-nonce" + // try to login again with changed nonce + resp, err = b.HandleRequest(loginRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || !resp.IsError() { + t.Fatalf("login attempt should have failed due to client nonce mismatch") + } + + // Check if a whitelist identity entry is created after the login. + wlRequest := &logical.Request{ + Operation: logical.ReadOperation, + Path: "identity-whitelist/" + instanceID, + Storage: storage, + } + resp, err = b.HandleRequest(wlRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil || resp.Data["role"] != roleName { + t.Fatalf("failed to read whitelist identity") + } + + // Delete the whitelist identity entry. + wlRequest.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(wlRequest) + if err != nil { + t.Fatal(err) + } + if resp.IsError() { + t.Fatalf("failed to delete whitelist identity") + } + + // Allow a fresh login. + resp, err = b.HandleRequest(loginRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Fatalf("login attempt failed") + } +} diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go new file mode 100644 index 0000000000..f08b7c04ae --- /dev/null +++ b/builtin/credential/aws/client.go @@ -0,0 +1,103 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault/helper/awsutil" + "github.com/hashicorp/vault/logical" +) + +// getClientConfig 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) { + credsConfig := &awsutil.CredentialsConfig{ + Region: region, + } + + // Read the configured secret key and access key + config, err := b.clientConfigEntryInternal(s) + if err != nil { + return nil, err + } + + endpoint := aws.String("") + if config != nil { + // Override the default endpoint with the configured endpoint. + if config.Endpoint != "" { + endpoint = aws.String(config.Endpoint) + } + + credsConfig.AccessKey = config.AccessKey + credsConfig.SecretKey = config.SecretKey + } + + credsConfig.HTTPClient = cleanhttp.DefaultClient() + + creds, err := credsConfig.GenerateCredentialChain() + if err != nil { + return nil, err + } + if creds == nil { + return nil, fmt.Errorf("could not compile valid credential providers from static config, environemnt, shared, or instance metadata") + } + + // Create a config that can be used to make the API calls. + return &aws.Config{ + Credentials: creds, + Region: aws.String(region), + HTTPClient: cleanhttp.DefaultClient(), + Endpoint: endpoint, + }, nil +} + +// flushCachedEC2Clients deletes all the cached ec2 client objects from the backend. +// If the client credentials configuration is deleted or updated in the backend, all +// the cached EC2 client objects will be flushed. +// +// Write lock should be acquired using b.configMutex.Lock() before calling this method +// and lock should be released using b.configMutex.Unlock() after the method returns. +func (b *backend) flushCachedEC2Clients() { + // deleting items in map during iteration is safe. + for region, _ := range b.EC2ClientsMap { + delete(b.EC2ClientsMap, region) + } +} + +// clientEC2 creates a client to interact with AWS EC2 API. +func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error) { + b.configMutex.RLock() + if b.EC2ClientsMap[region] != nil { + defer b.configMutex.RUnlock() + // If the client object was already created, return it. + return b.EC2ClientsMap[region], nil + } + + // Release the read lock and acquire the write lock. + b.configMutex.RUnlock() + b.configMutex.Lock() + defer b.configMutex.Unlock() + + // If the client gets created while switching the locks, return it. + if b.EC2ClientsMap[region] != nil { + return b.EC2ClientsMap[region], nil + } + + // Create a AWS config object using a chain of providers. + awsConfig, err := b.getClientConfig(s, region) + if err != nil { + return nil, err + } + + // Create a new EC2 client object, cache it and return the same. + b.EC2ClientsMap[region] = ec2.New(session.New(awsConfig)) + return b.EC2ClientsMap[region], nil +} diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go new file mode 100644 index 0000000000..86c91ce9c5 --- /dev/null +++ b/builtin/credential/aws/path_config_certificate.go @@ -0,0 +1,334 @@ +package aws + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "math/big" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +// dsaSignature represents the contents of the signature of a signed +// content using digital signature algorithm. +type dsaSignature struct { + R, S *big.Int +} + +// As per AWS documentation, this public key is valid for US East (N. Virginia), +// US West (Oregon), US West (N. California), EU (Ireland), EU (Frankfurt), +// Asia Pacific (Tokyo), Asia Pacific (Seoul), Asia Pacific (Singapore), +// Asia Pacific (Sydney), and South America (Sao Paulo). +// +// It's also the same certificate, but for some reason listed separately, for +// GovCloud (US) +const genericAWSPublicCertificate = `-----BEGIN CERTIFICATE----- +MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw +FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD +VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z +ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u +IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl +cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e +ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 +VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P +hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j +k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U +hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF +lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf +MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW +MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw +vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw +7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K +-----END CERTIFICATE----- +` + +// pathListCertificates creates a path that enables listing of all +// the AWS public certificates registered with Vault. +func pathListCertificates(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/certificates/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathCertificatesList, + }, + + HelpSynopsis: pathListCertificatesHelpSyn, + HelpDescription: pathListCertificatesHelpDesc, + } +} + +func pathConfigCertificate(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/certificate/" + framework.GenericNameRegex("cert_name"), + Fields: map[string]*framework.FieldSchema{ + "cert_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the certificate.", + }, + "aws_public_cert": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "AWS Public cert required to verify PKCS7 signature of the EC2 instance metadata.", + }, + }, + + ExistenceCheck: b.pathConfigCertificateExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigCertificateCreateUpdate, + logical.UpdateOperation: b.pathConfigCertificateCreateUpdate, + logical.ReadOperation: b.pathConfigCertificateRead, + logical.DeleteOperation: b.pathConfigCertificateDelete, + }, + + HelpSynopsis: pathConfigCertificateSyn, + HelpDescription: pathConfigCertificateDesc, + } +} + +// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. +// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. +func (b *backend) pathConfigCertificateExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + certName := data.Get("cert_name").(string) + if certName == "" { + return false, fmt.Errorf("missing cert_name") + } + + entry, err := b.awsPublicCertificateEntry(req.Storage, certName) + if err != nil { + return false, err + } + return entry != nil, nil +} + +// pathCertificatesList is used to list all the AWS public certificates registered with Vault. +func (b *backend) pathCertificatesList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + certs, err := req.Storage.List("config/certificate/") + if err != nil { + return nil, err + } + return logical.ListResponse(certs), nil +} + +// Decodes the PEM encoded certiticate and parses it into a x509 cert. +func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) { + // Decode the PEM block and error out if a block is not detected in the first attempt. + decodedPublicCert, rest := pem.Decode([]byte(certificate)) + if len(rest) != 0 { + return nil, fmt.Errorf("invalid certificate; should be one PEM block only") + } + + // Check if the certificate can be parsed. + publicCert, err := x509.ParseCertificate(decodedPublicCert.Bytes) + if err != nil { + return nil, err + } + if publicCert == nil { + return nil, fmt.Errorf("invalid certificate; failed to parse certificate") + } + return publicCert, nil +} + +// awsPublicCertificates returns a slice of all the parsed AWS public +// certificates, that were registered using `config/certificate/` endpoint. +// This method will also append default certificate in the backend, to the slice. +func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { + // Lock at beginning and use internal method so that we are consistent as + // we iterate through + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + var certs []*x509.Certificate + + // Append the generic certificate provided in the AWS EC2 instance metadata documentation. + decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate) + if err != nil { + return nil, err + } + certs = append(certs, decodedCert) + + // Get the list of all the registered certificates. + registeredCerts, err := s.List("config/certificate/") + if err != nil { + return nil, err + } + + // Iterate through each certificate, parse and append it to a slice. + for _, cert := range registeredCerts { + certEntry, err := b.awsPublicCertificateEntryInternal(s, cert) + if err != nil { + return nil, err + } + if certEntry == nil { + return nil, fmt.Errorf("certificate storage has a nil entry under the name:%s\n", cert) + } + decodedCert, err := decodePEMAndParseCertificate(certEntry.AWSPublicCert) + if err != nil { + return nil, err + } + certs = append(certs, decodedCert) + } + + return certs, nil +} + +// awsPublicCertificate is used to get the configured AWS Public Key that is used +// to verify the PKCS#7 signature of the instance identity document. +func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + return b.awsPublicCertificateEntryInternal(s, certName) +} + +// Internal version of the above that does no locking +func (b *backend) awsPublicCertificateEntryInternal(s logical.Storage, certName string) (*awsPublicCert, error) { + entry, err := s.Get("config/certificate/" + certName) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result awsPublicCert + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +// pathConfigCertificateDelete is used to delete the previously configured AWS Public Key +// that is used to verify the PKCS#7 signature of the instance identity document. +func (b *backend) pathConfigCertificateDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + certName := data.Get("cert_name").(string) + if certName == "" { + return logical.ErrorResponse("missing cert_name"), nil + } + + return nil, req.Storage.Delete("config/certificate/" + certName) +} + +// pathConfigCertificateRead is used to view the configured AWS Public Key that is +// used to verify the PKCS#7 signature of the instance identity document. +func (b *backend) pathConfigCertificateRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + certName := data.Get("cert_name").(string) + if certName == "" { + return logical.ErrorResponse("missing cert_name"), nil + } + + certificateEntry, err := b.awsPublicCertificateEntry(req.Storage, certName) + if err != nil { + return nil, err + } + if certificateEntry == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(certificateEntry).Map(), + }, nil +} + +// pathConfigCertificateCreateUpdate is used to register an AWS Public Key that is +// used to verify the PKCS#7 signature of the instance identity document. +func (b *backend) pathConfigCertificateCreateUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + certName := data.Get("cert_name").(string) + if certName == "" { + return logical.ErrorResponse("missing cert_name"), nil + } + + b.configMutex.Lock() + defer b.configMutex.Unlock() + + // Check if there is already a certificate entry registered. + certEntry, err := b.awsPublicCertificateEntryInternal(req.Storage, certName) + if err != nil { + return nil, err + } + if certEntry == nil { + certEntry = &awsPublicCert{} + } + + // Check if the value is provided by the client. + certStrData, ok := data.GetOk("aws_public_cert") + if ok { + if certBytes, err := base64.StdEncoding.DecodeString(certStrData.(string)); err == nil { + certEntry.AWSPublicCert = string(certBytes) + } else { + certEntry.AWSPublicCert = certStrData.(string) + } + } else { + // aws_public_cert should be supplied for both create and update operations. + // If it is not provided, throw an error. + return logical.ErrorResponse("missing aws_public_cert"), nil + } + + // If explicitly set to empty string, error out. + if certEntry.AWSPublicCert == "" { + return logical.ErrorResponse("invalid aws_public_cert"), nil + } + + // Verify the certificate by decoding it and parsing it. + publicCert, err := decodePEMAndParseCertificate(certEntry.AWSPublicCert) + if err != nil { + return nil, err + } + if publicCert == nil { + return logical.ErrorResponse("invalid certificate; failed to decode and parse certificate"), nil + } + + // Ensure that we have not + // If none of the checks fail, save the provided certificate. + entry, err := logical.StorageEntryJSON("config/certificate/"+certName, certEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +// Struct awsPublicCert holds the AWS Public Key that is used to verify the PKCS#7 signature +// of the instnace identity document. +type awsPublicCert struct { + AWSPublicCert string `json:"aws_public_cert" structs:"aws_public_cert" mapstructure:"aws_public_cert"` +} + +const pathConfigCertificateSyn = ` +Adds the AWS Public Key that is used to verify the PKCS#7 signature of the identidy document. +` + +const pathConfigCertificateDesc = ` +AWS Public Key which is used to verify the PKCS#7 signature of the identity document, +varies by region. The public key(s) can be found in AWS EC2 instance metadata documentation. +The default key that is used to verify the signature is the one that is applicable for +following regions: US East (N. Virginia), US West (Oregon), US West (N. California), +EU (Ireland), EU (Frankfurt), Asia Pacific (Tokyo), Asia Pacific (Seoul), Asia Pacific (Singapore), +Asia Pacific (Sydney), and South America (Sao Paulo). + +If the instances belongs to region other than the above, the public key(s) for the +corresponding regions should be registered using this endpoint. PKCS#7 is verified +using a collection of certificates containing the default certificate and all the +certificates that are registered using this endpoint. +` +const pathListCertificatesHelpSyn = ` +Lists all the AWS public certificates that are registered with the backend. +` +const pathListCertificatesHelpDesc = ` +Certificates will be listed by their respective names that were used during registration. +` diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go new file mode 100644 index 0000000000..38e2ad7ffa --- /dev/null +++ b/builtin/credential/aws/path_config_client.go @@ -0,0 +1,199 @@ +package aws + +import ( + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfigClient(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/client$", + Fields: map[string]*framework.FieldSchema{ + "access_key": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "AWS Access key with permissions to query EC2 DescribeInstances API.", + }, + + "secret_key": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "AWS Secret key with permissions to query EC2 DescribeInstances API.", + }, + + "endpoint": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "URL to override the default generated endpoint for making AWS EC2 API calls.", + }, + }, + + ExistenceCheck: b.pathConfigClientExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigClientCreateUpdate, + logical.UpdateOperation: b.pathConfigClientCreateUpdate, + logical.DeleteOperation: b.pathConfigClientDelete, + logical.ReadOperation: b.pathConfigClientRead, + }, + + HelpSynopsis: pathConfigClientHelpSyn, + HelpDescription: pathConfigClientHelpDesc, + } +} + +// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. +// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. +func (b *backend) pathConfigClientExistenceCheck( + req *logical.Request, data *framework.FieldData) (bool, error) { + + entry, err := b.clientConfigEntry(req.Storage) + if err != nil { + return false, err + } + return entry != nil, nil +} + +// Fetch the client configuration required to access the AWS API. +func (b *backend) clientConfigEntry(s logical.Storage) (*clientConfig, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + return b.clientConfigEntryInternal(s) +} + +// Internal version that does no locking +func (b *backend) clientConfigEntryInternal(s logical.Storage) (*clientConfig, error) { + entry, err := s.Get("config/client") + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result clientConfig + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +func (b *backend) pathConfigClientRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + clientConfig, err := b.clientConfigEntry(req.Storage) + if err != nil { + return nil, err + } + + if clientConfig == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(clientConfig).Map(), + }, nil +} + +func (b *backend) pathConfigClientDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + if err := req.Storage.Delete("config/client"); err != nil { + return nil, err + } + + // Remove all the cached EC2 client objects in the backend. + b.flushCachedEC2Clients() + + return nil, nil +} + +// pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key' +// that can be used to interact with AWS EC2 API. +func (b *backend) pathConfigClientCreateUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + configEntry, err := b.clientConfigEntryInternal(req.Storage) + if err != nil { + return nil, err + } + if configEntry == nil { + configEntry = &clientConfig{} + } + + changedCreds := false + + accessKeyStr, ok := data.GetOk("access_key") + if ok { + if configEntry.AccessKey != accessKeyStr.(string) { + changedCreds = true + configEntry.AccessKey = accessKeyStr.(string) + } + } else if req.Operation == logical.CreateOperation { + // Use the default + configEntry.AccessKey = data.Get("access_key").(string) + } + + secretKeyStr, ok := data.GetOk("secret_key") + if ok { + if configEntry.SecretKey != secretKeyStr.(string) { + changedCreds = true + configEntry.SecretKey = secretKeyStr.(string) + } + } else if req.Operation == logical.CreateOperation { + configEntry.SecretKey = data.Get("secret_key").(string) + } + + endpointStr, ok := data.GetOk("endpoint") + if ok { + if configEntry.Endpoint != endpointStr.(string) { + changedCreds = true + configEntry.Endpoint = endpointStr.(string) + } + } else if req.Operation == logical.CreateOperation { + configEntry.Endpoint = data.Get("endpoint").(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. + // Hence, the readers of this endpoint should do the validation on + // the validation of keys before using them. + entry, err := logical.StorageEntryJSON("config/client", configEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + if changedCreds { + b.flushCachedEC2Clients() + } + + return nil, nil +} + +// 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"` +} + +const pathConfigClientHelpSyn = ` +Configure the client credentials that are used to query instance details from AWS EC2 API. +` + +const pathConfigClientHelpDesc = ` +AWS auth backend makes DescribeInstances API call to retrieve information regarding +the instance that performs login. The aws_secret_key and aws_access_key registered with Vault should have the +permissions to make the API call. +` diff --git a/builtin/credential/aws/path_config_tidy_identity_whitelist.go b/builtin/credential/aws/path_config_tidy_identity_whitelist.go new file mode 100644 index 0000000000..700e5fa21a --- /dev/null +++ b/builtin/credential/aws/path_config_tidy_identity_whitelist.go @@ -0,0 +1,149 @@ +package aws + +import ( + "fmt" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +const ( + identityWhitelistConfigPath = "config/tidy/identity-whitelist" +) + +func pathConfigTidyIdentityWhitelist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: fmt.Sprintf("%s$", identityWhitelistConfigPath), + Fields: map[string]*framework.FieldSchema{ + "safety_buffer": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 259200, //72h + Description: `The amount of extra time that must have passed beyond the identity's +expiration, before it is removed from the backend storage.`, + }, + "disable_periodic_tidy": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set to 'true', disables the periodic tidying of the 'identity-whitelist/' entries.", + }, + }, + + ExistenceCheck: b.pathConfigTidyIdentityWhitelistExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigTidyIdentityWhitelistCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyIdentityWhitelistCreateUpdate, + logical.ReadOperation: b.pathConfigTidyIdentityWhitelistRead, + logical.DeleteOperation: b.pathConfigTidyIdentityWhitelistDelete, + }, + + HelpSynopsis: pathConfigTidyIdentityWhitelistHelpSyn, + HelpDescription: pathConfigTidyIdentityWhitelistHelpDesc, + } +} + +func (b *backend) pathConfigTidyIdentityWhitelistExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + entry, err := b.configTidyIdentities(req.Storage) + if err != nil { + return false, err + } + return entry != nil, nil +} + +func (b *backend) configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + return b.configTidyIdentitiesInternal(s) +} + +func (b *backend) configTidyIdentitiesInternal(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { + entry, err := s.Get(identityWhitelistConfigPath) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result tidyWhitelistIdentityConfig + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +func (b *backend) pathConfigTidyIdentityWhitelistCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + configEntry, err := b.configTidyIdentitiesInternal(req.Storage) + if err != nil { + return nil, err + } + if configEntry == nil { + configEntry = &tidyWhitelistIdentityConfig{} + } + + safetyBufferInt, ok := data.GetOk("safety_buffer") + if ok { + configEntry.SafetyBuffer = safetyBufferInt.(int) + } else if req.Operation == logical.CreateOperation { + configEntry.SafetyBuffer = data.Get("safety_buffer").(int) + } + + disablePeriodicTidyBool, ok := data.GetOk("disable_periodic_tidy") + if ok { + configEntry.DisablePeriodicTidy = disablePeriodicTidyBool.(bool) + } else if req.Operation == logical.CreateOperation { + configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) + } + + entry, err := logical.StorageEntryJSON(identityWhitelistConfigPath, configEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathConfigTidyIdentityWhitelistRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + clientConfig, err := b.configTidyIdentities(req.Storage) + if err != nil { + return nil, err + } + if clientConfig == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(clientConfig).Map(), + }, nil +} + +func (b *backend) pathConfigTidyIdentityWhitelistDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + return nil, req.Storage.Delete(identityWhitelistConfigPath) +} + +type tidyWhitelistIdentityConfig struct { + SafetyBuffer int `json:"safety_buffer" structs:"safety_buffer" mapstructure:"safety_buffer"` + DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"` +} + +const pathConfigTidyIdentityWhitelistHelpSyn = ` +Configures the periodic tidying operation of the whitelisted identity entries. +` +const pathConfigTidyIdentityWhitelistHelpDesc = ` +By default, the expired entries in the whitelist will be attempted to be removed +periodically. This operation will look for expired items in the list and purges them. +However, there is a safety buffer duration (defaults to 72h), purges the entries +only if they have been persisting this duration, past its expiration time. +` diff --git a/builtin/credential/aws/path_config_tidy_roletag_blacklist.go b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go new file mode 100644 index 0000000000..6932f5ec96 --- /dev/null +++ b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go @@ -0,0 +1,150 @@ +package aws + +import ( + "fmt" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +const ( + roletagBlacklistConfigPath = "config/tidy/roletag-blacklist" +) + +func pathConfigTidyRoletagBlacklist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: fmt.Sprintf("%s$", roletagBlacklistConfigPath), + Fields: map[string]*framework.FieldSchema{ + "safety_buffer": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 15552000, //180d + Description: `The amount of extra time that must have passed beyond the roletag +expiration, before it is removed from the backend storage. +Defaults to 4320h (180 days).`, + }, + + "disable_periodic_tidy": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set to 'true', disables the periodic tidying of blacklisted entries.", + }, + }, + + ExistenceCheck: b.pathConfigTidyRoletagBlacklistExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigTidyRoletagBlacklistCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyRoletagBlacklistCreateUpdate, + logical.ReadOperation: b.pathConfigTidyRoletagBlacklistRead, + logical.DeleteOperation: b.pathConfigTidyRoletagBlacklistDelete, + }, + + HelpSynopsis: pathConfigTidyRoletagBlacklistHelpSyn, + HelpDescription: pathConfigTidyRoletagBlacklistHelpDesc, + } +} + +func (b *backend) pathConfigTidyRoletagBlacklistExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + entry, err := b.configTidyRoleTags(req.Storage) + if err != nil { + return false, err + } + return entry != nil, nil +} + +func (b *backend) configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + return b.configTidyRoleTagsInternal(s) +} + +func (b *backend) configTidyRoleTagsInternal(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { + entry, err := s.Get(roletagBlacklistConfigPath) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result tidyBlacklistRoleTagConfig + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (b *backend) pathConfigTidyRoletagBlacklistCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + configEntry, err := b.configTidyRoleTagsInternal(req.Storage) + if err != nil { + return nil, err + } + if configEntry == nil { + configEntry = &tidyBlacklistRoleTagConfig{} + } + safetyBufferInt, ok := data.GetOk("safety_buffer") + if ok { + configEntry.SafetyBuffer = safetyBufferInt.(int) + } else if req.Operation == logical.CreateOperation { + configEntry.SafetyBuffer = data.Get("safety_buffer").(int) + } + disablePeriodicTidyBool, ok := data.GetOk("disable_periodic_tidy") + if ok { + configEntry.DisablePeriodicTidy = disablePeriodicTidyBool.(bool) + } else if req.Operation == logical.CreateOperation { + configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) + } + + entry, err := logical.StorageEntryJSON(roletagBlacklistConfigPath, configEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathConfigTidyRoletagBlacklistRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + clientConfig, err := b.configTidyRoleTags(req.Storage) + if err != nil { + return nil, err + } + if clientConfig == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(clientConfig).Map(), + }, nil +} + +func (b *backend) pathConfigTidyRoletagBlacklistDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + return nil, req.Storage.Delete(roletagBlacklistConfigPath) +} + +type tidyBlacklistRoleTagConfig struct { + SafetyBuffer int `json:"safety_buffer" structs:"safety_buffer" mapstructure:"safety_buffer"` + DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"` +} + +const pathConfigTidyRoletagBlacklistHelpSyn = ` +Configures the periodic tidying operation of the blacklisted role tag entries. +` +const pathConfigTidyRoletagBlacklistHelpDesc = ` +By default, the expired entries in the blacklist will be attempted to be removed +periodically. This operation will look for expired items in the list and purges them. +However, there is a safety buffer duration (defaults to 72h), purges the entries +only if they have been persisting this duration, past its expiration time. +` diff --git a/builtin/credential/aws/path_identity_whitelist.go b/builtin/credential/aws/path_identity_whitelist.go new file mode 100644 index 0000000000..ba7b861b78 --- /dev/null +++ b/builtin/credential/aws/path_identity_whitelist.go @@ -0,0 +1,154 @@ +package aws + +import ( + "time" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathIdentityWhitelist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "identity-whitelist/" + framework.GenericNameRegex("instance_id"), + Fields: map[string]*framework.FieldSchema{ + "instance_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `EC2 instance ID. A successful login operation from an EC2 instance +gets cached in this whitelist, keyed off of instance ID.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathIdentityWhitelistRead, + logical.DeleteOperation: b.pathIdentityWhitelistDelete, + }, + + HelpSynopsis: pathIdentityWhitelistSyn, + HelpDescription: pathIdentityWhitelistDesc, + } +} + +func pathListIdentityWhitelist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "identity-whitelist/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathWhitelistIdentitiesList, + }, + + HelpSynopsis: pathListIdentityWhitelistHelpSyn, + HelpDescription: pathListIdentityWhitelistHelpDesc, + } +} + +// pathWhitelistIdentitiesList is used to list all the instance IDs that are present +// in the identity whitelist. This will list both valid and expired entries. +func (b *backend) pathWhitelistIdentitiesList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + identities, err := req.Storage.List("whitelist/identity/") + if err != nil { + return nil, err + } + return logical.ListResponse(identities), nil +} + +// Fetch an item from the whitelist given an instance ID. +func whitelistIdentityEntry(s logical.Storage, instanceID string) (*whitelistIdentity, error) { + entry, err := s.Get("whitelist/identity/" + instanceID) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result whitelistIdentity + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +// Stores an instance ID and the information required to validate further login/renewal attempts from +// the same instance ID. +func setWhitelistIdentityEntry(s logical.Storage, instanceID string, identity *whitelistIdentity) error { + entry, err := logical.StorageEntryJSON("whitelist/identity/"+instanceID, identity) + if err != nil { + return err + } + + if err := s.Put(entry); err != nil { + return err + } + return nil +} + +// pathIdentityWhitelistDelete is used to delete an entry from the identity whitelist given an instance ID. +func (b *backend) pathIdentityWhitelistDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + instanceID := data.Get("instance_id").(string) + if instanceID == "" { + return logical.ErrorResponse("missing instance_id"), nil + } + + return nil, req.Storage.Delete("whitelist/identity/" + instanceID) +} + +// pathIdentityWhitelistRead is used to view an entry in the identity whitelist given an instance ID. +func (b *backend) pathIdentityWhitelistRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + instanceID := data.Get("instance_id").(string) + if instanceID == "" { + return logical.ErrorResponse("missing instance_id"), nil + } + + entry, err := whitelistIdentityEntry(req.Storage, instanceID) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(entry).Map(), + }, nil +} + +// Struct to represent each item in the identity whitelist. +type whitelistIdentity struct { + Role string `json:"role" structs:"role" mapstructure:"role"` + ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` + CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` + PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` + ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` + LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` +} + +const pathIdentityWhitelistSyn = ` +Read or delete entries in the identity whitelist. +` + +const pathIdentityWhitelistDesc = ` +Each login from an EC2 instance creates/updates an entry in the identity whitelist. + +Entries in this list can be viewed or deleted using this endpoint. + +By default, a cron task will periodically look for expired entries in the whitelist +and deletes them. The duration to periodically run this, is one hour by default. +However, this can be configured using the 'config/tidy/identities' endpoint. This tidy +action can be triggered via the API as well, using the 'tidy/identities' endpoint. +` + +const pathListIdentityWhitelistHelpSyn = ` +Lists the items present in the identity whitelist. +` + +const pathListIdentityWhitelistHelpDesc = ` +The entries in the identity whitelist is keyed off of the EC2 instance IDs. +This endpoint lists all the entries present in the identity whitelist, both +expired and un-expired entries. Use 'tidy/identities' endpoint to clean-up +the whitelist of identities. +` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go new file mode 100644 index 0000000000..7aecff4461 --- /dev/null +++ b/builtin/credential/aws/path_login.go @@ -0,0 +1,567 @@ +package aws + +import ( + "encoding/json" + "encoding/pem" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/fullsailor/pkcs7" + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathLogin(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "login$", + Fields: map[string]*framework.FieldSchema{ + "role": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `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.`, + }, + + "pkcs7": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "PKCS7 signature of the identity document.", + }, + + "nonce": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `The nonce created by a client of this backend. When 'disallow_reauthentication' +option is enabled on either the role or the role tag, then nonce parameter is +optional. It is a required parameter otherwise.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathLoginUpdate, + }, + + HelpSynopsis: pathLoginSyn, + HelpDescription: pathLoginDesc, + } +} + +// validateInstance queries the status of the EC2 instance using AWS EC2 API and +// checks if the instance is running and is healthy. +func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.DescribeInstancesOutput, error) { + // Create an EC2 client to pull the instance information + ec2Client, err := b.clientEC2(s, region) + if err != nil { + return nil, err + } + + status, err := ec2Client.DescribeInstances(&ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("instance-id"), + Values: []*string{ + aws.String(instanceID), + }, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", instanceID, err) + } + if len(status.Reservations) == 0 { + return nil, fmt.Errorf("no reservations found in instance description") + + } + if len(status.Reservations[0].Instances) == 0 { + return nil, fmt.Errorf("no instance details found in reservations") + } + if *status.Reservations[0].Instances[0].InstanceId != instanceID { + return nil, fmt.Errorf("expected instance ID not matching the instance ID in the instance description") + } + if status.Reservations[0].Instances[0].State == nil { + return nil, fmt.Errorf("instance state in instance description is nil") + } + if *status.Reservations[0].Instances[0].State.Code != 16 || + *status.Reservations[0].Instances[0].State.Name != "running" { + return nil, fmt.Errorf("instance is not in 'running' state") + } + return status, nil +} + +// validateMetadata matches the given client nonce and pending time with the one cached +// in the identity whitelist during the previous login. But, if reauthentication is +// disabled, login attempt is failed immediately. +func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, roleEntry *awsRoleEntry) error { + // If reauthentication is disabled, doesn't matter what other metadata is provided, + // authentication will not succeed. + if storedIdentity.DisallowReauthentication { + return fmt.Errorf("reauthentication is disabled") + } + + givenPendingTime, err := time.Parse(time.RFC3339, pendingTime) + if err != nil { + return err + } + + storedPendingTime, err := time.Parse(time.RFC3339, storedIdentity.PendingTime) + if err != nil { + return err + } + + // When the presented client nonce does not match the cached entry, it is + // either that a rogue client is trying to login or that a valid client + // suffered a migration. The migration is detected via pendingTime in the + // instance metadata, which sadly is only updated when an instance is + // stopped and started but *not* when the instance is rebooted. If reboot + // survivability is needed, either instrumentation to delete the instance + // ID from the whitelist is necessary, or the client must durably store + // the nonce. + // + // If the `allow_instance_migration` property of the registered role is + // enabled, then the client nonce mismatch is ignored, as long as the + // pending time in the presented instance identity document is newer than + // the cached pending time. The new pendingTime is stored and used for + // future checks. + // + // This is a weak criterion and hence the `allow_instance_migration` option + // should be used with caution. + if clientNonce != storedIdentity.ClientNonce { + if !roleEntry.AllowInstanceMigration { + return fmt.Errorf("client nonce mismatch") + } + if roleEntry.AllowInstanceMigration && !givenPendingTime.After(storedPendingTime) { + return fmt.Errorf("client nonce mismatch and instance meta-data incorrect") + } + } + + // Ensure that the 'pendingTime' on the given identity document is not before the + // 'pendingTime' that was used for previous login. This disallows old metadata documents + // from being used to perform login. + if givenPendingTime.Before(storedPendingTime) { + return fmt.Errorf("instance meta-data is older than the one used for previous login") + } + return nil +} + +// Verifies the correctness of the authenticated attributes present in the PKCS#7 +// signature. After verification, extracts the instance identity document from the +// signature, parses it and returns it. +func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) { + // Insert the header and footer for the signature to be able to pem decode it. + pkcs7B64 = fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", pkcs7B64) + + // Decode the PEM encoded signature. + pkcs7BER, pkcs7Rest := pem.Decode([]byte(pkcs7B64)) + if len(pkcs7Rest) != 0 { + return nil, fmt.Errorf("failed to decode the PEM encoded PKCS#7 signature") + } + + // Parse the signature from asn1 format into a struct. + pkcs7Data, err := pkcs7.Parse(pkcs7BER.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse the BER encoded PKCS#7 signature: %s\n", err) + } + + // Get the public certificates that are used to verify the signature. + // This returns a slice of certificates containing the default certificate + // and all the registered certificates via 'config/certificate/' endpoint + publicCerts, err := b.awsPublicCertificates(s) + if err != nil { + return nil, err + } + if publicCerts == nil || len(publicCerts) == 0 { + return nil, fmt.Errorf("certificates to verify the signature are not found") + } + + // Before calling Verify() on the PKCS#7 struct, set the certificates to be used + // to verify the contents in the signer information. + pkcs7Data.Certificates = publicCerts + + // Verify extracts the authenticated attributes in the PKCS#7 signature, and verifies + // the authenticity of the content using 'dsa.PublicKey' embedded in the public certificate. + if pkcs7Data.Verify() != nil { + return nil, fmt.Errorf("failed to verify the signature") + } + + // Check if the signature has content inside of it. + if len(pkcs7Data.Content) == 0 { + return nil, fmt.Errorf("instance identity document could not be found in the signature") + } + + var identityDoc identityDocument + err = json.Unmarshal(pkcs7Data.Content, &identityDoc) + if err != nil { + return nil, err + } + + return &identityDoc, nil +} + +// pathLoginUpdate 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( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + pkcs7B64 := data.Get("pkcs7").(string) + if pkcs7B64 == "" { + return logical.ErrorResponse("missing pkcs7"), nil + } + + // Verify the signature of the identity document. + identityDoc, err := b.parseIdentityDocument(req.Storage, pkcs7B64) + if err != nil { + return nil, err + } + if identityDoc == nil { + return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil + } + + roleName := data.Get("role").(string) + + // If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for. + if roleName == "" { + roleName = identityDoc.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, identityDoc.InstanceID, identityDoc.Region) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil + } + + // Get the entry for the role used by the instance. + roleEntry, err := b.awsRole(req.Storage, roleName) + if err != nil { + return nil, err + } + if roleEntry == nil { + return logical.ErrorResponse("role entry not found"), nil + } + + // Get the entry from the identity whitelist, if there is one. + storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDoc.InstanceID) + if err != nil { + return nil, err + } + + clientNonce := data.Get("nonce").(string) + + // This is NOT a first login attempt from the client. + if storedIdentity != nil { + // Check if the client nonce match the cached nonce and if the pending time + // of the identity document is not before the pending time of the document + // with which previous login was made. If 'allow_instance_migration' is + // enabled on the registered role, client nonce requirement is relaxed. + if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, roleEntry); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + } + + // Load the current values for max TTL and policies from the role entry, + // before checking for overriding max TTL in the role tag. The shortest + // max TTL is used to cap the token TTL; the longest max TTL is used to + // make the whitelist entry as long as possible as it controls for replay + // attacks. + shortestMaxTTL := b.System().MaxLeaseTTL() + longestMaxTTL := b.System().MaxLeaseTTL() + if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL { + shortestMaxTTL = roleEntry.MaxTTL + } + if roleEntry.MaxTTL > longestMaxTTL { + longestMaxTTL = roleEntry.MaxTTL + } + + policies := roleEntry.Policies + rTagMaxTTL := time.Duration(0) + + // Read this value from the role entry; however, once it's been set, do not + // allow it to be flipped back. This prevents a role with this set to false + // to be overridden by a role tag, then have the role tag swapped and have + // this go back to false. + disallowReauthentication := roleEntry.DisallowReauthentication + if storedIdentity != nil { + if !disallowReauthentication && storedIdentity.DisallowReauthentication { + disallowReauthentication = true + } + } + + 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, identityDoc, roleName, roleEntry, instanceDesc) + if err != nil { + return nil, err + } + if resp == nil { + return logical.ErrorResponse("failed to fetch and verify the role tag"), nil + } + + // If there are no policies on the role tag, policies on the role are inherited. + // If policies on role tag are set, by this point, it is verified that it is a subset of the + // policies on the role. So, apply only those. + if len(resp.Policies) != 0 { + policies = resp.Policies + } + + // If 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 + } + + // Cache the value of role tag's max_ttl value. + rTagMaxTTL = resp.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 resp.MaxTTL > longestMaxTTL { + longestMaxTTL = resp.MaxTTL + } + } + + // Save the login attempt in the identity whitelist. + currentTime := time.Now().UTC() + if storedIdentity == nil { + // Role, ClientNonce and CreationTime of the identity entry, + // once set, should never change. + storedIdentity = &whitelistIdentity{ + Role: roleName, + ClientNonce: clientNonce, + CreationTime: currentTime, + } + } + + // DisallowReauthentication, PendingTime, LastUpdatedTime and ExpirationTime may change. + storedIdentity.LastUpdatedTime = currentTime + storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL) + storedIdentity.PendingTime = identityDoc.PendingTime + storedIdentity.DisallowReauthentication = disallowReauthentication + + // Performing the clientNonce empty check after determining the DisallowReauthentication + // option. This is to make clientNonce optional when DisallowReauthentication is set. + if clientNonce == "" && !storedIdentity.DisallowReauthentication { + return logical.ErrorResponse("missing nonce"), nil + } + + // Limit the nonce to a reasonable length. + if len(clientNonce) > 128 && !storedIdentity.DisallowReauthentication { + return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil + } + + if err = setWhitelistIdentityEntry(req.Storage, identityDoc.InstanceID, storedIdentity); err != nil { + return nil, err + } + + resp := &logical.Response{ + Auth: &logical.Auth{ + Policies: policies, + Metadata: map[string]string{ + "instance_id": identityDoc.InstanceID, + "region": identityDoc.Region, + "role_tag_max_ttl": rTagMaxTTL.String(), + "role": roleName, + "ami_id": identityDoc.AmiID, + }, + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + TTL: b.System().DefaultLeaseTTL(), + }, + }, + } + + // Cap the TTL value. + if shortestMaxTTL < resp.Auth.TTL { + resp.Auth.TTL = shortestMaxTTL + } + + return resp, nil + +} + +// 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, identityDoc *identityDocument, roleName string, roleEntry *awsRoleEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) { + if identityDoc == nil { + return nil, fmt.Errorf("nil identityDoc") + } + if roleEntry == nil { + return nil, fmt.Errorf("nil roleEntry") + } + if instanceDesc == nil { + return nil, fmt.Errorf("nil instanceDesc") + } + + // Input validation on instanceDesc is not performed here considering + // that it would have been done in validateInstance method. + tags := instanceDesc.Reservations[0].Instances[0].Tags + if tags == nil || len(tags) == 0 { + return nil, fmt.Errorf("missing tag with key %s on the instance", roleEntry.RoleTag) + } + + // Iterate through the tags attached on the instance and look for + // a tag with its 'key' matching the expected role tag value. + rTagValue := "" + for _, tagItem := range tags { + if tagItem.Key != nil && *tagItem.Key == roleEntry.RoleTag { + rTagValue = *tagItem.Value + break + } + } + + // If 'role_tag' is enabled on the role, and if a corresponding tag is not found + // to be attached to the instance, fail. + if rTagValue == "" { + return nil, fmt.Errorf("missing tag with key %s on the instance", roleEntry.RoleTag) + } + + // Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC. + rTag, err := b.parseAndVerifyRoleTagValue(s, rTagValue) + if err != nil { + return nil, err + } + + // Check if the role name with which this login is being made is same + // as the role name embedded in the tag. + if rTag.Role != roleName { + return nil, fmt.Errorf("role on the tag is not matching the role supplied") + } + + // If instance_id was set on the role tag, check if the same instance is attempting to login. + if rTag.InstanceID != "" && rTag.InstanceID != identityDoc.InstanceID { + return nil, fmt.Errorf("role tag is being used by an unauthorized instance.") + } + + // Check if the role tag is blacklisted. + blacklistEntry, err := b.blacklistRoleTagEntry(s, rTagValue) + if err != nil { + return nil, err + } + if blacklistEntry != nil { + return nil, fmt.Errorf("role tag is blacklisted") + } + + // Ensure that the policies on the RoleTag is a subset of policies on the role + if !strutil.StrListSubset(roleEntry.Policies, rTag.Policies) { + return nil, fmt.Errorf("policies on the role tag must be subset of policies on the role") + } + + return &roleTagLoginResponse{ + Policies: rTag.Policies, + MaxTTL: rTag.MaxTTL, + DisallowReauthentication: rTag.DisallowReauthentication, + }, nil +} + +// pathLoginRenew is used to renew an authenticated token. +func (b *backend) pathLoginRenew( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + instanceID := req.Auth.Metadata["instance_id"] + if instanceID == "" { + return nil, fmt.Errorf("unable to fetch instance ID from metadata during renewal") + } + + region := req.Auth.Metadata["region"] + if region == "" { + return nil, fmt.Errorf("unable to fetch region from metadata during renewal") + } + + // Cross check that the instance is still in 'running' state + _, err := b.validateInstance(req.Storage, instanceID, region) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil + } + + storedIdentity, err := whitelistIdentityEntry(req.Storage, instanceID) + if err != nil { + return nil, err + } + + // Ensure that role entry is not deleted. + roleEntry, err := b.awsRole(req.Storage, storedIdentity.Role) + if err != nil { + return nil, err + } + if roleEntry == nil { + return logical.ErrorResponse("role entry not found"), nil + } + + // If the login was made using the role tag, then max_ttl from tag + // is cached in internal data during login and used here to cap the + // max_ttl of renewal. + rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) + if err != nil { + return nil, err + } + + // Re-evaluate the maxTTL bounds. + shortestMaxTTL := b.System().MaxLeaseTTL() + longestMaxTTL := b.System().MaxLeaseTTL() + if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL { + shortestMaxTTL = roleEntry.MaxTTL + } + if roleEntry.MaxTTL > longestMaxTTL { + longestMaxTTL = roleEntry.MaxTTL + } + if rTagMaxTTL > time.Duration(0) && rTagMaxTTL < shortestMaxTTL { + shortestMaxTTL = rTagMaxTTL + } + if rTagMaxTTL > longestMaxTTL { + longestMaxTTL = rTagMaxTTL + } + + // Only LastUpdatedTime and ExpirationTime change and all other fields remain the same. + currentTime := time.Now().UTC() + storedIdentity.LastUpdatedTime = currentTime + storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL) + + if err = setWhitelistIdentityEntry(req.Storage, instanceID, storedIdentity); err != nil { + return nil, err + } + + return framework.LeaseExtend(req.Auth.TTL, shortestMaxTTL, b.System())(req, data) +} + +// Struct to represent items of interest from the EC2 instance identity document. +type identityDocument struct { + Tags map[string]interface{} `json:"tags,omitempty" structs:"tags" mapstructure:"tags"` + InstanceID string `json:"instanceId,omitempty" structs:"instanceId" mapstructure:"instanceId"` + AmiID string `json:"imageId,omitempty" structs:"imageId" mapstructure:"imageId"` + Region string `json:"region,omitempty" structs:"region" mapstructure:"region"` + PendingTime string `json:"pendingTime,omitempty" structs:"pendingTime" mapstructure:"pendingTime"` +} + +type roleTagLoginResponse struct { + Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` +} + +const pathLoginSyn = ` +Authenticates an EC2 instance with Vault. +` + +const pathLoginDesc = ` +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 +registered role is enabled, in which case client nonce is optional. + +First login attempt, creates a whitelist entry in Vault associating the instance to the nonce +provided. All future logins will succeed only if the client nonce matches the nonce in the +whitelisted entry. + +By default, a cron task will periodically look for expired entries in the whitelist +and deletes them. The duration to periodically run this, is one hour by default. +However, this can be configured using the 'config/tidy/identities' endpoint. This tidy +action can be triggered via the API as well, using the 'tidy/identities' endpoint. +` diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go new file mode 100644 index 0000000000..adb8698763 --- /dev/null +++ b/builtin/credential/aws/path_role.go @@ -0,0 +1,335 @@ +package aws + +import ( + "fmt" + "strings" + "time" + + "github.com/fatih/structs" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathRole(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "role/" + framework.GenericNameRegex("role"), + Fields: map[string]*framework.FieldSchema{ + "role": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role.", + }, + + "bound_ami_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `If set, defines a constraint on the EC2 instances that they should be +using the AMI ID specified by this parameter.`, + }, + + "role_tag": &framework.FieldSchema{ + 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.", + }, + + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 0, + Description: "The maximum allowed lifetime of tokens issued using this role.", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "default", + Description: "Policies to be set on tokens issued using this role.", + }, + + "allow_instance_migration": &framework.FieldSchema{ + 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.", + }, + + "disallow_reauthentication": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws/identity-whitelist/' endpoint.", + }, + }, + + ExistenceCheck: b.pathRoleExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathRoleCreateUpdate, + logical.UpdateOperation: b.pathRoleCreateUpdate, + logical.ReadOperation: b.pathRoleRead, + logical.DeleteOperation: b.pathRoleDelete, + }, + + HelpSynopsis: pathRoleSyn, + HelpDescription: pathRoleDesc, + } +} + +func pathListRole(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "role/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleList, + }, + + HelpSynopsis: pathListRolesHelpSyn, + HelpDescription: pathListRolesHelpDesc, + } +} + +func pathListRoles(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roles/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleList, + }, + + HelpSynopsis: pathListRolesHelpSyn, + HelpDescription: pathListRolesHelpDesc, + } +} + +// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. +// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. +func (b *backend) pathRoleExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + entry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role").(string))) + if err != nil { + return false, err + } + return entry != nil, nil +} + +// awsRole is used to get the information registered for the given AMI ID. +func (b *backend) awsRole(s logical.Storage, role string) (*awsRoleEntry, error) { + b.roleMutex.RLock() + defer b.roleMutex.RUnlock() + + return b.awsRoleInternal(s, role) +} + +func (b *backend) awsRoleInternal(s logical.Storage, role string) (*awsRoleEntry, error) { + entry, err := s.Get("role/" + strings.ToLower(role)) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result awsRoleEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +// pathRoleDelete is used to delete the information registered for a given AMI ID. +func (b *backend) pathRoleDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName := data.Get("role").(string) + if roleName == "" { + return logical.ErrorResponse("missing role"), nil + } + + b.roleMutex.Lock() + defer b.roleMutex.Unlock() + + return nil, req.Storage.Delete("role/" + strings.ToLower(roleName)) +} + +// pathRoleList is used to list all the AMI IDs registered with Vault. +func (b *backend) pathRoleList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.roleMutex.RLock() + defer b.roleMutex.RUnlock() + + roles, err := req.Storage.List("role/") + if err != nil { + return nil, err + } + return logical.ListResponse(roles), nil +} + +// pathRoleRead is used to view the information registered for a given AMI ID. +func (b *backend) pathRoleRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleEntry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role").(string))) + if err != nil { + return nil, err + } + if roleEntry == nil { + return nil, nil + } + + // Prepare the map of all the entries in the roleEntry. + respData := structs.New(roleEntry).Map() + + // HMAC key belonging to the role should NOT be exported. + delete(respData, "hmac_key") + + // Display the max_ttl in seconds. + respData["max_ttl"] = roleEntry.MaxTTL / time.Second + + return &logical.Response{ + Data: respData, + }, nil +} + +// pathRoleCreateUpdate is used to associate Vault policies to a given AMI ID. +func (b *backend) pathRoleCreateUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + roleName := strings.ToLower(data.Get("role").(string)) + if roleName == "" { + return logical.ErrorResponse("missing role"), nil + } + + b.roleMutex.Lock() + defer b.roleMutex.Unlock() + + roleEntry, err := b.awsRoleInternal(req.Storage, roleName) + if err != nil { + return nil, err + } + if roleEntry == nil { + roleEntry = &awsRoleEntry{} + } + + // Set the bound parameters only if they are supplied. + // There are no default values for bound parameters. + boundAmiIDStr, ok := data.GetOk("bound_ami_id") + if ok { + roleEntry.BoundAmiID = boundAmiIDStr.(string) + } + + // At least one bound parameter should be set. Currently, only + // 'bound_ami_id' is supported. Check if that is set. + if roleEntry.BoundAmiID == "" { + return logical.ErrorResponse("role is not bounded to any resource; set bound_ami_id"), nil + } + + policiesStr, ok := data.GetOk("policies") + if ok { + roleEntry.Policies = policyutil.ParsePolicies(policiesStr.(string)) + } else if req.Operation == logical.CreateOperation { + roleEntry.Policies = []string{"default"} + } + + disallowReauthenticationBool, ok := data.GetOk("disallow_reauthentication") + if ok { + roleEntry.DisallowReauthentication = disallowReauthenticationBool.(bool) + } else if req.Operation == logical.CreateOperation { + roleEntry.DisallowReauthentication = data.Get("disallow_reauthentication").(bool) + } + + allowInstanceMigrationBool, ok := data.GetOk("allow_instance_migration") + if ok { + roleEntry.AllowInstanceMigration = allowInstanceMigrationBool.(bool) + } else if req.Operation == logical.CreateOperation { + roleEntry.AllowInstanceMigration = data.Get("allow_instance_migration").(bool) + } + + var resp logical.Response + + maxTTLInt, ok := data.GetOk("max_ttl") + if ok { + maxTTL := time.Duration(maxTTLInt.(int)) * time.Second + systemMaxTTL := b.System().MaxLeaseTTL() + if maxTTL > systemMaxTTL { + resp.AddWarning(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds; TTL will be capped at login time", maxTTL/time.Second, systemMaxTTL/time.Second)) + } + + if maxTTL < time.Duration(0) { + return logical.ErrorResponse("max_ttl cannot be negative"), nil + } + + roleEntry.MaxTTL = maxTTL + } else if req.Operation == logical.CreateOperation { + roleEntry.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second + } + + roleTagStr, ok := data.GetOk("role_tag") + if ok { + 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 { + roleEntry.RoleTag = data.Get("role_tag").(string) + } + + if roleEntry.HMACKey == "" { + roleEntry.HMACKey, err = uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("failed to generate role HMAC key: %v", err) + } + } + + entry, err := logical.StorageEntryJSON("role/"+roleName, roleEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + if len(resp.Warnings()) == 0 { + return nil, nil + } + + return &resp, nil +} + +// Struct to hold the information associated with an AMI ID in Vault. +type awsRoleEntry struct { + BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"` + 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"` + MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` + Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` + HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"` +} + +const pathRoleSyn = ` +Create a role and associate policies to it. +` + +const pathRoleDesc = ` +A precondition for login is that a role should be created in the backend. +The login endpoint takes in the role name against which the instance +should be validated. After authenticating the instance, the authorization +for the instance to access Vault's resources is determined by the policies +that are associated to the role though this endpoint. + +When the instances require only a subset of policies on the role, then +'role_tag' option on the role can be enabled to create a role tag via the +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. + +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 +limit of the 'max_ttl' value on the backend's mount. +` + +const pathListRolesHelpSyn = ` +Lists all the roles that are registered with Vault. +` + +const pathListRolesHelpDesc = ` +Roles will be listed by their respective role names. +` diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go new file mode 100644 index 0000000000..4929bc07b8 --- /dev/null +++ b/builtin/credential/aws/path_role_tag.go @@ -0,0 +1,432 @@ +package aws + +import ( + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +const roleTagVersion = "v1" + +func pathRoleTag(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "role/" + framework.GenericNameRegex("role") + "/tag$", + Fields: map[string]*framework.FieldSchema{ + "role": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role.", + }, + + "instance_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Instance ID for which this tag is intended for. +If set, the created tag can only be used by the instance with the given ID.`, + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Policies to be associated with the tag. If set, must be a subset of the role's policies. If set, but set to an empty value, only the 'default' policy will be given to issued tokens.", + }, + + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 0, + Description: "If set, specifies the maximum allowed token lifetime.", + }, + + "allow_instance_migration": &framework.FieldSchema{ + 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.", + }, + + "disallow_reauthentication": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "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 the 'auth/aws/identity-whitelist/' endpoint.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathRoleTagUpdate, + }, + + HelpSynopsis: pathRoleTagSyn, + HelpDescription: pathRoleTagDesc, + } +} + +// pathRoleTagUpdate is used to create an EC2 instance tag which will +// identify the Vault resources that the instance will be authorized for. +func (b *backend) pathRoleTagUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + roleName := strings.ToLower(data.Get("role").(string)) + if roleName == "" { + return logical.ErrorResponse("missing role"), nil + } + + // Fetch the role entry + roleEntry, err := b.awsRole(req.Storage, roleName) + if err != nil { + return nil, err + } + if roleEntry == nil { + return logical.ErrorResponse(fmt.Sprintf("entry not found for role %s", roleName)), nil + } + + // If RoleTag is empty, disallow creation of tag. + if roleEntry.RoleTag == "" { + return logical.ErrorResponse("tag creation is not enabled for this role"), nil + } + + // There should be a HMAC key present in the role entry + if roleEntry.HMACKey == "" { + // Not being able to find the HMACKey is an internal error + return nil, fmt.Errorf("failed to find the HMAC key") + } + + resp := &logical.Response{} + + // Instance ID is an optional field. + instanceID := strings.ToLower(data.Get("instance_id").(string)) + + // If no policies field was not supplied, then the tag should inherit all the policies + // on the role. But, it was provided, but set to empty explicitly, only "default" policy + // should be inherited. So, by leaving the policies var unset to anything when it is not + // supplied, we ensure that it inherits all the policies on the role. + var policies []string + policiesStr, ok := data.GetOk("policies") + if ok { + policies = policyutil.ParsePolicies(policiesStr.(string)) + } + if !strutil.StrListSubset(roleEntry.Policies, policies) { + resp.AddWarning("Policies on the tag are not a subset of the policies set on the role. Login will not be allowed with this tag unless the role policies are updated.") + } + + // This is an optional field. + disallowReauthentication := data.Get("disallow_reauthentication").(bool) + + // This is an optional field. + allowInstanceMigration := data.Get("allow_instance_migration").(bool) + if allowInstanceMigration && !roleEntry.AllowInstanceMigration { + resp.AddWarning("Role does not allow instance migration. Login will not be allowed with this tag unless the role value is updated.") + } + + // max_ttl for the role tag should be less than the max_ttl set on the role. + maxTTL := time.Duration(data.Get("max_ttl").(int)) * time.Second + + // max_ttl on the tag should not be greater than the system view's max_ttl value. + if maxTTL > b.System().MaxLeaseTTL() { + resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the mount maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second)) + } + // If max_ttl is set for the role, check the bounds for tag's max_ttl value using that. + if roleEntry.MaxTTL != time.Duration(0) && maxTTL > roleEntry.MaxTTL { + resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the role maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, roleEntry.MaxTTL/time.Second)) + } + + if maxTTL < time.Duration(0) { + return logical.ErrorResponse("max_ttl cannot be negative"), nil + } + + // Create a random nonce. + nonce, err := createRoleTagNonce() + if err != nil { + return nil, err + } + + // Create a role tag out of all the information provided. + rTagValue, err := createRoleTagValue(&roleTag{ + Version: roleTagVersion, + Role: roleName, + Nonce: nonce, + Policies: policies, + MaxTTL: maxTTL, + InstanceID: instanceID, + DisallowReauthentication: disallowReauthentication, + AllowInstanceMigration: allowInstanceMigration, + }, roleEntry) + if err != nil { + return nil, err + } + + // Return the key to be used for the tag and the value to be used for that tag key. + // This key value pair should be set on the EC2 instance. + resp.Data = map[string]interface{}{ + "tag_key": roleEntry.RoleTag, + "tag_value": rTagValue, + } + + return resp, nil +} + +// createRoleTagValue prepares the plaintext version of the role tag, +// and appends a HMAC of the plaintext value to it, before returning. +func createRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (string, error) { + if rTag == nil { + return "", fmt.Errorf("nil role tag") + } + + if roleEntry == nil { + return "", fmt.Errorf("nil role entry") + } + + // Attach version, nonce, policies and maxTTL to the role tag value. + rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag) + if err != nil { + return "", err + } + + // Attach HMAC to tag's plaintext and return. + return appendHMAC(rTagPlaintext, roleEntry) +} + +// Takes in the plaintext part of the role tag, creates a HMAC of it and returns +// a role tag value containing both the plaintext part and the HMAC part. +func appendHMAC(rTagPlaintext string, roleEntry *awsRoleEntry) (string, error) { + if rTagPlaintext == "" { + return "", fmt.Errorf("empty role tag plaintext string") + } + + if roleEntry == nil { + return "", fmt.Errorf("nil role entry") + } + + // Create the HMAC of the value + hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext) + if err != nil { + return "", err + } + + // attach the HMAC to the value + rTagValue := fmt.Sprintf("%s:%s", rTagPlaintext, hmacB64) + + // This limit of 255 is enforced on the EC2 instance. Hence complying to that here. + if len(rTagValue) > 255 { + return "", fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") + } + + return rTagValue, nil +} + +// verifyRoleTagValue rebuilds the role tag's plaintext part, computes the HMAC +// from it using the role specific HMAC key and compares it with the received HMAC. +func verifyRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (bool, error) { + if rTag == nil { + return false, fmt.Errorf("nil role tag") + } + + if roleEntry == nil { + return false, fmt.Errorf("nil role entry") + } + + // Fetch the plaintext part of role tag + rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag) + if err != nil { + return false, err + } + + // Compute the HMAC of the plaintext + hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext) + if err != nil { + return false, err + } + + return subtle.ConstantTimeCompare([]byte(rTag.HMAC), []byte(hmacB64)) == 1, nil +} + +// prepareRoleTagPlaintextValue builds the role tag value without the HMAC in it. +func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { + if rTag == nil { + return "", fmt.Errorf("nil role tag") + } + if rTag.Version == "" { + return "", fmt.Errorf("missing version") + } + if rTag.Nonce == "" { + return "", fmt.Errorf("missing nonce") + } + if rTag.Role == "" { + return "", fmt.Errorf("missing role") + } + + // Attach Version, Nonce, Role, DisallowReauthentication and AllowInstanceMigration + // fields to the role tag. + value := fmt.Sprintf("%s:%s:r=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.Role, strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration)) + + // Attach the policies only if they are specified. + if len(rTag.Policies) != 0 { + value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) + } + + // Attach instance_id if set. + if rTag.InstanceID != "" { + value = fmt.Sprintf("%s:i=%s", value, rTag.InstanceID) + } + + // Attach max_ttl if it is provided. + if int(rTag.MaxTTL.Seconds()) > 0 { + value = fmt.Sprintf("%s:t=%d", value, int(rTag.MaxTTL.Seconds())) + } + + return value, nil +} + +// Parses the tag from string form into a struct form. This method +// also verifies the correctness of the parsed role tag. +func (b *backend) parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { + tagItems := strings.Split(tag, ":") + + // Tag must contain version, nonce, policies and HMAC + if len(tagItems) < 4 { + return nil, fmt.Errorf("invalid tag") + } + + rTag := &roleTag{} + + // Cache the HMAC value. The last item in the collection. + rTag.HMAC = tagItems[len(tagItems)-1] + + // Remove the HMAC from the list. + tagItems = tagItems[:len(tagItems)-1] + + // Version will be the first element. + rTag.Version = tagItems[0] + if rTag.Version != roleTagVersion { + return nil, fmt.Errorf("invalid role tag version") + } + + // Nonce will be the second element. + rTag.Nonce = tagItems[1] + + // Delete the version and nonce from the list. + tagItems = tagItems[2:] + + for _, tagItem := range tagItems { + var err error + switch { + case strings.Contains(tagItem, "i="): + rTag.InstanceID = strings.TrimPrefix(tagItem, "i=") + case strings.Contains(tagItem, "r="): + rTag.Role = strings.TrimPrefix(tagItem, "r=") + case strings.Contains(tagItem, "p="): + rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",") + case strings.Contains(tagItem, "d="): + rTag.DisallowReauthentication, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "d=")) + if err != nil { + return nil, err + } + case strings.Contains(tagItem, "m="): + rTag.AllowInstanceMigration, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "m=")) + if err != nil { + return nil, err + } + case strings.Contains(tagItem, "t="): + rTag.MaxTTL, err = time.ParseDuration(fmt.Sprintf("%ss", strings.TrimPrefix(tagItem, "t="))) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unrecognized item %s in tag", tagItem) + } + } + + if rTag.Role == "" { + return nil, fmt.Errorf("missing role name") + } + + roleEntry, err := b.awsRole(s, rTag.Role) + if err != nil { + return nil, err + } + if roleEntry == nil { + return nil, fmt.Errorf("entry not found for %s", rTag.Role) + } + + // Create a HMAC of the plaintext value of role tag and compare it with the given value. + verified, err := verifyRoleTagValue(rTag, roleEntry) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("role tag signature verification failed") + } + + return rTag, nil +} + +// Creates base64 encoded HMAC using a per-role key. +func createRoleTagHMACBase64(key, value string) (string, error) { + if key == "" { + return "", fmt.Errorf("invalid HMAC key") + } + hm := hmac.New(sha256.New, []byte(key)) + hm.Write([]byte(value)) + + // base64 encode the hmac bytes. + return base64.StdEncoding.EncodeToString(hm.Sum(nil)), nil +} + +// Creates a base64 encoded random nonce. +func createRoleTagNonce() (string, error) { + if uuidBytes, err := uuid.GenerateRandomBytes(8); err != nil { + return "", err + } else { + return base64.StdEncoding.EncodeToString(uuidBytes), nil + } +} + +// Struct roleTag represents a role tag in a struc form. +type roleTag struct { + Version string `json:"version" structs:"version" mapstructure:"version"` + InstanceID string `json:"instance_id" structs:"instance_id" mapstructure:"instance_id"` + Nonce string `json:"nonce" structs:"nonce" mapstructure:"nonce"` + Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` + Role string `json:"role" structs:"role" mapstructure:"role"` + HMAC string `json:"hmac" structs:"hmac" mapstructure:"hmac"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` + AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` +} + +func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { + return rTag1 != nil && + rTag2 != nil && + rTag1.Version == rTag2.Version && + rTag1.Nonce == rTag2.Nonce && + policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && + rTag1.MaxTTL == rTag2.MaxTTL && + rTag1.Role == rTag2.Role && + rTag1.HMAC == rTag2.HMAC && + rTag1.InstanceID == rTag2.InstanceID && + rTag1.DisallowReauthentication == rTag2.DisallowReauthentication && + rTag1.AllowInstanceMigration == rTag2.AllowInstanceMigration +} + +const pathRoleTagSyn = ` +Create a tag on a role in order to be able to further restrict the capabilities of a role. +` + +const pathRoleTagDesc = ` +If there are needs to apply only a subset of role's capabilities to any specific +instance, create a role tag using this endpoint and attach the tag on the instance +before performing login. + +To be able to create a role tag, the 'role_tag' option on the role should be +enabled via the endpoint 'role/'. Also, the policies to be associated +with the tag should be a subset of the policies associated with the registered role. + +This endpoint will return both the 'key' and the 'value' of the tag to be set +on the EC2 instance. +` diff --git a/builtin/credential/aws/path_roletag_blacklist.go b/builtin/credential/aws/path_roletag_blacklist.go new file mode 100644 index 0000000000..ff7362449d --- /dev/null +++ b/builtin/credential/aws/path_roletag_blacklist.go @@ -0,0 +1,255 @@ +package aws + +import ( + "encoding/base64" + "time" + + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathRoletagBlacklist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roletag-blacklist/(?P.*)", + Fields: map[string]*framework.FieldSchema{ + "role_tag": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Role tag to be blacklisted. The tag can be supplied as-is. In order +to avoid any encoding problems, it can be base64 encoded.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathRoletagBlacklistUpdate, + logical.ReadOperation: b.pathRoletagBlacklistRead, + logical.DeleteOperation: b.pathRoletagBlacklistDelete, + }, + + HelpSynopsis: pathRoletagBlacklistSyn, + HelpDescription: pathRoletagBlacklistDesc, + } +} + +// Path to list all the blacklisted tags. +func pathListRoletagBlacklist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roletag-blacklist/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoletagBlacklistsList, + }, + + HelpSynopsis: pathListRoletagBlacklistHelpSyn, + HelpDescription: pathListRoletagBlacklistHelpDesc, + } +} + +// Lists all the blacklisted role tags. +func (b *backend) pathRoletagBlacklistsList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.blacklistMutex.RLock() + defer b.blacklistMutex.RUnlock() + + tags, err := req.Storage.List("blacklist/roletag/") + if err != nil { + return nil, err + } + + // Tags are base64 encoded before indexing to avoid problems + // with the path separators being present in the tag. + // Reverse it before returning the list response. + for i, keyB64 := range tags { + if key, err := base64.StdEncoding.DecodeString(keyB64); err != nil { + return nil, err + } else { + // Overwrite the result with the decoded string. + tags[i] = string(key) + } + } + return logical.ListResponse(tags), nil +} + +// Fetch an entry from the role tag blacklist for a given tag. +// This method takes a role tag in its original form and not a base64 encoded form. +func (b *backend) blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { + b.blacklistMutex.RLock() + defer b.blacklistMutex.RUnlock() + + return b.blacklistRoleTagEntryInternal(s, tag) +} + +func (b *backend) blacklistRoleTagEntryInternal(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { + entry, err := s.Get("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag))) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result roleTagBlacklistEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +// Deletes an entry from the role tag blacklist for a given tag. +func (b *backend) pathRoletagBlacklistDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.blacklistMutex.Lock() + defer b.blacklistMutex.Unlock() + + tag := data.Get("role_tag").(string) + if tag == "" { + return logical.ErrorResponse("missing role_tag"), nil + } + + return nil, req.Storage.Delete("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag))) +} + +// If the given role tag is blacklisted, returns the details of the blacklist entry. +// Returns 'nil' otherwise. +func (b *backend) pathRoletagBlacklistRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + tag := data.Get("role_tag").(string) + if tag == "" { + return logical.ErrorResponse("missing role_tag"), nil + } + + entry, err := b.blacklistRoleTagEntry(req.Storage, tag) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + return &logical.Response{ + Data: structs.New(entry).Map(), + }, nil +} + +// pathRoletagBlacklistUpdate is used to blacklist a given role tag. +// Before a role tag is blacklisted, the correctness of the plaintext part +// in the role tag is verified using the associated HMAC. +func (b *backend) pathRoletagBlacklistUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + // The role_tag value provided, optionally can be base64 encoded. + tagInput := data.Get("role_tag").(string) + if tagInput == "" { + return logical.ErrorResponse("missing role_tag"), nil + } + + tag := "" + + // Try to base64 decode the value. + tagBytes, err := base64.StdEncoding.DecodeString(tagInput) + if err != nil { + // If the decoding failed, use the value as-is. + tag = tagInput + } else { + // If the decoding succeeded, use the decoded value. + tag = string(tagBytes) + } + + // Parse and verify the role tag from string form to a struct form and verify it. + rTag, err := b.parseAndVerifyRoleTagValue(req.Storage, tag) + if err != nil { + return nil, err + } + if rTag == nil { + return logical.ErrorResponse("failed to verify the role tag and parse it"), nil + } + + // Get the entry for the role mentioned in the role tag. + roleEntry, err := b.awsRole(req.Storage, rTag.Role) + if err != nil { + return nil, err + } + if roleEntry == nil { + return logical.ErrorResponse("role entry not found"), nil + } + + b.blacklistMutex.Lock() + defer b.blacklistMutex.Unlock() + + // Check if the role tag is already blacklisted. If yes, update it. + blEntry, err := b.blacklistRoleTagEntryInternal(req.Storage, tag) + if err != nil { + return nil, err + } + if blEntry == nil { + blEntry = &roleTagBlacklistEntry{} + } + + currentTime := time.Now().UTC() + + // Check if this is a creation of blacklist entry. + if blEntry.CreationTime.IsZero() { + // Set the creation time for the blacklist entry. + // This should not be updated after setting it once. + // If blacklist operation is invoked more than once, only update the expiration time. + blEntry.CreationTime = currentTime + } + + // Decide the expiration time based on the max_ttl values. Since this is + // restricting access, use the greatest duration, not the least. + maxDur := rTag.MaxTTL + if roleEntry.MaxTTL > maxDur { + maxDur = roleEntry.MaxTTL + } + if b.System().MaxLeaseTTL() > maxDur { + maxDur = b.System().MaxLeaseTTL() + } + + blEntry.ExpirationTime = currentTime.Add(maxDur) + + entry, err := logical.StorageEntryJSON("blacklist/roletag/"+base64.StdEncoding.EncodeToString([]byte(tag)), blEntry) + if err != nil { + return nil, err + } + + // Store the blacklist entry. + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +type roleTagBlacklistEntry struct { + CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` + ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` +} + +const pathRoletagBlacklistSyn = ` +Blacklist a previously created role tag. +` + +const pathRoletagBlacklistDesc = ` +Blacklist a role tag so that it cannot be used by any EC2 instance to perform further +logins. This can be used if the role tag is suspected or believed to be possessed by +an unintended party. + +By default, a cron task will periodically look for expired entries in the blacklist +and deletes them. The duration to periodically run this, is one hour by default. +However, this can be configured using the 'config/tidy/roletags' endpoint. This tidy +action can be triggered via the API as well, using the 'tidy/roletags' endpoint. + +Also note that delete operation is supported on this endpoint to remove specific +entries from the blacklist. +` + +const pathListRoletagBlacklistHelpSyn = ` +Lists the blacklisted role tags. +` + +const pathListRoletagBlacklistHelpDesc = ` +Lists all the entries present in the blacklist. This will show both the valid +entries and the expired entries in the blacklist. Use 'tidy/roletags' endpoint +to clean-up the blacklist of role tags based on expiration time. +` diff --git a/builtin/credential/aws/path_tidy_identity_whitelist.go b/builtin/credential/aws/path_tidy_identity_whitelist.go new file mode 100644 index 0000000000..58c6353787 --- /dev/null +++ b/builtin/credential/aws/path_tidy_identity_whitelist.go @@ -0,0 +1,96 @@ +package aws + +import ( + "fmt" + "sync/atomic" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathTidyIdentityWhitelist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "tidy/identity-whitelist$", + Fields: map[string]*framework.FieldSchema{ + "safety_buffer": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 259200, + Description: `The amount of extra time that must have passed beyond the identity's +expiration, before it is removed from the backend storage.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathTidyIdentityWhitelistUpdate, + }, + + HelpSynopsis: pathTidyIdentityWhitelistSyn, + HelpDescription: pathTidyIdentityWhitelistDesc, + } +} + +// tidyWhitelistIdentity is used to delete entries in the whitelist that are expired. +func (b *backend) tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error { + grabbed := atomic.CompareAndSwapUint32(&b.tidyWhitelistCASGuard, 0, 1) + if grabbed { + defer atomic.StoreUint32(&b.tidyWhitelistCASGuard, 0) + } else { + return fmt.Errorf("identity whitelist tidy operation already running") + } + + bufferDuration := time.Duration(safety_buffer) * time.Second + + identities, err := s.List("whitelist/identity/") + if err != nil { + return err + } + + for _, instanceID := range identities { + identityEntry, err := s.Get("whitelist/identity/" + instanceID) + if err != nil { + return fmt.Errorf("error fetching identity of instanceID %s: %s", instanceID, err) + } + + if identityEntry == nil { + return fmt.Errorf("identity entry for instanceID %s is nil", instanceID) + } + + if identityEntry.Value == nil || len(identityEntry.Value) == 0 { + return fmt.Errorf("found identity entry for instanceID %s but actual identity is empty", instanceID) + } + + var result whitelistIdentity + if err := identityEntry.DecodeJSON(&result); err != nil { + return err + } + + if time.Now().UTC().After(result.ExpirationTime.Add(bufferDuration)) { + if err := s.Delete("whitelist/identity" + instanceID); err != nil { + return fmt.Errorf("error deleting identity of instanceID %s from storage: %s", instanceID, err) + } + } + } + + return nil +} + +// pathTidyIdentityWhitelistUpdate is used to delete entries in the whitelist that are expired. +func (b *backend) pathTidyIdentityWhitelistUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return nil, b.tidyWhitelistIdentity(req.Storage, data.Get("safety_buffer").(int)) +} + +const pathTidyIdentityWhitelistSyn = ` +Clean-up the whitelist instance identity entries. +` + +const pathTidyIdentityWhitelistDesc = ` +When an instance identity is whitelisted, the expiration time of the whitelist +entry is set based on the maximum 'max_ttl' value set on: the role, the role tag +and the backend's mount. + +When this endpoint is invoked, all the entries that are expired will be deleted. +A 'safety_buffer' (duration in seconds) can be provided, to ensure deletion of +only those entries that are expired before 'safety_buffer' seconds. +` diff --git a/builtin/credential/aws/path_tidy_roletag_blacklist.go b/builtin/credential/aws/path_tidy_roletag_blacklist.go new file mode 100644 index 0000000000..6856b3473e --- /dev/null +++ b/builtin/credential/aws/path_tidy_roletag_blacklist.go @@ -0,0 +1,95 @@ +package aws + +import ( + "fmt" + "sync/atomic" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathTidyRoletagBlacklist(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "tidy/roletag-blacklist$", + Fields: map[string]*framework.FieldSchema{ + "safety_buffer": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 259200, // 72h + Description: `The amount of extra time that must have passed beyond the roletag +expiration, before it is removed from the backend storage.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathTidyRoletagBlacklistUpdate, + }, + + HelpSynopsis: pathTidyRoletagBlacklistSyn, + HelpDescription: pathTidyRoletagBlacklistDesc, + } +} + +// tidyBlacklistRoleTag is used to clean-up the entries in the role tag blacklist. +func (b *backend) tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error { + grabbed := atomic.CompareAndSwapUint32(&b.tidyBlacklistCASGuard, 0, 1) + if grabbed { + defer atomic.StoreUint32(&b.tidyBlacklistCASGuard, 0) + } else { + return fmt.Errorf("roletag blacklist tidy operation already running") + } + + bufferDuration := time.Duration(safety_buffer) * time.Second + tags, err := s.List("blacklist/roletag/") + if err != nil { + return err + } + + for _, tag := range tags { + tagEntry, err := s.Get("blacklist/roletag/" + tag) + if err != nil { + return fmt.Errorf("error fetching tag %s: %s", tag, err) + } + + if tagEntry == nil { + return fmt.Errorf("tag entry for tag %s is nil", tag) + } + + if tagEntry.Value == nil || len(tagEntry.Value) == 0 { + return fmt.Errorf("found entry for tag %s but actual tag is empty", tag) + } + + var result roleTagBlacklistEntry + if err := tagEntry.DecodeJSON(&result); err != nil { + return err + } + + if time.Now().UTC().After(result.ExpirationTime.Add(bufferDuration)) { + if err := s.Delete("blacklist/roletag" + tag); err != nil { + return fmt.Errorf("error deleting tag %s from storage: %s", tag, err) + } + } + } + + return nil +} + +// pathTidyRoletagBlacklistUpdate is used to clean-up the entries in the role tag blacklist. +func (b *backend) pathTidyRoletagBlacklistUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return nil, b.tidyBlacklistRoleTag(req.Storage, data.Get("safety_buffer").(int)) +} + +const pathTidyRoletagBlacklistSyn = ` +Clean-up the blacklist role tag entries. +` + +const pathTidyRoletagBlacklistDesc = ` +When a role tag is blacklisted, the expiration time of the blacklist entry is +set based on the maximum 'max_ttl' value set on: the role, the role tag and the +backend's mount. + +When this endpoint is invoked, all the entries that are expired will be deleted. +A 'safety_buffer' (duration in seconds) can be provided, to ensure deletion of +only those entries that are expired before 'safety_buffer' seconds. +` diff --git a/builtin/logical/aws/backend.go b/builtin/logical/aws/backend.go index 8d9614d609..721a4c382a 100644 --- a/builtin/logical/aws/backend.go +++ b/builtin/logical/aws/backend.go @@ -35,8 +35,8 @@ func Backend() *framework.Backend { secretAccessKeys(&b), }, - Rollback: rollback, - RollbackMinAge: 5 * time.Minute, + WALRollback: walRollback, + WALRollbackMinAge: 5 * time.Minute, } return b.Backend diff --git a/builtin/logical/aws/rollback.go b/builtin/logical/aws/rollback.go index 8f133396fe..5d1b335ed0 100644 --- a/builtin/logical/aws/rollback.go +++ b/builtin/logical/aws/rollback.go @@ -7,12 +7,12 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -var rollbackMap = map[string]framework.RollbackFunc{ +var walRollbackMap = map[string]framework.WALRollbackFunc{ "user": pathUserRollback, } -func rollback(req *logical.Request, kind string, data interface{}) error { - f, ok := rollbackMap[kind] +func walRollback(req *logical.Request, kind string, data interface{}) error { + f, ok := walRollbackMap[kind] if !ok { return fmt.Errorf("unknown type to rollback") } diff --git a/cli/commands.go b/cli/commands.go index 497b1697ea..a2f84a691b 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/vault/version" credAppId "github.com/hashicorp/vault/builtin/credential/app-id" + 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" @@ -63,6 +64,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { }, CredentialBackends: map[string]logical.Factory{ "cert": credCert.Factory, + "aws": credAws.Factory, "app-id": credAppId.Factory, "github": credGitHub.Factory, "userpass": credUserpass.Factory, diff --git a/logical/framework/backend.go b/logical/framework/backend.go index 92fccbc581..9f274fd22d 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -42,14 +42,24 @@ type Backend struct { // and ease specifying callbacks for revocation, renewal, etc. Secrets []*Secret - // Rollback is called when a WAL entry (see wal.go) has to be rolled + // PeriodicFunc is the callback, which if set, will be invoked when the + // periodic timer of RollbackManager ticks. This can be used by + // backends to do anything it wishes to do periodically. + // + // PeriodicFunc can be invoked to, say to periodically delete stale + // entries in backend's storage, while the backend is still being used. + // (Note the different of this action from what `Clean` does, which is + // invoked just before the backend is unmounted). + PeriodicFunc periodicFunc + + // WALRollback is called when a WAL entry (see wal.go) has to be rolled // back. It is called with the data from the entry. // - // RollbackMinAge is the minimum age of a WAL entry before it is attempted + // WALRollbackMinAge is the minimum age of a WAL entry before it is attempted // to be rolled back. This should be longer than the maximum time it takes // to successfully create a secret. - Rollback RollbackFunc - RollbackMinAge time.Duration + WALRollback WALRollbackFunc + WALRollbackMinAge time.Duration // Clean is called on unload to clean up e.g any existing connections // to the backend, if required. @@ -66,11 +76,15 @@ type Backend struct { pathsRe []*regexp.Regexp } +// periodicFunc is the callback called when the RollbackManager's timer ticks. +// This can be utilized by the backends to do anything it wants. +type periodicFunc func(*logical.Request) error + // OperationFunc is the callback called for an operation on a path. type OperationFunc func(*logical.Request, *FieldData) (*logical.Response, error) -// RollbackFunc is the callback for rollbacks. -type RollbackFunc func(*logical.Request, string, interface{}) error +// WALRollbackFunc is the callback for rollbacks. +type WALRollbackFunc func(*logical.Request, string, interface{}) error // CleanupFunc is the callback for backend unload. type CleanupFunc func() @@ -394,6 +408,19 @@ func (b *Backend) handleRevokeRenew( } } +// handleRollback invokes the PeriodicFunc set on the backend. It also does a WAL rollback operation. +func (b *Backend) handleRollback( + req *logical.Request) (*logical.Response, error) { + // Response is not expected from the periodic operation. + if b.PeriodicFunc != nil { + if err := b.PeriodicFunc(req); err != nil { + return nil, err + } + } + + return b.handleWALRollback(req) +} + func (b *Backend) handleAuthRenew(req *logical.Request) (*logical.Response, error) { if b.AuthRenew == nil { return logical.ErrorResponse("this auth type doesn't support renew"), nil @@ -402,9 +429,9 @@ func (b *Backend) handleAuthRenew(req *logical.Request) (*logical.Response, erro return b.AuthRenew(req, nil) } -func (b *Backend) handleRollback( +func (b *Backend) handleWALRollback( req *logical.Request) (*logical.Response, error) { - if b.Rollback == nil { + if b.WALRollback == nil { return nil, logical.ErrUnsupportedOperation } @@ -419,7 +446,7 @@ func (b *Backend) handleRollback( // Calculate the minimum time that the WAL entries could be // created in order to be rolled back. - age := b.RollbackMinAge + age := b.WALRollbackMinAge if age == 0 { age = 10 * time.Minute } @@ -443,8 +470,8 @@ func (b *Backend) handleRollback( continue } - // Attempt a rollback - err = b.Rollback(req, entry.Kind, entry.Data) + // Attempt a WAL rollback + err = b.WALRollback(req, entry.Kind, entry.Data) if err != nil { err = fmt.Errorf( "Error rolling back '%s' entry: %s", entry.Kind, err) diff --git a/logical/framework/backend_test.go b/logical/framework/backend_test.go index 946c7a90a0..a3b34d7ca3 100644 --- a/logical/framework/backend_test.go +++ b/logical/framework/backend_test.go @@ -314,8 +314,8 @@ func TestBackendHandleRequest_rollback(t *testing.T) { } b := &Backend{ - Rollback: callback, - RollbackMinAge: 1 * time.Millisecond, + WALRollback: callback, + WALRollbackMinAge: 1 * time.Millisecond, } storage := new(logical.InmemStorage) @@ -349,8 +349,8 @@ func TestBackendHandleRequest_rollbackMinAge(t *testing.T) { } b := &Backend{ - Rollback: callback, - RollbackMinAge: 5 * time.Second, + WALRollback: callback, + WALRollbackMinAge: 5 * time.Second, } storage := new(logical.InmemStorage) diff --git a/vault/rollback.go b/vault/rollback.go index 443a2c0ba0..1c13d6f7c2 100644 --- a/vault/rollback.go +++ b/vault/rollback.go @@ -30,10 +30,10 @@ const ( type RollbackManager struct { logger *log.Logger - // This gives the current mount table, plus a RWMutex that is - // locked for reading. It is up to the caller to RUnlock it - // when done with the mount table - mounts func() []*MountEntry + // This gives the current mount table of both logical and credential backends, + // plus a RWMutex that is locked for reading. It is up to the caller to RUnlock + // it when done with the mount table. + backends func() []*MountEntry router *Router period time.Duration @@ -55,10 +55,10 @@ type rollbackState struct { } // NewRollbackManager is used to create a new rollback manager -func NewRollbackManager(logger *log.Logger, mounts func() []*MountEntry, router *Router) *RollbackManager { +func NewRollbackManager(logger *log.Logger, backendsFunc func() []*MountEntry, router *Router) *RollbackManager { r := &RollbackManager{ logger: logger, - mounts: mounts, + backends: backendsFunc, router: router, period: rollbackPeriod, inflight: make(map[string]*rollbackState), @@ -109,9 +109,9 @@ func (m *RollbackManager) triggerRollbacks() { m.inflightLock.Lock() defer m.inflightLock.Unlock() - mounts := m.mounts() + backends := m.backends() - for _, e := range mounts { + for _, e := range backends { if _, ok := m.inflight[e.Path]; !ok { m.startRollback(e.Path) } @@ -184,16 +184,24 @@ func (m *RollbackManager) Rollback(path string) error { // startRollback is used to start the rollback manager after unsealing func (c *Core) startRollback() error { - mountsFunc := func() []*MountEntry { + backendsFunc := func() []*MountEntry { ret := []*MountEntry{} c.mountsLock.RLock() defer c.mountsLock.RUnlock() for _, entry := range c.mounts.Entries { ret = append(ret, entry) } + c.authLock.RLock() + defer c.authLock.RUnlock() + for _, entry := range c.auth.Entries { + if !strings.HasPrefix(entry.Path, "auth/") { + entry.Path = "auth/" + entry.Path + } + ret = append(ret, entry) + } return ret } - c.rollback = NewRollbackManager(c.logger, mountsFunc, c.router) + c.rollback = NewRollbackManager(c.logger, backendsFunc, c.router) c.rollback.Start() return nil } diff --git a/vendor/github.com/fullsailor/pkcs7/.gitignore b/vendor/github.com/fullsailor/pkcs7/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/fullsailor/pkcs7/LICENSE b/vendor/github.com/fullsailor/pkcs7/LICENSE new file mode 100644 index 0000000000..75f3209085 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Andrew Smith + +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/fullsailor/pkcs7/README.md b/vendor/github.com/fullsailor/pkcs7/README.md new file mode 100644 index 0000000000..7ec77b5192 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/README.md @@ -0,0 +1,7 @@ +# pkcs7 + +[![GoDoc](https://godoc.org/github.com/fullsailor/pkcs7?status.svg)](https://godoc.org/github.com/fullsailor/pkcs7) + +pkcs7 implements parsing and creating signed and enveloped messages. + +- Documentation on [GoDoc](http://godoc.org/github.com/fullsailor/pkcs7) diff --git a/vendor/github.com/fullsailor/pkcs7/ber.go b/vendor/github.com/fullsailor/pkcs7/ber.go new file mode 100644 index 0000000000..45924baff3 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/ber.go @@ -0,0 +1,228 @@ +package pkcs7 + +import ( + "bytes" + "errors" +) + +var encodeIndent = 0 + +type asn1Object interface { + EncodeTo(writer *bytes.Buffer) error +} + +type asn1Structured struct { + tagBytes []byte + content []asn1Object +} + +func (s asn1Structured) EncodeTo(out *bytes.Buffer) error { + //fmt.Printf("%s--> tag: % X\n", strings.Repeat("| ", encodeIndent), s.tagBytes) + encodeIndent++ + inner := new(bytes.Buffer) + for _, obj := range s.content { + err := obj.EncodeTo(inner) + if err != nil { + return err + } + } + encodeIndent-- + out.Write(s.tagBytes) + encodeLength(out, inner.Len()) + out.Write(inner.Bytes()) + return nil +} + +type asn1Primitive struct { + tagBytes []byte + length int + content []byte +} + +func (p asn1Primitive) EncodeTo(out *bytes.Buffer) error { + _, err := out.Write(p.tagBytes) + if err != nil { + return err + } + if err = encodeLength(out, p.length); err != nil { + return err + } + //fmt.Printf("%s--> tag: % X length: %d\n", strings.Repeat("| ", encodeIndent), p.tagBytes, p.length) + //fmt.Printf("%s--> content length: %d\n", strings.Repeat("| ", encodeIndent), len(p.content)) + out.Write(p.content) + + return nil +} + +func ber2der(ber []byte) ([]byte, error) { + if len(ber) == 0 { + return nil, errors.New("ber2der: input ber is empty") + } + //fmt.Printf("--> ber2der: Transcoding %d bytes\n", len(ber)) + out := new(bytes.Buffer) + + obj, _, err := readObject(ber, 0) + if err != nil { + return nil, err + } + obj.EncodeTo(out) + + // if offset < len(ber) { + // return nil, fmt.Errorf("ber2der: Content longer than expected. Got %d, expected %d", offset, len(ber)) + //} + + return out.Bytes(), nil +} + +// encodes lengths that are longer than 127 into string of bytes +func marshalLongLength(out *bytes.Buffer, i int) (err error) { + n := lengthLength(i) + + for ; n > 0; n-- { + err = out.WriteByte(byte(i >> uint((n-1)*8))) + if err != nil { + return + } + } + + return nil +} + +// computes the byte length of an encoded length value +func lengthLength(i int) (numBytes int) { + numBytes = 1 + for i > 255 { + numBytes++ + i >>= 8 + } + return +} + +// encodes the length in DER format +// If the length fits in 7 bits, the value is encoded directly. +// +// Otherwise, the number of bytes to encode the length is first determined. +// This number is likely to be 4 or less for a 32bit length. This number is +// added to 0x80. The length is encoded in big endian encoding follow after +// +// Examples: +// length | byte 1 | bytes n +// 0 | 0x00 | - +// 120 | 0x78 | - +// 200 | 0x81 | 0xC8 +// 500 | 0x82 | 0x01 0xF4 +// +func encodeLength(out *bytes.Buffer, length int) (err error) { + if length >= 128 { + l := lengthLength(length) + err = out.WriteByte(0x80 | byte(l)) + if err != nil { + return + } + err = marshalLongLength(out, length) + if err != nil { + return + } + } else { + err = out.WriteByte(byte(length)) + if err != nil { + return + } + } + return +} + +func readObject(ber []byte, offset int) (asn1Object, int, error) { + //fmt.Printf("\n====> Starting readObject at offset: %d\n\n", offset) + tagStart := offset + b := ber[offset] + offset++ + tag := b & 0x1F // last 5 bits + if tag == 0x1F { + tag = 0 + for ber[offset] >= 0x80 { + tag = tag*128 + ber[offset] - 0x80 + offset++ + } + tag = tag*128 + ber[offset] - 0x80 + offset++ + } + tagEnd := offset + + kind := b & 0x20 + /* + if kind == 0 { + fmt.Print("--> Primitive\n") + } else { + fmt.Print("--> Constructed\n") + } + */ + // read length + var length int + l := ber[offset] + offset++ + hack := 0 + if l > 0x80 { + numberOfBytes := (int)(l & 0x7F) + if numberOfBytes > 4 { // int is only guaranteed to be 32bit + return nil, 0, errors.New("ber2der: BER tag length too long") + } + if numberOfBytes == 4 && (int)(ber[offset]) > 0x7F { + return nil, 0, errors.New("ber2der: BER tag length is negative") + } + if 0x0 == (int)(ber[offset]) { + return nil, 0, errors.New("ber2der: BER tag length has leading zero") + } + //fmt.Printf("--> (compute length) indicator byte: %x\n", l) + //fmt.Printf("--> (compute length) length bytes: % X\n", ber[offset:offset+numberOfBytes]) + for i := 0; i < numberOfBytes; i++ { + length = length*256 + (int)(ber[offset]) + offset++ + } + } else if l == 0x80 { + // find length by searching content + markerIndex := bytes.LastIndex(ber[offset:], []byte{0x0, 0x0}) + if markerIndex == -1 { + return nil, 0, errors.New("ber2der: Invalid BER format") + } + length = markerIndex + hack = 2 + //fmt.Printf("--> (compute length) marker found at offset: %d\n", markerIndex+offset) + } else { + length = (int)(l) + } + + //fmt.Printf("--> length : %d\n", length) + contentEnd := offset + length + if contentEnd > len(ber) { + return nil, 0, errors.New("ber2der: BER tag length is more than available data") + } + //fmt.Printf("--> content start : %d\n", offset) + //fmt.Printf("--> content end : %d\n", contentEnd) + //fmt.Printf("--> content : % X\n", ber[offset:contentEnd]) + var obj asn1Object + if kind == 0 { + obj = asn1Primitive{ + tagBytes: ber[tagStart:tagEnd], + length: length, + content: ber[offset:contentEnd], + } + } else { + var subObjects []asn1Object + for offset < contentEnd { + var subObj asn1Object + var err error + subObj, offset, err = readObject(ber[:contentEnd], offset) + if err != nil { + return nil, 0, err + } + subObjects = append(subObjects, subObj) + } + obj = asn1Structured{ + tagBytes: ber[tagStart:tagEnd], + content: subObjects, + } + } + + return obj, contentEnd + hack, nil +} diff --git a/vendor/github.com/fullsailor/pkcs7/ber_test.go b/vendor/github.com/fullsailor/pkcs7/ber_test.go new file mode 100644 index 0000000000..32dc88a4a7 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/ber_test.go @@ -0,0 +1,61 @@ +package pkcs7 + +import ( + "bytes" + "encoding/asn1" + "strings" + "testing" +) + +func TestBer2Der(t *testing.T) { + // indefinite length fixture + ber := []byte{0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00} + expected := []byte{0x30, 0x03, 0x02, 0x01, 0x01} + der, err := ber2der(ber) + if err != nil { + t.Fatalf("ber2der failed with error: %v", err) + } + if bytes.Compare(der, expected) != 0 { + t.Errorf("ber2der result did not match.\n\tExpected: % X\n\tActual: % X", expected, der) + } + + if der2, err := ber2der(der); err != nil { + t.Errorf("ber2der on DER bytes failed with error: %v", err) + } else { + if !bytes.Equal(der, der2) { + t.Error("ber2der is not idempotent") + } + } + var thing struct { + Number int + } + rest, err := asn1.Unmarshal(der, &thing) + if err != nil { + t.Errorf("Cannot parse resulting DER because: %v", err) + } else if len(rest) > 0 { + t.Errorf("Resulting DER has trailing data: % X", rest) + } +} + +func TestBer2Der_Negatives(t *testing.T) { + fixtures := []struct { + Input []byte + ErrorContains string + }{ + {[]byte{0x30, 0x85}, "length too long"}, + {[]byte{0x30, 0x84, 0x80, 0x0, 0x0, 0x0}, "length is negative"}, + {[]byte{0x30, 0x82, 0x0, 0x1}, "length has leading zero"}, + {[]byte{0x30, 0x80, 0x1, 0x2}, "Invalid BER format"}, + {[]byte{0x30, 0x03, 0x01, 0x02}, "length is more than available data"}, + } + + for _, fixture := range fixtures { + _, err := ber2der(fixture.Input) + if err == nil { + t.Errorf("No error thrown. Expected: %s", fixture.ErrorContains) + } + if !strings.Contains(err.Error(), fixture.ErrorContains) { + t.Errorf("Unexpected error thrown.\n\tExpected: /%s/\n\tActual: %s", fixture.ErrorContains, err.Error()) + } + } +} diff --git a/vendor/github.com/fullsailor/pkcs7/pkcs7.go b/vendor/github.com/fullsailor/pkcs7/pkcs7.go new file mode 100644 index 0000000000..c5ee084fe8 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/pkcs7.go @@ -0,0 +1,776 @@ +// Package pkcs7 implements parsing and generation of some PKCS#7 structures. +package pkcs7 + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/des" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "math/big" + "sort" + "time" + + _ "crypto/sha1" // for crypto.SHA1 +) + +// PKCS7 Represents a PKCS7 structure +type PKCS7 struct { + Content []byte + Certificates []*x509.Certificate + CRLs []pkix.CertificateList + Signers []signerInfo + raw interface{} +} + +type contentInfo struct { + ContentType asn1.ObjectIdentifier + Content asn1.RawValue `asn1:"explicit,optional,tag:0"` +} + +// ErrUnsupportedContentType is returned when a PKCS7 content is not supported. +// Currently only Data (1.2.840.113549.1.7.1), Signed Data (1.2.840.113549.1.7.2), +// and Enveloped Data are supported (1.2.840.113549.1.7.3) +var ErrUnsupportedContentType = errors.New("pkcs7: cannot parse data: unimplemented content type") + +type unsignedData []byte + +var ( + oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1} + oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2} + oidEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 3} + oidSignedAndEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 4} + oidDigestedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 5} + oidEncryptedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 6} + oidAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} + oidAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} + oidAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5} +) + +type signedData struct { + Version int `asn1:"default:1"` + DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"` + ContentInfo contentInfo + Certificates rawCertificates `asn1:"optional,tag:0"` + CRLs []pkix.CertificateList `asn1:"optional,tag:1"` + SignerInfos []signerInfo `asn1:"set"` +} + +type rawCertificates struct { + Raw asn1.RawContent +} + +type envelopedData struct { + Version int + RecipientInfos []recipientInfo `asn1:"set"` + EncryptedContentInfo encryptedContentInfo +} + +type recipientInfo struct { + Version int + IssuerAndSerialNumber issuerAndSerial + KeyEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedKey []byte +} + +type encryptedContentInfo struct { + ContentType asn1.ObjectIdentifier + ContentEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedContent asn1.RawValue `asn1:"tag:0,optional,explicit"` +} + +type attribute struct { + Type asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"set"` +} + +type issuerAndSerial struct { + IssuerName asn1.RawValue + SerialNumber *big.Int +} + +// MessageDigestMismatchError is returned when the signer data digest does not +// match the computed digest for the contained content +type MessageDigestMismatchError struct { + ExpectedDigest []byte + ActualDigest []byte +} + +func (err *MessageDigestMismatchError) Error() string { + return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest) +} + +type signerInfo struct { + Version int `asn1:"default:1"` + IssuerAndSerialNumber issuerAndSerial + DigestAlgorithm pkix.AlgorithmIdentifier + AuthenticatedAttributes []attribute `asn1:"optional,tag:0"` + DigestEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedDigest []byte + UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"` +} + +// Parse decodes a DER encoded PKCS7 package +func Parse(data []byte) (p7 *PKCS7, err error) { + if len(data) == 0 { + return nil, errors.New("pkcs7: input data is empty") + } + var info contentInfo + der, err := ber2der(data) + if err != nil { + return nil, err + } + rest, err := asn1.Unmarshal(der, &info) + if len(rest) > 0 { + err = asn1.SyntaxError{Msg: "trailing data"} + return + } + if err != nil { + return + } + + // fmt.Printf("--> Content Type: %s", info.ContentType) + switch { + case info.ContentType.Equal(oidSignedData): + return parseSignedData(info.Content.Bytes) + case info.ContentType.Equal(oidEnvelopedData): + return parseEnvelopedData(info.Content.Bytes) + } + return nil, ErrUnsupportedContentType +} + +func parseSignedData(data []byte) (*PKCS7, error) { + var sd signedData + asn1.Unmarshal(data, &sd) + certs, err := sd.Certificates.Parse() + if err != nil { + return nil, err + } + // fmt.Printf("--> Signed Data Version %d\n", sd.Version) + + var compound asn1.RawValue + var content unsignedData + + // The Content.Bytes maybe empty on PKI responses. + if len(sd.ContentInfo.Content.Bytes) > 0 { + if _, err := asn1.Unmarshal(sd.ContentInfo.Content.Bytes, &compound); err != nil { + return nil, err + } + } + // Compound octet string + if compound.IsCompound { + if _, err = asn1.Unmarshal(compound.Bytes, &content); err != nil { + return nil, err + } + } else { + // assuming this is tag 04 + content = compound.Bytes + } + return &PKCS7{ + Content: content, + Certificates: certs, + CRLs: sd.CRLs, + Signers: sd.SignerInfos, + raw: sd}, nil +} + +func (raw rawCertificates) Parse() ([]*x509.Certificate, error) { + if len(raw.Raw) == 0 { + return nil, nil + } + + var val asn1.RawValue + if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil { + return nil, err + } + + return x509.ParseCertificates(val.Bytes) +} + +func parseEnvelopedData(data []byte) (*PKCS7, error) { + var ed envelopedData + if _, err := asn1.Unmarshal(data, &ed); err != nil { + return nil, err + } + return &PKCS7{ + raw: ed, + }, nil +} + +// Verify checks the signatures of a PKCS7 object +// WARNING: Verify does not check signing time or verify certificate chains at +// this time. +func (p7 *PKCS7) Verify() (err error) { + if len(p7.Signers) == 0 { + return errors.New("pkcs7: Message has no signers") + } + for _, signer := range p7.Signers { + if err := verifySignature(p7, signer); err != nil { + return err + } + } + return nil +} + +func verifySignature(p7 *PKCS7, signer signerInfo) error { + if len(signer.AuthenticatedAttributes) > 0 { + // TODO(fullsailor): First check the content type match + var digest []byte + err := unmarshalAttribute(signer.AuthenticatedAttributes, oidAttributeMessageDigest, &digest) + if err != nil { + return err + } + hash, err := getHashForOID(signer.DigestAlgorithm.Algorithm) + if err != nil { + return err + } + h := hash.New() + h.Write(p7.Content) + computed := h.Sum(nil) + if !hmac.Equal(digest, computed) { + return &MessageDigestMismatchError{ + ExpectedDigest: digest, + ActualDigest: computed, + } + } + } + cert := getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) + if cert == nil { + return errors.New("pkcs7: No certificate for signer") + } + // TODO(fullsailor): Optionally verify certificate chain + // TODO(fullsailor): Optionally verify signingTime against certificate NotAfter/NotBefore + encodedAttributes, err := marshalAttributes(signer.AuthenticatedAttributes) + if err != nil { + return err + } + algo := x509.SHA1WithRSA + return cert.CheckSignature(algo, encodedAttributes, signer.EncryptedDigest) +} + +func marshalAttributes(attrs []attribute) ([]byte, error) { + encodedAttributes, err := asn1.Marshal(struct { + A []attribute `asn1:"set"` + }{A: attrs}) + if err != nil { + return nil, err + } + + // Remove the leading sequence octets + var raw asn1.RawValue + asn1.Unmarshal(encodedAttributes, &raw) + return raw.Bytes, nil +} + +var ( + oidDigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} + oidEncryptionAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} +) + +func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndSerial) *x509.Certificate { + for _, cert := range certs { + if isCertMatchForIssuerAndSerial(cert, ias) { + return cert + } + } + return nil +} + +func getHashForOID(oid asn1.ObjectIdentifier) (crypto.Hash, error) { + switch { + case oid.Equal(oidDigestAlgorithmSHA1): + return crypto.SHA1, nil + } + return crypto.Hash(0), ErrUnsupportedAlgorithm +} + +// GetOnlySigner returns an x509.Certificate for the first signer of the signed +// data payload. If there are more or less than one signer, nil is returned +func (p7 *PKCS7) GetOnlySigner() *x509.Certificate { + if len(p7.Signers) != 1 { + return nil + } + signer := p7.Signers[0] + return getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) +} + +// ErrUnsupportedAlgorithm tells you when our quick dev assumptions have failed +var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3 and AES-256-CBC supported") + +// ErrNotEncryptedContent is returned when attempting to Decrypt data that is not encrypted data +var ErrNotEncryptedContent = errors.New("pkcs7: content data is a decryptable data type") + +// Decrypt decrypts encrypted content info for recipient cert and private key +func (p7 *PKCS7) Decrypt(cert *x509.Certificate, pk crypto.PrivateKey) ([]byte, error) { + data, ok := p7.raw.(envelopedData) + if !ok { + return nil, ErrNotEncryptedContent + } + recipient := selectRecipientForCertificate(data.RecipientInfos, cert) + if recipient.EncryptedKey == nil { + return nil, errors.New("pkcs7: no enveloped recipient for provided certificate") + } + if priv := pk.(*rsa.PrivateKey); priv != nil { + var contentKey []byte + contentKey, err := rsa.DecryptPKCS1v15(rand.Reader, priv, recipient.EncryptedKey) + if err != nil { + return nil, err + } + return data.EncryptedContentInfo.decrypt(contentKey) + } + fmt.Printf("Unsupported Private Key: %v\n", pk) + return nil, ErrUnsupportedAlgorithm +} + +var oidEncryptionAlgorithmDESCBC = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 7} +var oidEncryptionAlgorithmDESEDE3CBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 3, 7} +var oidEncryptionAlgorithmAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} + +func (eci encryptedContentInfo) decrypt(key []byte) ([]byte, error) { + alg := eci.ContentEncryptionAlgorithm.Algorithm + if !alg.Equal(oidEncryptionAlgorithmDESCBC) && !alg.Equal(oidEncryptionAlgorithmDESEDE3CBC) && !alg.Equal(oidEncryptionAlgorithmAES256CBC) { + fmt.Printf("Unsupported Content Encryption Algorithm: %s\n", alg) + return nil, ErrUnsupportedAlgorithm + } + + // EncryptedContent can either be constructed of multple OCTET STRINGs + // or _be_ a tagged OCTET STRING + var cyphertext []byte + if eci.EncryptedContent.IsCompound { + // Complex case to concat all of the children OCTET STRINGs + var buf bytes.Buffer + cypherbytes := eci.EncryptedContent.Bytes + for { + var part []byte + cypherbytes, _ = asn1.Unmarshal(cypherbytes, &part) + buf.Write(part) + if cypherbytes == nil { + break + } + } + cyphertext = buf.Bytes() + } else { + // Simple case, the bytes _are_ the cyphertext + cyphertext = eci.EncryptedContent.Bytes + } + + var block cipher.Block + var err error + + switch { + case alg.Equal(oidEncryptionAlgorithmDESCBC): + block, err = des.NewCipher(key) + case alg.Equal(oidEncryptionAlgorithmDESEDE3CBC): + block, err = des.NewTripleDESCipher(key) + case alg.Equal(oidEncryptionAlgorithmAES256CBC): + block, err = aes.NewCipher(key) + } + if err != nil { + return nil, err + } + + iv := eci.ContentEncryptionAlgorithm.Parameters.Bytes + if len(iv) != block.BlockSize() { + return nil, errors.New("pkcs7: encryption algorithm parameters are malformed") + } + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(cyphertext)) + mode.CryptBlocks(plaintext, cyphertext) + if plaintext, err = unpad(plaintext, mode.BlockSize()); err != nil { + return nil, err + } + return plaintext, nil +} + +func selectRecipientForCertificate(recipients []recipientInfo, cert *x509.Certificate) recipientInfo { + for _, recp := range recipients { + if isCertMatchForIssuerAndSerial(cert, recp.IssuerAndSerialNumber) { + return recp + } + } + return recipientInfo{} +} + +func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool { + return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Compare(cert.RawIssuer, ias.IssuerName.FullBytes) == 0 +} + +func pad(data []byte, blocklen int) ([]byte, error) { + if blocklen < 1 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + padlen := blocklen - (len(data) % blocklen) + if padlen == 0 { + padlen = blocklen + } + pad := bytes.Repeat([]byte{byte(padlen)}, padlen) + return append(data, pad...), nil +} + +func unpad(data []byte, blocklen int) ([]byte, error) { + if blocklen < 1 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + if len(data)%blocklen != 0 || len(data) == 0 { + return nil, fmt.Errorf("invalid data len %d", len(data)) + } + + // the last byte is the length of padding + padlen := int(data[len(data)-1]) + + // check padding integrity, all bytes should be the same + pad := data[len(data)-padlen:] + for _, padbyte := range pad { + if padbyte != byte(padlen) { + return nil, errors.New("invalid padding") + } + } + + return data[:len(data)-padlen], nil +} + +func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error { + for _, attr := range attrs { + if attr.Type.Equal(attributeType) { + _, err := asn1.Unmarshal(attr.Value.Bytes, out) + return err + } + } + return errors.New("pkcs7: attribute type not in attributes") +} + +// UnmarshalSignedAttribute decodes a single attribute from the signer info +func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, out interface{}) error { + sd, ok := p7.raw.(signedData) + if !ok { + return errors.New("pkcs7: payload is not signedData content") + } + if len(sd.SignerInfos) < 1 { + return errors.New("pkcs7: payload has no signers") + } + attributes := sd.SignerInfos[0].AuthenticatedAttributes + return unmarshalAttribute(attributes, attributeType, out) +} + +// SignedData is an opaque data structure for creating signed data payloads +type SignedData struct { + sd signedData + certs []*x509.Certificate + messageDigest []byte +} + +// Attribute represents a key value pair attribute. Value must be marshalable byte +// `encoding/asn1` +type Attribute struct { + Type asn1.ObjectIdentifier + Value interface{} +} + +// SignerInfoConfig are optional values to include when adding a signer +type SignerInfoConfig struct { + ExtraSignedAttributes []Attribute +} + +// NewSignedData initializes a SignedData with content +func NewSignedData(data []byte) (*SignedData, error) { + content, err := asn1.Marshal(data) + if err != nil { + return nil, err + } + ci := contentInfo{ + ContentType: oidData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, + } + digAlg := pkix.AlgorithmIdentifier{ + Algorithm: oidDigestAlgorithmSHA1, + } + h := crypto.SHA1.New() + h.Write(data) + md := h.Sum(nil) + sd := signedData{ + ContentInfo: ci, + Version: 1, + DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{digAlg}, + } + return &SignedData{sd: sd, messageDigest: md}, nil +} + +type attributes struct { + types []asn1.ObjectIdentifier + values []interface{} +} + +// Add adds the attribute, maintaining insertion order +func (attrs *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) { + attrs.types = append(attrs.types, attrType) + attrs.values = append(attrs.values, value) +} + +type sortableAttribute struct { + SortKey []byte + Attribute attribute +} + +type attributeSet []sortableAttribute + +func (sa attributeSet) Len() int { + return len(sa) +} + +func (sa attributeSet) Less(i, j int) bool { + return bytes.Compare(sa[i].SortKey, sa[j].SortKey) < 0 +} + +func (sa attributeSet) Swap(i, j int) { + sa[i], sa[j] = sa[j], sa[i] +} + +func (sa attributeSet) Attributes() []attribute { + attrs := make([]attribute, len(sa)) + for i, attr := range sa { + attrs[i] = attr.Attribute + } + return attrs +} + +func (attrs *attributes) ForMarshaling() ([]attribute, error) { + sortables := make(attributeSet, len(attrs.types)) + for i := range sortables { + attrType := attrs.types[i] + attrValue := attrs.values[i] + asn1Value, err := asn1.Marshal(attrValue) + if err != nil { + return nil, err + } + attr := attribute{ + Type: attrType, + Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag + } + encoded, err := asn1.Marshal(attr) + if err != nil { + return nil, err + } + sortables[i] = sortableAttribute{ + SortKey: encoded, + Attribute: attr, + } + } + sort.Sort(sortables) + return sortables.Attributes(), nil +} + +// AddSigner signs attributes about the content and adds certificate to payload +func (sd *SignedData) AddSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error { + attrs := &attributes{} + attrs.Add(oidAttributeContentType, sd.sd.ContentInfo.ContentType) + attrs.Add(oidAttributeMessageDigest, sd.messageDigest) + attrs.Add(oidAttributeSigningTime, time.Now()) + for _, attr := range config.ExtraSignedAttributes { + attrs.Add(attr.Type, attr.Value) + } + finalAttrs, err := attrs.ForMarshaling() + if err != nil { + return err + } + signature, err := signAttributes(finalAttrs, pkey, crypto.SHA1) + if err != nil { + return err + } + + ias, err := cert2issuerAndSerial(cert) + if err != nil { + return err + } + + signer := signerInfo{ + AuthenticatedAttributes: finalAttrs, + DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidDigestAlgorithmSHA1}, + DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidEncryptionAlgorithmRSA}, + IssuerAndSerialNumber: ias, + EncryptedDigest: signature, + Version: 1, + } + // create signature of signed attributes + sd.certs = append(sd.certs, cert) + sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer) + return nil +} + +// AddCertificate adds the certificate to the payload. Useful for parent certificates +func (sd *SignedData) AddCertificate(cert *x509.Certificate) { + sd.certs = append(sd.certs, cert) +} + +// Finish marshals the content and its signers +func (sd *SignedData) Finish() ([]byte, error) { + sd.sd.Certificates = marshalCertificates(sd.certs) + inner, err := asn1.Marshal(sd.sd) + if err != nil { + return nil, err + } + outer := contentInfo{ + ContentType: oidSignedData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true}, + } + return asn1.Marshal(outer) +} + +func cert2issuerAndSerial(cert *x509.Certificate) (issuerAndSerial, error) { + var ias issuerAndSerial + // The issuer RDNSequence has to match exactly the sequence in the certificate + // We cannot use cert.Issuer.ToRDNSequence() here since it mangles the sequence + ias.IssuerName = asn1.RawValue{FullBytes: cert.RawIssuer} + ias.SerialNumber = cert.SerialNumber + + return ias, nil +} + +// signs the DER encoded form of the attributes with the private key +func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) { + attrBytes, err := marshalAttributes(attrs) + if err != nil { + return nil, err + } + h := hash.New() + h.Write(attrBytes) + hashed := h.Sum(nil) + switch priv := pkey.(type) { + case *rsa.PrivateKey: + return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed) + } + return nil, ErrUnsupportedAlgorithm +} + +// concats and wraps the certificates in the RawValue structure +func marshalCertificates(certs []*x509.Certificate) rawCertificates { + var buf bytes.Buffer + for _, cert := range certs { + buf.Write(cert.Raw) + } + // Even though, the tag & length are stripped out during marshalling the + // RawContent, we have to encode it into the RawContent. If its missing, + // then `asn1.Marshal()` will strip out the certificate wrapper instead. + var val = asn1.RawValue{Bytes: buf.Bytes(), Class: 2, Tag: 0, IsCompound: true} + b, _ := asn1.Marshal(val) + return rawCertificates{Raw: b} +} + +// DegenerateCertificate creates a signed data structure containing only the +// provided certificate +func DegenerateCertificate(cert []byte) ([]byte, error) { + emptyContent := contentInfo{ContentType: oidData} + sd := signedData{ + Version: 1, + ContentInfo: emptyContent, + Certificates: rawCertificates{Raw: cert}, + CRLs: []pkix.CertificateList{}, + } + content, err := asn1.Marshal(sd) + if err != nil { + return nil, err + } + signedContent := contentInfo{ + ContentType: oidSignedData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, + } + return asn1.Marshal(signedContent) +} + +// Encrypt creates and returns an envelope data PKCS7 structure with encrypted +// recipient keys for each recipient public key +// TODO(fullsailor): Add support for encrypting content with other algorithms +func Encrypt(content []byte, recipients []*x509.Certificate) ([]byte, error) { + + // Create DES key & CBC IV + key := make([]byte, 8) + iv := make([]byte, des.BlockSize) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + _, err = rand.Read(iv) + if err != nil { + return nil, err + } + + // Encrypt padded content + block, err := des.NewCipher(key) + if err != nil { + return nil, err + } + mode := cipher.NewCBCEncrypter(block, iv) + plaintext, err := pad(content, mode.BlockSize()) + cyphertext := make([]byte, len(plaintext)) + mode.CryptBlocks(cyphertext, plaintext) + + // Prepare ASN.1 Encrypted Content Info + eci := encryptedContentInfo{ + ContentType: oidData, + ContentEncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: oidEncryptionAlgorithmDESCBC, + Parameters: asn1.RawValue{Tag: 4, Bytes: iv}, + }, + EncryptedContent: marshalEncryptedContent(cyphertext), + } + + // Prepare each recipient's encrypted cipher key + recipientInfos := make([]recipientInfo, len(recipients)) + for i, recipient := range recipients { + encrypted, err := encryptKey(key, recipient) + if err != nil { + return nil, err + } + ias, err := cert2issuerAndSerial(recipient) + if err != nil { + return nil, err + } + info := recipientInfo{ + Version: 0, + IssuerAndSerialNumber: ias, + KeyEncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: oidEncryptionAlgorithmRSA, + }, + EncryptedKey: encrypted, + } + recipientInfos[i] = info + } + + // Prepare envelope content + envelope := envelopedData{ + EncryptedContentInfo: eci, + Version: 0, + RecipientInfos: recipientInfos, + } + innerContent, err := asn1.Marshal(envelope) + if err != nil { + return nil, err + } + + // Prepare outer payload structure + wrapper := contentInfo{ + ContentType: oidEnvelopedData, + Content: asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: innerContent}, + } + + return asn1.Marshal(wrapper) +} + +func marshalEncryptedContent(content []byte) asn1.RawValue { + asn1Content, _ := asn1.Marshal(content) + return asn1.RawValue{Tag: 0, Class: 2, Bytes: asn1Content, IsCompound: true} +} + +func encryptKey(key []byte, recipient *x509.Certificate) ([]byte, error) { + if pub := recipient.PublicKey.(*rsa.PublicKey); pub != nil { + return rsa.EncryptPKCS1v15(rand.Reader, pub, key) + } + return nil, ErrUnsupportedAlgorithm +} diff --git a/vendor/github.com/fullsailor/pkcs7/pkcs7_test.go b/vendor/github.com/fullsailor/pkcs7/pkcs7_test.go new file mode 100644 index 0000000000..ad02c9d349 --- /dev/null +++ b/vendor/github.com/fullsailor/pkcs7/pkcs7_test.go @@ -0,0 +1,410 @@ +package pkcs7 + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io" + "math/big" + "testing" + "time" +) + +func TestVerify(t *testing.T) { + fixture := UnmarshalTestFixture(SignedTestFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Errorf("Parse encountered unexpected error: %v", err) + } + + if err := p7.Verify(); err != nil { + t.Errorf("Verify failed with error: %v", err) + } + expected := []byte("We the People") + if bytes.Compare(p7.Content, expected) != 0 { + t.Errorf("Signed content does not match.\n\tExpected:%s\n\tActual:%s", expected, p7.Content) + + } +} + +func TestVerifyEC2(t *testing.T) { + fixture := UnmarshalTestFixture(EC2IdentityDocumentFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Errorf("Parse encountered unexpected error: %v", err) + } + p7.Certificates = []*x509.Certificate{fixture.Certificate} + if err := p7.Verify(); err != nil { + t.Errorf("Verify failed with error: %v", err) + } +} + +func TestDecrypt(t *testing.T) { + fixture := UnmarshalTestFixture(EncryptedTestFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Fatal(err) + } + content, err := p7.Decrypt(fixture.Certificate, fixture.PrivateKey) + if err != nil { + t.Errorf("Cannot Decrypt with error: %v", err) + } + expected := []byte("This is a test") + if bytes.Compare(content, expected) != 0 { + t.Errorf("Decrypted result does not match.\n\tExpected:%s\n\tActual:%s", expected, content) + } +} + +func TestDegenerateCertificate(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + deg, err := DegenerateCertificate(cert.Certificate.Raw) + if err != nil { + t.Fatal(err) + } + fmt.Printf("=== BEGIN DEGENERATE CERT ===\n% X\n=== END DEGENERATE CERT ===\n", deg) +} + +func TestSign(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + content := []byte("Hello World") + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + fmt.Printf("=== BEGIN SIGNED RESULT ===\n% X\n=== END SIGNED RESULT ===\n", signed) + + p7, err := Parse(signed) + if err != nil { + t.Fatalf("Cannot parse our signed data: %s", err) + } + if bytes.Compare(content, p7.Content) != 0 { + t.Errorf("Our content was not in the parsed data:\n\tExpected: %s\n\tActual: %s", content, p7.Content) + } + if err := p7.Verify(); err != nil { + t.Errorf("Cannot verify our signed data: %s", err) + } +} + +func TestEncrypt(t *testing.T) { + plaintext := []byte("Hello Secret World!") + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + encrypted, err := Encrypt(plaintext, []*x509.Certificate{cert.Certificate}) + if err != nil { + t.Fatal(err) + } + p7, err := Parse(encrypted) + if err != nil { + t.Fatalf("cannot Parse encrypted result: %s", err) + } + result, err := p7.Decrypt(cert.Certificate, cert.PrivateKey) + if err != nil { + t.Fatalf("cannot Decrypt encrypted result: %s", err) + } + if bytes.Compare(plaintext, result) != 0 { + t.Errorf("encrypted data does not match plaintext:\n\tExpected: %s\n\tActual: %s", plaintext, result) + } +} + +func TestUnmarshalSignedAttribute(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + content := []byte("Hello World") + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + oidTest := asn1.ObjectIdentifier{2, 3, 4, 5, 6, 7} + testValue := "TestValue" + if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{ + ExtraSignedAttributes: []Attribute{Attribute{Type: oidTest, Value: testValue}}, + }); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + p7, err := Parse(signed) + var actual string + err = p7.UnmarshalSignedAttribute(oidTest, &actual) + if err != nil { + t.Fatalf("Cannot unmarshal test value: %s", err) + } + if testValue != actual { + t.Errorf("Attribute does not match test value\n\tExpected: %s\n\tActual: %s", testValue, actual) + } +} + +func TestPad(t *testing.T) { + tests := []struct { + Original []byte + Expected []byte + BlockSize int + }{ + {[]byte{0x1, 0x2, 0x3, 0x10}, []byte{0x1, 0x2, 0x3, 0x10, 0x4, 0x4, 0x4, 0x4}, 8}, + {[]byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0}, []byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8}, 8}, + } + for _, test := range tests { + padded, err := pad(test.Original, test.BlockSize) + if err != nil { + t.Errorf("pad encountered error: %s", err) + continue + } + if bytes.Compare(test.Expected, padded) != 0 { + t.Errorf("pad results mismatch:\n\tExpected: %X\n\tActual: %X", test.Expected, padded) + } + } +} + +type certKeyPair struct { + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey +} + +func createTestCertificate() (certKeyPair, error) { + signer, err := createTestCertificateByIssuer("Eddard Stark", nil) + if err != nil { + return certKeyPair{}, err + } + pair, err := createTestCertificateByIssuer("Jon Snow", signer) + if err != nil { + return certKeyPair{}, err + } + return *pair, nil +} + +func createTestCertificateByIssuer(name string, issuer *certKeyPair) (*certKeyPair, error) { + + priv, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 32) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + SignatureAlgorithm: x509.SHA256WithRSA, + Subject: pkix.Name{ + CommonName: name, + Organization: []string{"Acme Co"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + } + var issuerCert *x509.Certificate + var issuerKey crypto.PrivateKey + if issuer != nil { + issuerCert = issuer.Certificate + issuerKey = issuer.PrivateKey + } else { + issuerCert = &template + issuerKey = priv + } + cert, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, priv.Public(), issuerKey) + if err != nil { + return nil, err + } + leaf, err := x509.ParseCertificate(cert) + if err != nil { + return nil, err + } + return &certKeyPair{ + Certificate: leaf, + PrivateKey: priv, + }, nil +} + +type TestFixture struct { + Input []byte + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey +} + +func UnmarshalTestFixture(testPEMBlock string) TestFixture { + var result TestFixture + var derBlock *pem.Block + var pemBlock = []byte(testPEMBlock) + for { + derBlock, pemBlock = pem.Decode(pemBlock) + if derBlock == nil { + break + } + switch derBlock.Type { + case "PKCS7": + result.Input = derBlock.Bytes + case "CERTIFICATE": + result.Certificate, _ = x509.ParseCertificate(derBlock.Bytes) + case "PRIVATE KEY": + result.PrivateKey, _ = x509.ParsePKCS1PrivateKey(derBlock.Bytes) + } + } + + return result +} + +func MarshalTestFixture(t TestFixture, w io.Writer) { + if t.Input != nil { + pem.Encode(w, &pem.Block{Type: "PKCS7", Bytes: t.Input}) + } + if t.Certificate != nil { + pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: t.Certificate.Raw}) + } + if t.PrivateKey != nil { + pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(t.PrivateKey)}) + } +} + +var SignedTestFixture = ` +-----BEGIN PKCS7----- +MIIDVgYJKoZIhvcNAQcCoIIDRzCCA0MCAQExCTAHBgUrDgMCGjAcBgkqhkiG9w0B +BwGgDwQNV2UgdGhlIFBlb3BsZaCCAdkwggHVMIIBQKADAgECAgRpuDctMAsGCSqG +SIb3DQEBCzApMRAwDgYDVQQKEwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3Rh +cmswHhcNMTUwNTA2MDQyNDQ4WhcNMTYwNTA2MDQyNDQ4WjAlMRAwDgYDVQQKEwdB +Y21lIENvMREwDwYDVQQDEwhKb24gU25vdzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEAqr+tTF4mZP5rMwlXp1y+crRtFpuLXF1zvBZiYMfIvAHwo1ta8E1IcyEP +J1jIiKMcwbzeo6kAmZzIJRCTezq9jwXUsKbQTvcfOH9HmjUmXBRWFXZYoQs/OaaF +a45deHmwEeMQkuSWEtYiVKKZXtJOtflKIT3MryJEDiiItMkdybUCAwEAAaMSMBAw +DgYDVR0PAQH/BAQDAgCgMAsGCSqGSIb3DQEBCwOBgQDK1EweZWRL+f7Z+J0kVzY8 +zXptcBaV4Lf5wGZJLJVUgp33bpLNpT3yadS++XQJ+cvtW3wADQzBSTMduyOF8Zf+ +L7TjjrQ2+F2HbNbKUhBQKudxTfv9dJHdKbD+ngCCdQJYkIy2YexsoNG0C8nQkggy +axZd/J69xDVx6pui3Sj8sDGCATYwggEyAgEBMDEwKTEQMA4GA1UEChMHQWNtZSBD +bzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrAgRpuDctMAcGBSsOAwIaoGEwGAYJKoZI +hvcNAQkDMQsGCSqGSIb3DQEHATAgBgkqhkiG9w0BCQUxExcRMTUwNTA2MDAyNDQ4 +LTA0MDAwIwYJKoZIhvcNAQkEMRYEFG9D7gcTh9zfKiYNJ1lgB0yTh4sZMAsGCSqG +SIb3DQEBAQSBgFF3sGDU9PtXty/QMtpcFa35vvIOqmWQAIZt93XAskQOnBq4OloX +iL9Ct7t1m4pzjRm0o9nDkbaSLZe7HKASHdCqijroScGlI8M+alJ8drHSFv6ZIjnM +FIwIf0B2Lko6nh9/6mUXq7tbbIHa3Gd1JUVire/QFFtmgRXMbXYk8SIS +-----END PKCS7----- +-----BEGIN CERTIFICATE----- +MIIB1TCCAUCgAwIBAgIEabg3LTALBgkqhkiG9w0BAQswKTEQMA4GA1UEChMHQWNt +ZSBDbzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrMB4XDTE1MDUwNjA0MjQ0OFoXDTE2 +MDUwNjA0MjQ0OFowJTEQMA4GA1UEChMHQWNtZSBDbzERMA8GA1UEAxMISm9uIFNu +b3cwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKq/rUxeJmT+azMJV6dcvnK0 +bRabi1xdc7wWYmDHyLwB8KNbWvBNSHMhDydYyIijHMG83qOpAJmcyCUQk3s6vY8F +1LCm0E73Hzh/R5o1JlwUVhV2WKELPzmmhWuOXXh5sBHjEJLklhLWIlSimV7STrX5 +SiE9zK8iRA4oiLTJHcm1AgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIAoDALBgkqhkiG +9w0BAQsDgYEAytRMHmVkS/n+2fidJFc2PM16bXAWleC3+cBmSSyVVIKd926SzaU9 +8mnUvvl0CfnL7Vt8AA0MwUkzHbsjhfGX/i+04460Nvhdh2zWylIQUCrncU37/XSR +3Smw/p4AgnUCWJCMtmHsbKDRtAvJ0JIIMmsWXfyevcQ1ceqbot0o/LA= +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIICXgIBAAKBgQCqv61MXiZk/mszCVenXL5ytG0Wm4tcXXO8FmJgx8i8AfCjW1rw +TUhzIQ8nWMiIoxzBvN6jqQCZnMglEJN7Or2PBdSwptBO9x84f0eaNSZcFFYVdlih +Cz85poVrjl14ebAR4xCS5JYS1iJUople0k61+UohPcyvIkQOKIi0yR3JtQIDAQAB +AoGBAIPLCR9N+IKxodq11lNXEaUFwMHXc1zqwP8no+2hpz3+nVfplqqubEJ4/PJY +5AgbJoIfnxVhyBXJXu7E+aD/OPneKZrgp58YvHKgGvvPyJg2gpC/1Fh0vQB0HNpI +1ZzIZUl8ZTUtVgtnCBUOh5JGI4bFokAqrT//Uvcfd+idgxqBAkEA1ZbP/Kseld14 +qbWmgmU5GCVxsZRxgR1j4lG3UVjH36KXMtRTm1atAam1uw3OEGa6Y3ANjpU52FaB +Hep5rkk4FQJBAMynMo1L1uiN5GP+KYLEF5kKRxK+FLjXR0ywnMh+gpGcZDcOae+J ++t1gLoWBIESH/Xt639T7smuSfrZSA9V0EyECQA8cvZiWDvLxmaEAXkipmtGPjKzQ +4PsOtkuEFqFl07aKDYKmLUg3aMROWrJidqsIabWxbvQgsNgSvs38EiH3wkUCQQCg +ndxb7piVXb9RBwm3OoU2tE1BlXMX+sVXmAkEhd2dwDsaxrI3sHf1xGXem5AimQRF +JBOFyaCnMotGNioSHY5hAkEAxyXcNixQ2RpLXJTQZtwnbk0XDcbgB+fBgXnv/4f3 +BCvcu85DqJeJyQv44Oe1qsXEX9BfcQIOVaoep35RPlKi9g== +-----END PRIVATE KEY-----` + +// Content is "This is a test" +var EncryptedTestFixture = ` +-----BEGIN PKCS7----- +MIIBFwYJKoZIhvcNAQcDoIIBCDCCAQQCAQAxgcowgccCAQAwMjApMRAwDgYDVQQK +EwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3RhcmsCBQDL+CvWMAsGCSqGSIb3 +DQEBAQSBgKyP/5WlRTZD3dWMrLOX6QRNDrXEkQjhmToRwFZdY3LgUh25ZU0S/q4G +dHPV21Fv9lQD+q7l3vfeHw8M6Z1PKi9sHMVfxAkQpvaI96DTIT3YHtuLC1w3geCO +8eFWTq2qS4WChSuS/yhYosjA1kTkE0eLnVZcGw0z/WVuEZznkdyIMDIGCSqGSIb3 +DQEHATARBgUrDgMCBwQImpKsUyMPpQigEgQQRcWWrCRXqpD5Njs0GkJl+g== +-----END PKCS7----- +-----BEGIN CERTIFICATE----- +MIIB1jCCAUGgAwIBAgIFAMv4K9YwCwYJKoZIhvcNAQELMCkxEDAOBgNVBAoTB0Fj +bWUgQ28xFTATBgNVBAMTDEVkZGFyZCBTdGFyazAeFw0xNTA1MDYwMzU2NDBaFw0x +NjA1MDYwMzU2NDBaMCUxEDAOBgNVBAoTB0FjbWUgQ28xETAPBgNVBAMTCEpvbiBT +bm93MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK6NU0R0eiCYVquU4RcjKc +LzGfx0aa1lMr2TnLQUSeLFZHFxsyyMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg +8+Zg2r8xnnney7abxcuv0uATWSIeKlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP ++Zxp2ni5qHNraf3wE2VPIQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAKAwCwYJKoZI +hvcNAQELA4GBAIr2F7wsqmEU/J/kLyrCgEVXgaV/sKZq4pPNnzS0tBYk8fkV3V18 +sBJyHKRLL/wFZASvzDcVGCplXyMdAOCyfd8jO3F9Ac/xdlz10RrHJT75hNu3a7/n +9KNwKhfN4A1CQv2x372oGjRhCW5bHNCWx4PIVeNzCyq/KZhyY9sxHE6f +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIICXgIBAAKBgQDK6NU0R0eiCYVquU4RcjKcLzGfx0aa1lMr2TnLQUSeLFZHFxsy +yMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg8+Zg2r8xnnney7abxcuv0uATWSIe +KlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP+Zxp2ni5qHNraf3wE2VPIQIDAQAB +AoGBALyvnSt7KUquDen7nXQtvJBudnf9KFPt//OjkdHHxNZNpoF/JCSqfQeoYkeu +MdAVYNLQGMiRifzZz4dDhA9xfUAuy7lcGQcMCxEQ1dwwuFaYkawbS0Tvy2PFlq2d +H5/HeDXU4EDJ3BZg0eYj2Bnkt1sJI35UKQSxblQ0MY2q0uFBAkEA5MMOogkgUx1C +67S1tFqMUSM8D0mZB0O5vOJZC5Gtt2Urju6vywge2ArExWRXlM2qGl8afFy2SgSv +Xk5eybcEiQJBAOMRwwbEoW5NYHuFFbSJyWll4n71CYuWuQOCzehDPyTb80WFZGLV +i91kFIjeERyq88eDE5xVB3ZuRiXqaShO/9kCQQCKOEkpInaDgZSjskZvuJ47kByD +6CYsO4GIXQMMeHML8ncFH7bb6AYq5ybJVb2NTU7QLFJmfeYuhvIm+xdOreRxAkEA +o5FC5Jg2FUfFzZSDmyZ6IONUsdF/i78KDV5nRv1R+hI6/oRlWNCtTNBv/lvBBd6b +dseUE9QoaQZsn5lpILEvmQJAZ0B+Or1rAYjnbjnUhdVZoy9kC4Zov+4UH3N/BtSy +KJRWUR0wTWfZBPZ5hAYZjTBEAFULaYCXlQKsODSp0M1aQA== +-----END PRIVATE KEY-----` + +var EC2IdentityDocumentFixture = ` +-----BEGIN PKCS7----- +MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA +JIAEggGmewogICJwcml2YXRlSXAiIDogIjE3Mi4zMC4wLjI1MiIsCiAgImRldnBh +eVByb2R1Y3RDb2RlcyIgOiBudWxsLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1 +cy1lYXN0LTFhIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3Rh +bmNlSWQiIDogImktZjc5ZmU1NmMiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVs +bCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIg +OiAiMTIxNjU5MDE0MzM0IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwK +ICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDhUMDM6MDE6MzhaIiwKICAiYXJj +aGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJy +YW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAA +AAAxggEYMIIBFAIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5n +dG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2Vi +IFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0B +CQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDgwMzAxNDRaMCMG +CSqGSIb3DQEJBDEWBBTuUc28eBXmImAautC+wOjqcFCBVjAJBgcqhkjOOAQDBC8w +LQIVAKA54NxGHWWCz5InboDmY/GHs33nAhQ6O/ZI86NwjA9Vz3RNMUJrUPU5tAAA +AAAAAA== +-----END PKCS7----- +-----BEGIN CERTIFICATE----- +MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw +FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD +VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z +ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u +IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl +cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e +ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 +VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P +hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j +k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U +hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF +lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf +MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW +MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw +vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw +7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K +-----END CERTIFICATE-----` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md new file mode 100644 index 0000000000..37452036b6 --- /dev/null +++ b/website/source/docs/auth/aws.html.md @@ -0,0 +1,1455 @@ +--- +layout: "docs" +page_title: "Auth Backend: AWS EC2" +sidebar_current: "docs-auth-aws" +description: |- + The AWS EC2 backend allows automated authentication of AWS EC2 instances. +--- + +# Auth Backend: AWS EC2 + +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 +provisioning security-sensitive credentials (tokens, username/password, client +certificates, etc). Instead, it treats AWS as a Trusted Third Party and uses +the cryptographically signed dynamic metadata information that uniquely +represents each EC2 instance. + +## Authentication Workflow + +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).) + +One piece of "dynamic metadata" available to the EC2 instance, is the instance +identity document, a JSON representation of a collection of instance metadata. +Importantly, AWS also provides a copy of this metadata in PKCS#7 format signed +with its public key, and publishes the public keys used (which are grouped by +region). (Details on the instance identity document and the signature can be +found +[here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html).) + +During login, the backend verifies the signature on the PKCS#7 document, +ensuring that the information contained within, is certified accurate by AWS. +Before succeeding the login attempt and returning a Vault token, the backend +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. + +## Authorization Workflow + +The basic mechanism of operaion is per-role. Roles are registered in the +backend and associated with various optional restricitons, such as the set +of allowed policies and max TTLs on the generated tokens. Each role can +be specified with the contraints that are to be met during the login. For +example, currently the contraint 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. + +In many cases, an organization will use a "seed AMI" that is specialized after +bootup by configuration management or similar processes. For this reason, an +role entry in the backend can also be associated with a "role tag". 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. +If a role with AMI bound contraint, has "role tag" enabled on the role, and +the EC2 instance performing login does not have an expected tag on it, or if the +tag on the instance is deleted for some reason, authentication fails. + +The role tags can be generated at will by an operator with appropriate API +access. They are HMAC-signed by a per-role key stored within the backend, allowing +the backend to verify the authenticity of a found role tag and ensure that it has +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. + +## Client Nonce + +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 +token. The backend addresses this problem by using a Trust On First Use (TOFU) +mechanism that allows the first client to present the PKCS#7 signature of the +document to be authenticated and denying the rest. An important property of +this design is detection of unauthorized access: if an unintended party authenticates, +the intended client will be unable to authenticate and can raise an alert for +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 +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. + +In the default method of operation, the client supplies a unique nonce during +the first authentication attempt, storing this nonce in the client's memory for +future use. This nonce is stored in the whitelist, tied to the instance ID. +Subsequent authentication attempts by the client require the nonce to match; +since only the original client knows the nonce, only the original client is +allowed to reauthenticate. (This is the reason that this is a whitelist rather +than a blacklist; by default, it's keeping track of clients allowed to +reauthenticate, rather than those that are not.) + +It is up to the client to behave correctly with respect to the nonce; if the +client stores the nonce on disk it can survive reboots, but could also give +access to other users or applications on the instance. It is also up to the +operator to ensure that client nonces are in fact unique; sharing nonces allows +a compromise of the nonce value to enable an attacker that gains access to any +EC2 instance to imitate the legitimate client on that instance. This is why +nonces can be disabled on the backend side in favor of only a single +authentication per instance; in some cases, such as when using ASGs, instances +are immutable and single-boot anyways, and in conjunction with a high max TTL, +reauthentication may not be needed (and if it is, the instance can simply be +shut down and allow ASG to start a new one). + +In both cases, entries can be removed from the whitelist by instance ID, +allowing reauthentication by a client if the nonce is lost (or not used) and an +operator approves the process. + +One other point: if available by the OS/distribution being used with the EC2 +instance, it is not a bad idea to firewall access to the signed PKCS#7 metadata +to ensure that it is accessible only to the matching user(s) that require +access. + +## Advanced Options and Caveats + +### Dynamic Management of Policies Via Role Tags + +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 +verification of PKCS#7 signature and instance health, the backend will query +for the value of a specific tag with the configured key that is attached to the +instance. The tag holds information that represents a *subset* of privileges that +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/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 +in order to escalate privileges. + +When 'role_tag' option is enabled on a role, the instances are required to have a +role tag. If the tag is not found on the EC2 instance, authentication will fail. +This is to ensure that privileges of an instance are never escalated for not +having the tag on it or for getting the tag removed. If the role tag creation does +not specify the policy component, the client will inherit the allowed policies set +on the role. If the role tag creation specifies the policy component but it contains +no policies, the token will contain only the `default` policy; by default, this policy +allows only manipulation (revocation, renewal, lookup) of the existing token, plus +access to its [cubbyhole](https://www.vaultproject.io/docs/secrets/cubbyhole/index.html). +This can be useful to allow instances access to a secure "scratch space" for +storing data (via the token's cubbyhole) but without granting any access to +other resources provided by or resident in Vault. + +### Handling Lost Client Nonces + +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/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 +is placed onto a host upon creation, it is given a `pendingTime` value in the +instance identity document (documentation from AWS does not cover this option, +unfortunately). If an instance is stopped and started, the `pendingTime` value +is updated (this does not apply to reboots, however). + +The backend can take advantage of this via the `allow_instance_migration` +option, which is set per-role. When this option is enabled, if the client nonce +does not match the saved nonce, the `pendingTime` value in the instance +identity document will be checked; if it is newer than the stored `pendingTime` +value, the backend assumes that the client was stopped/started and allows the +client to log in successfully, storing the new nonce as the valid nonce for +that client. This essentially re-starts the TOFU mechanism any time the +instance is stopped and started, so should be used with caution. Just like with +initial authentication, the legitimate client should have a way to alert (or an +alert should trigger based on its logs) if it is denied authentication. + +Unfortunately, the `allow_instance_migration` only helps during stop/start +actions; the current metadata does not provide for a way to allow this +automatic behavior during reboots. The backend will be updated if this needed +metadata becomes available. + +The `allow_instance_migration` option is set per-role, and can also be +specified in a role tag. Since role tags can only restrict behavior, if the +option is set to `false` on the role, a value of `true` in the role tag takes +effect; however, if the option is set to `true` on the role, a value set in the +role tag has no effect. + +### Disabling Reauthentication + +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 +login will be allowed per instance. If the intended client successfully +retrieves a token during login, it can be sure that its token will not be +hijacked by another entity. + +When `disallow_reauthentication` option is enabled, the client can choose not +to supply a nonce during login, although it is not an error to do so (the nonce +is simply ignored). Note that reauthentication is enabled by default. If only +a single login is desired, `disallow_reauthentication` should be set explicitly +on the role or on the role tag. + +The `disallow_reauthentication` option is set per-role, and can also be +specified in a role tag. Since role tags can only restrict behavior, if the +option is set to `false` on the role, a value of `true` in the role tag takes +effect; however, if the option is set to `true` on the role, a value set in the +role tag has no effect. + +### Blacklisting Role Tags + +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/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. + +### Expiration Times and Tidying of `blacklist` and `whitelist` Entries + +The expired entries in both identity `whitelist` and role tag `blacklist` are +deleted automatically. The entries in both of these lists contain an expiration +time which is dynamically determined by three factors: `max_ttl` set on the role, +`max_ttl` set on the role tag, and `max_ttl` value of the backend mount. The +least of these three dictates the maximum TTL of the issued token, and +correspondingly will be set as the expiration times of these entries. + +The endpoints `aws/auth/tidy/identity-whitelist` and `aws/auth/tidy/roletag-blacklist` are +provided to clean up the entries present in these lists. These endpoints allow +defining a safety buffer, such that an entry must not only be expired, but be +past expiration by the amount of time dictated by the safety buffer in order +to actually remove the entry. + +Automatic deletion of expired entries is performed by the periodic function +of the backend. This function does the tidying of both blacklist role tags +and whitelist identities. Periodic tidying is activated by default and will +have a safety buffer of 72 hours, meaning only those entries are deleted which +were expired before 72 hours from when the tidy operation is being performed. +This can be configured via `config/tidy/roletag-blacklist` and `config/tidy/identity-whitelist` +endpoints. + +### Varying Public Certificates + +The AWS public certificate which contains the public key used to verify the +PKCS#7 signature varies for groups of regions. The default public certificate +provided with the backend is applicable for many regions. Instances whose PKCS#7 +signatures cannot be verified by the default public certificate, 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/config/certificate/` endpoint. + +### Dangling Tokens + +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 +instance fails to renew the token on time. + +## Authentication + +### Via the CLI + +#### Enable AWS EC2 authentication in Vault. + +``` +$ vault auth-enable aws +``` + +#### Configure the credentials required to make AWS API calls + +Note: the client uses the official AWS SDK and will use environment variable or +IAM role-provided credentials if available. + +``` +$ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA +``` + +#### Configure the policies on the role. + +``` +$ vault write auth/aws/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h +``` + +#### Perform the login operation + +``` +$ vault write auth/aws/login role=dev-role pkcs7=MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA nonce=vault-client-nonce +``` + + +### Via the API + +#### Enable AWS EC2 authentication in Vault. + +``` +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/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/role/dev-role -d '{"bound_ami_id":"ami-fce3c696","policies":"prod,dev","max_ttl":"500h"}' +``` + +#### Perform the login operation + +``` +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role":"dev-role","pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"vault-client-nonce"}' +``` + + +The response will be in JSON. For example: + +```javascript +{ + "auth": { + "renewable": true, + "lease_duration": 1800000, + "metadata": { + "role_tag_max_ttl": "0", + "instance_id": "i-de0f1344" + "ami_id": "ami-fce3c696" + "role": "dev-prod" + }, + "policies": [ + "default", + "dev", + "prod" + ], + "accessor": "20b89871-e6f2-1160-fb29-31c2f6d4645e", + "client_token": "c9368254-3f21-aded-8a6f-7c818e81b17a" + }, + "warnings": null, + "data": null, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +## API +### /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. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/config/client`
+ +
Parameters
+
+
    +
  • + access_key + required + AWS Access key with permissions to query EC2 DescribeInstances API. +
  • +
+
    +
  • + secret_key + required + AWS Secret key with permissions to query EC2 DescribeInstances API. +
  • +
+
    +
  • + endpoint + optional + URL to override the default generated endpoint for making AWS EC2 API calls. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ +#### GET +
+
Description
+ Returns the previously configured AWS access credentials. +
+ +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/config/client`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +``` +{ + "auth": null, + "warnings": null, + "data": { + "secret_key": "vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj", + "access_key": "VKIAJBRHKH6EVTTNXDHA" + "endpoint" "", + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### DELETE +
+
Description
+
+ Deletes the previously configured AWS access credentials. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/config/client`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/config/certificate/ +#### POST +
+
Description
+
+ Registers an AWS public key that is used to verify the PKCS#7 signature of the + EC2 instance metadata. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/config/certificate/`
+ +
Parameters
+
+
    +
  • + cert_name + required + Name of the certificate. +
  • +
+
    +
  • + aws_public_cert + required + AWS Public key required to verify PKCS7 signature of the EC2 instance metadata. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the previously configured AWS public key. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/config/certificate/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "aws_public_cert": "-----BEGIN CERTIFICATE-----\nMIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw\nFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD\nVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z\nODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u\nIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl\ncnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e\nih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3\nVyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P\nhviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j\nk+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U\nhhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF\nlRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf\nMNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW\nMXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw\nvSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw\n7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K\n-----END CERTIFICATE-----\n" + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ +#### LIST +
+
Description
+
+ Lists all the AWS public certificates that are registered with the backend. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/config/certificates?list=true`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "keys": [ + "cert1" + ] + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ +### /auth/aws/config/tidy/identity-whitelist +##### POST +
+
Description
+
+ Configures the periodic tidying operation of the whitelisted identity entries. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/config/tidy/identity-whitelist`
+ +
Parameters
+
+
    +
  • + safety_buffer + optional + The amount of extra time that must have passed beyond the `roletag` expiration, + before it is removed from the backend storage. Defaults to 72h. +
  • +
+
    +
  • + disable_periodic_tidy + optional + If set to 'true', disables the periodic tidying of the 'identity-whitelist/' + entries. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the previously configured periodic whitelist tidying settings. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/config/tidy/identity-whitelist`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "safety_buffer": 60, + "disable_periodic_tidy": false + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ +#### DELETE +
+
Description
+
+ Deletes the previously configured periodic whitelist tidying settings. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/config/tidy/identity-whitelist`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + + +### /auth/aws/config/tidy/roletag-blacklist +##### POST +
+
Description
+
+ Configures the periodic tidying operation of the blacklisted role tag entries. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/config/tidy/roletag-blacklist`
+ +
Parameters
+
+ +
    +
  • + safety_buffer + optional + The amount of extra time that must have passed beyond the `roletag` expiration, before it is removed from the backend storage. Defaults to 72h. +
  • +
+
    +
  • + disable_periodic_tidy + optional + If set to 'true', disables the periodic tidying of the 'roletag-blacklist/' entries. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the previously configured periodic blacklist tidying settings. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/config/tidy/roletag-blacklist`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "safety_buffer": 60, + "disable_periodic_tidy": false + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ +#### DELETE +
+
Description
+
+ Deletes the previously configured periodic blacklist tidying settings. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/config/tidy/roletag-blacklist`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + + +### /auth/aws/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. Currently only one constraint is supported which is 'bound_ami_id', which must + be specified. Going forward, when more than one constraint is supported, the requirement will be to specify at + least one constraint, but not necessarily 'bound_ami_id'. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/role/`
+ +
Parameters
+
+
    +
  • + role + required + Name of the role. +
  • +
+
    +
  • + bound_ami_id + required + If set, defines a constraint on the EC2 instances that they should be using the AMI ID specified by this parameter. +
  • +
+
    +
  • + role_tag + 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. +
  • +
+
    +
  • + max_ttl + optional + The maximum allowed lifetime of tokens issued using this role. +
  • +
+
    +
  • + policies + optional + Policies to be set on tokens issued using this role. +
  • +
+
    +
  • + allow_instance_migration + optional + If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution. +
  • +
+
    +
  • + disallow_reauthentication + optional + If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws/identity-whitelist/' endpoint. Defaults to 'false'. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the previously registered role configuration. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/role/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "bound_ami_id": "ami-fce36987", + "role_tag": "", + "policies": [ + "default", + "dev", + "prod" + ], + "max_ttl": 1800000, + "disallow_reauthentication": false, + "allow_instance_migration": false + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### LIST +
+
Description
+
+ Lists all the roles that are registered with the backend. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/roles?list=true`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "keys": [ + "dev-role", + "prod-role" + ] + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### DELETE +
+
Description
+
+ Deletes the previously registered role. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/role/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/role//tag +#### POST +
+
Description
+
+ Creates a role tag on the role. Role tags provide an effective way to restrict the + capabilities that are set on the role. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/role//tag`
+ +
Parameters
+
+
    +
  • + role + required + Name of the role. +
  • +
+
    +
  • + policies + optional + Policies to be associated with the tag. If set, must be a subset of + the role's policies. If set, but set to an empty value, only the + 'default' policy will be given to issued tokens. +
  • +
+
    +
  • + max_ttl + optional + If set, specifies the maximum allowed token lifetime. +
  • +
+
    +
  • + instance_id + optional + Instance ID for which this tag is intended for. If set, the created tag can only be used by the instance with the given ID. +
  • +
+
    +
  • + disallow_reauthentication + optional + If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/identity-whitelist endpoint. Defaults to 'false'. +
  • +
+
    +
  • + allow_instance_migration + optional + If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution. Defaults to 'false'. +
  • +
+
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "tag_value": "v1:09Vp0qGuyB8=:r=dev-role:p=default,prod:d=false:t=300h0m0s:uPLKCQxqsefRhrp1qmVa1wsQVUXXJG8UZP/pJIdVyOI=", + "tag_key": "VaultRole" + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +### /auth/aws/login +#### POST +
+
Description
+
+ Fetch a token. This endpoint verifies the pkcs#7 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. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/login`
+ +
Parameters
+
+
    +
  • + 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. +
  • +
+
    +
  • + pkcs7 + required + PKCS7 signature of the identity document. +
  • +
+
    +
  • + nonce + required/optional, depends + The `nonce` created by a client of this backend. When `disallow_reauthentication` + option is enabled on either the role or the role tag, then `nonce` parameter is + optional. It is a required parameter otherwise. +
  • +
+
+ +
Returns
+
+ +```javascript +{ + "auth": { + "renewable": true, + "lease_duration": 1800000, + "metadata": { + "role_tag_max_ttl": "0", + "instance_id": "i-de0f1344" + "ami_id": "ami-fce36983" + "role": "dev-role" + }, + "policies": [ + "default", + "dev", + ], + "accessor": "20b89871-e6f2-1160-fb29-31c2f6d4645e", + "client_token": "c9368254-3f21-aded-8a6f-7c818e81b17a" + }, + "warnings": null, + "data": null, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +### /auth/aws/roletag-blacklist/ +#### POST +
+
Description
+
+ Places a valid role tag in a blacklist. This ensures that the role tag + cannot be used by any instance to perform a login operation again. + Note that if the role tag was previousy used to perfom a successful + login, placing the tag in the blacklist does not invalidate the + already issued token. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/roletag-blacklist/`
+ +
Parameters
+
+
    +
  • + role_tag + required + Role tag to be blacklisted. The tag can be supplied as-is. In order + to avoid any encoding problems, it can be base64 encoded. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the blacklist entry of a previously blacklisted role tag. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/broletag-blacklist/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "expiration_time": "2016-04-25T10:35:20.127058773-04:00", + "creation_time": "2016-04-12T22:35:01.178348124-04:00" + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### LIST +
+
Description
+
+ Lists all the role tags that are blacklisted. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/roletag-blacklist?list=true`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "keys": [ + "v1:09Vp0qGuyB8=:a=ami-fce3c696:p=default,prod:d=false:t=300h0m0s:uPLKCQxqsefRhrp1qmVa1wsQVUXXJG8UZP/" + ] + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### DELETE +
+
Description
+
+ Deletes a blacklisted role tag. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/roletag-blacklist/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/tidy/roletag-blacklist +#### POST +
+
Description
+
+ Cleans up the entries in the blacklist based on expiration time on the entry and `safety_buffer`. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/tidy/roletag-blacklist`
+ +
Parameters
+
+
    +
  • + safety_buffer + optional + The amount of extra time that must have passed beyond the `roletag` expiration, before it is removed from the backend storage. Defaults to 72h. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/identity-whitelist/ +#### GET +
+
Description
+
+ Returns an entry in the whitelist. An entry will be created/updated by every successful login. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/identity-whitelist/`
+ +
Parameters
+
+
    +
  • + instance_id + required + EC2 instance ID. A successful login operation from an EC2 instance + gets cached in this whitelist, keyed off of instance ID. +
  • +
+
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "pending_time": "2016-04-14T01:01:41Z", + "expiration_time": "2016-05-05 10:09:16.67077232 +0000 UTC", + "creation_time": "2016-04-14 14:09:16.67077232 +0000 UTC", + "client_nonce": "vault-client-nonce", + "role": "dev-role" + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### LIST +
+
Description
+
+ Lists all the instance IDs that are in the whitelist of successful logins. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/identity-whitelist?list=true`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. + +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "keys": [ + "i-aab47d37" + ] + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### DELETE +
+
Description
+
+ Deletes a cache of the successful login from an instance. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/identity-whitelist/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/tidy/identity-whitelist +#### POST +
+
Description
+
+ Cleans up the entries in the whitelist based on expiration time and `safety_buffer`. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/tidy/identity-whitelist`
+ +
Parameters
+
+
    +
  • + safety_buffer + optional + The amount of extra time that must have passed beyond the identity expiration, before it is removed from the backend storage. Defaults to 72h. +
  • +
+
+ +
Returns
+
`204` response code. +
+
diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8946caa24e..a551d09e6a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -181,6 +181,10 @@ > Username & Password + + > + AWS EC2 Auth +