From e886d5aab976ad2159e65c37d87acdaaff85970a Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 5 Apr 2016 20:42:26 -0400 Subject: [PATCH 01/79] AWS EC2 instances authentication backend --- builtin/credential/aws/backend.go | 70 +++ builtin/credential/aws/backend_test.go | 68 +++ builtin/credential/aws/client.go | 48 ++ .../credential/aws/path_blacklist_roletag.go | 245 +++++++++ .../aws/path_blacklist_roletag_tidy.go | 88 ++++ .../credential/aws/path_config_certificate.go | 245 +++++++++ builtin/credential/aws/path_config_client.go | 144 ++++++ builtin/credential/aws/path_image.go | 246 +++++++++ builtin/credential/aws/path_image_tag.go | 299 +++++++++++ builtin/credential/aws/path_login.go | 483 ++++++++++++++++++ .../credential/aws/path_whitelist_identity.go | 176 +++++++ .../aws/path_whitelist_identity_tidy.go | 87 ++++ builtin/credential/aws/role_tag_hmac_key.go | 44 ++ cli/commands.go | 2 + 14 files changed, 2245 insertions(+) create mode 100644 builtin/credential/aws/backend.go create mode 100644 builtin/credential/aws/backend_test.go create mode 100644 builtin/credential/aws/client.go create mode 100644 builtin/credential/aws/path_blacklist_roletag.go create mode 100644 builtin/credential/aws/path_blacklist_roletag_tidy.go create mode 100644 builtin/credential/aws/path_config_certificate.go create mode 100644 builtin/credential/aws/path_config_client.go create mode 100644 builtin/credential/aws/path_image.go create mode 100644 builtin/credential/aws/path_image_tag.go create mode 100644 builtin/credential/aws/path_login.go create mode 100644 builtin/credential/aws/path_whitelist_identity.go create mode 100644 builtin/credential/aws/path_whitelist_identity_tidy.go create mode 100644 builtin/credential/aws/role_tag_hmac_key.go diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go new file mode 100644 index 0000000000..b9221f2936 --- /dev/null +++ b/builtin/credential/aws/backend.go @@ -0,0 +1,70 @@ +package aws + +import ( + "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) +} + +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 + } + + var b backend + b.Salt = salt + b.Backend = &framework.Backend{ + Help: backendHelp, + + PathsSpecial: &logical.Paths{ + Unauthenticated: []string{ + "login", + }, + }, + + Paths: append([]*framework.Path{ + pathLogin(&b), + pathImage(&b), + pathListImages(&b), + pathImageTag(&b), + pathConfigClient(&b), + pathConfigCertificate(&b), + pathBlacklistRoleTag(&b), + pathListBlacklistRoleTags(&b), + pathBlacklistRoleTagTidy(&b), + pathWhitelistIdentity(&b), + pathWhitelistIdentityTidy(&b), + pathListWhitelistIdentities(&b), + }), + + AuthRenew: b.pathLoginRenew, + } + + return b.Backend, nil +} + +type backend struct { + *framework.Backend + Salt *salt.Salt +} + +const backendHelp = ` +AWS auth backend takes in a AWS EC2 instance identity document, its PKCS#7 signature +and a client created nonce to authenticates the instance with Vault. + +Authentication is backed by a preconfigured association of AMIs to Vault's policies +through 'image/' endpoint. For instances that share an AMI, an instance tag can +be created through 'image//tag'. This tag should be attached to the EC2 instance +before the instance attempts to login to Vault. +` diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go new file mode 100644 index 0000000000..861ce8dc25 --- /dev/null +++ b/builtin/credential/aws/backend_test.go @@ -0,0 +1,68 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" +) + +func TestBackend_ConfigClient(t *testing.T) { + config := logical.TestBackendConfig() + storageView := &logical.InmemStorage{} + config.StorageView = storageView + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + logicaltest.Test(t, logicaltest.TestCase{ + AcceptanceTest: false, + Backend: b, + Steps: []logicaltest.TestStep{}, + }) +} + +func TestBackend_parseRoleTagValue(t *testing.T) { + tag := "v1:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" + expected := roleTag{ + Version: "v1", + Nonce: "XwuKhyyBNJc=", + Policies: []string{"root"}, + MaxTTL: 10800000000000, + ImageID: "ami-fce3c696", + HMAC: "lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=", + } + actual, err := parseRoleTagValue(tag) + if err != nil { + t.Fatalf("err: %s", err) + } + if !actual.Equal(&expected) { + t.Fatalf("err: expected:%#v \ngot: %#v\n", expected, actual) + } + + tag = "v2:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" + actual, err = parseRoleTagValue(tag) + if err == nil { + t.Fatalf("err: expected error due to invalid role tag version", err) + } + + tag = "v1:XwuKhyyBNJc=:a=ami-fce3c696:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" + expected = roleTag{ + Version: "v1", + Nonce: "XwuKhyyBNJc=", + ImageID: "ami-fce3c696", + HMAC: "lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=", + } + actual, err = parseRoleTagValue(tag) + if err != nil { + t.Fatalf("err: %s", err) + } + + tag = "v1:XwuKhyyBNJc=:p=ami-fce3c696:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" + actual, err = parseRoleTagValue(tag) + if err == nil { + t.Fatalf("err: expected error due to missing image ID", err) + } +} diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go new file mode 100644 index 0000000000..483fe6f6ae --- /dev/null +++ b/builtin/credential/aws/client.go @@ -0,0 +1,48 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "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/logical" +) + +// getClientConfig creates a aws-sdk-go config, which is used to create +// client that can interact with AWS API. This reads out the secret key +// and access key that was configured via 'config/client' endpoint and +// uses them to create credentials required to make the AWS API calls. +func getClientConfig(s logical.Storage) (*aws.Config, error) { + // Read the configured secret key and access key + config, err := clientConfigEntry(s) + if err != nil { + return nil, err + } + if config == nil { + return nil, fmt.Errorf( + "client credentials haven't been configured. Please configure\n" + + "them at the 'config/client' endpoint") + } + + // Create the credentials required to access the API. + creds := credentials.NewStaticCredentials(config.AccessKey, config.SecretKey, "") + + // Create a config that can be used to make the API calls. + return &aws.Config{ + Credentials: creds, + Region: aws.String(config.Region), + HTTPClient: cleanhttp.DefaultClient(), + }, nil +} + +// clientEC2 creates a client to interact with AWS EC2 API. +func clientEC2(s logical.Storage) (*ec2.EC2, error) { + awsConfig, err := getClientConfig(s) + if err != nil { + return nil, err + } + return ec2.New(session.New(awsConfig)), nil +} diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go new file mode 100644 index 0000000000..dd5f42f781 --- /dev/null +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -0,0 +1,245 @@ +package aws + +import ( + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathBlacklistRoleTag(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "blacklist/roletag$", + Fields: map[string]*framework.FieldSchema{ + "role_tag": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Role tag that needs be blacklisted", + }, + }, + + ExistenceCheck: b.pathBlacklistRoleTagExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathBlacklistRoleTagUpdate, + logical.ReadOperation: b.pathBlacklistRoleTagRead, + logical.DeleteOperation: b.pathBlacklistRoleTagDelete, + }, + + HelpSynopsis: pathBlacklistRoleTagSyn, + HelpDescription: pathBlacklistRoleTagDesc, + } +} + +// Path to list all the blacklisted tags. +func pathListBlacklistRoleTags(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "blacklist/roletags/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathBlacklistRoleTagsList, + }, + + HelpSynopsis: pathListBlacklistRoleTagsHelpSyn, + HelpDescription: pathListBlacklistRoleTagsHelpDesc, + } +} + +// Lists all the blacklisted role tags. +func (b *backend) pathBlacklistRoleTagsList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + tags, err := req.Storage.List("blacklist/roletag/") + if err != nil { + return nil, err + } + return logical.ListResponse(tags), nil +} + +// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. +// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. +// +// A role should be allowed to be blacklisted even if it was prevously blacklisted. +func (b *backend) pathBlacklistRoleTagExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + return true, nil +} + +// Fetch an un-expired entry from the role tag blacklist for a given tag. +func blacklistRoleTagValidEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { + entry, err := blacklistRoleTagEntry(s, tag) + if err != nil { + return nil, err + } + + // Exclude the item if it is expired. + if entry == nil || time.Now().After(entry.ExpirationTime) { + return nil, nil + } + + return entry, nil +} + +// Fetch an entry from the role tag blacklist for a given tag. +func blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { + entry, err := s.Get("blacklist/roletag/" + 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) pathBlacklistRoleTagDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + tag := data.Get("role_tag").(string) + if tag == "" { + return logical.ErrorResponse("missing role_tag"), nil + } + + err := req.Storage.Delete("blacklist/roletag/" + tag) + if err != nil { + return nil, err + } + return nil, nil +} + +// If the given role tag is blacklisted, returns the details of the blacklist entry. +// Returns 'nil' otherwise. +func (b *backend) pathBlacklistRoleTagRead( + 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 := blacklistRoleTagEntry(req.Storage, tag) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "creation_time": entry.CreationTime, + "expiration_time": entry.ExpirationTime, + }, + }, nil +} + +// pathBlacklistRoleTagUpdate 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) pathBlacklistRoleTagUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + tag := data.Get("role_tag").(string) + if tag == "" { + return logical.ErrorResponse("missing role_tag"), nil + } + + // Parse the role tag from string form to a struct form. + rTag, err := parseRoleTagValue(tag) + if err != nil { + return nil, err + } + + // Build the plaintext form of the role tag and verify the prepared + // value using the HMAC. + verified, err := verifyRoleTagValue(req.Storage, rTag) + if err != nil { + return nil, err + } + if !verified { + return logical.ErrorResponse("role tag invalid"), nil + } + + // Get the entry for the AMI used by the instance. + imageEntry, err := awsImage(req.Storage, rTag.ImageID) + if err != nil { + return nil, err + } + if imageEntry == nil { + return logical.ErrorResponse("image entry not found"), nil + } + + blEntry, err := blacklistRoleTagEntry(req.Storage, tag) + if err != nil { + return nil, err + } + if blEntry == nil { + blEntry = &roleTagBlacklistEntry{} + } + + currentTime := time.Now() + + var epoch time.Time + if blEntry.CreationTime.Equal(epoch) { + // 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 + } + + // If max_ttl is not set for the role tag, fall back on the mount's max_ttl. + if rTag.MaxTTL == time.Duration(0) { + rTag.MaxTTL = b.System().MaxLeaseTTL() + } + + if imageEntry.MaxTTL > time.Duration(0) && rTag.MaxTTL > imageEntry.MaxTTL { + rTag.MaxTTL = imageEntry.MaxTTL + } + + // Expiration time is decided by the max_ttl value. + blEntry.ExpirationTime = currentTime.Add(rTag.MaxTTL) + + entry, err := logical.StorageEntryJSON("blacklist/roletag/"+tag, blEntry) + if err != nil { + return nil, err + } + + // Store it. + 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 pathBlacklistRoleTagSyn = ` +Blacklist a previously created role tag. +` + +const pathBlacklistRoleTagDesc = ` +Blacklist a role tag so that it cannot be used by an EC2 instance to perform logins +in the future. This can be used if the role tag is suspected or believed to be possessed +by an unauthorized entity. + +The entries in the blacklist are not automatically deleted. Although, they will have an +expiration time set on the entry. There is a separate endpoint 'blacklist/roletag/tidy', +that needs to be invoked to clean-up all the expired entries in the blacklist. +` + +const pathListBlacklistRoleTagsHelpSyn = ` +List the blacklisted role tags. +` + +const pathListBlacklistRoleTagsHelpDesc = ` +List all the entries present in the blacklist. This will show both the valid entries and +the expired entries in the blacklist. Use 'blacklist/roletag/tidy' endpoint to clean-up +the blacklist of role tags. +` diff --git a/builtin/credential/aws/path_blacklist_roletag_tidy.go b/builtin/credential/aws/path_blacklist_roletag_tidy.go new file mode 100644 index 0000000000..ccbc20f62f --- /dev/null +++ b/builtin/credential/aws/path_blacklist_roletag_tidy.go @@ -0,0 +1,88 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathBlacklistRoleTagTidy(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "blacklist/roletag/tidy$", + 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's +expiration, before it is removed from the backend storage.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathBlacklistRoleTagTidyUpdate, + }, + + HelpSynopsis: pathBlacklistRoleTagTidySyn, + HelpDescription: pathBlacklistRoleTagTidyDesc, + } +} + +// pathBlacklistRoleTagTidyUpdate is used to clean-up the entries in the role tag blacklist. +func (b *backend) pathBlacklistRoleTagTidyUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + // safety_buffer is an optional parameter. + safety_buffer := data.Get("safety_buffer").(int) + bufferDuration := time.Duration(safety_buffer) * time.Second + + tags, err := req.Storage.List("blacklist/roletag/") + if err != nil { + return nil, err + } + + for _, tag := range tags { + tagEntry, err := req.Storage.Get("blacklist/roletag/" + tag) + if err != nil { + return nil, fmt.Errorf("error fetching tag %s: %s", tag, err) + } + + if tagEntry == nil { + return nil, fmt.Errorf("tag entry for tag %s is nil", tag) + } + + if tagEntry.Value == nil || len(tagEntry.Value) == 0 { + return nil, fmt.Errorf("found entry for tag %s but actual tag is empty", tag) + } + + var result roleTagBlacklistEntry + if err := tagEntry.DecodeJSON(&result); err != nil { + return nil, err + } + + if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { + if err := req.Storage.Delete("blacklist/roletag" + tag); err != nil { + return nil, fmt.Errorf("error deleting tag %s from storage: %s", tag, err) + } + } + } + + return nil, nil +} + +const pathBlacklistRoleTagTidySyn = ` +Clean-up the blacklisted role tag entries. +` + +const pathBlacklistRoleTagTidyDesc = ` +When a role tag is blacklisted, the expiration time of the blacklist entry is +determined by the 'max_ttl' present in the role tag. If 'max_ttl' is not provided +in the role tag, the backend mount's 'max_ttl' value will be used to determine +the expiration time of the blacklist entry. + +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_config_certificate.go b/builtin/credential/aws/path_config_certificate.go new file mode 100644 index 0000000000..624fd106b2 --- /dev/null +++ b/builtin/credential/aws/path_config_certificate.go @@ -0,0 +1,245 @@ +package aws + +import ( + "crypto" + "crypto/dsa" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/pem" + "fmt" + "math/big" + + "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) +const defaultAWSPublicCert = ` +-----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----- +` + +func pathConfigCertificate(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/certificate$", + Fields: map[string]*framework.FieldSchema{ + "aws_public_cert": &framework.FieldSchema{ + Type: framework.TypeString, + Default: defaultAWSPublicCert, + Description: "AWS Public key required to verify PKCS7 signature.", + }, + }, + + ExistenceCheck: b.pathConfigCertificateExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigCertificateCreateUpdate, + logical.UpdateOperation: b.pathConfigCertificateCreateUpdate, + logical.ReadOperation: b.pathConfigCertificateRead, + }, + + 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) { + entry, err := awsPublicCertificateEntry(req.Storage) + if err != nil { + return false, err + } + return entry != nil, 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; failed to decode certificate") + } + + // 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 +} + +// awsPublicCertificateParsed will fetch the storage entry for the certificate, +// decodes it and returns the parsed certificate. +func awsPublicCertificateParsed(s logical.Storage) (*x509.Certificate, error) { + certEntry, err := awsPublicCertificateEntry(s) + if err != nil { + return nil, err + } + if certEntry == nil { + return decodePEMAndParseCertificate(defaultAWSPublicCert) + } + return decodePEMAndParseCertificate(certEntry.AWSPublicCert) +} + +// 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 awsPublicCertificateEntry(s logical.Storage) (*awsPublicCert, error) { + entry, err := s.Get("config/certificate") + if err != nil { + return nil, err + } + if entry == nil { + // Existence check depends on this being nil when the storage entry is not present. + return nil, nil + } + + var result awsPublicCert + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +// 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) { + certificateEntry, err := awsPublicCertificateEntry(req.Storage) + if err != nil { + return nil, err + } + if certificateEntry == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "aws_public_cert": certificateEntry.AWSPublicCert, + }, + }, 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) { + + // Check if there is already a certificate entry registered. + certEntry, err := awsPublicCertificateEntry(req.Storage) + if err != nil { + return nil, err + } + if certEntry == nil { + certEntry = &awsPublicCert{} + } + + // Check if the value is provided by the client. + certStrB64, ok := data.GetOk("aws_public_cert") + if ok { + certBytes, err := base64.StdEncoding.DecodeString(certStrB64.(string)) + if err != nil { + return nil, err + } + + certEntry.AWSPublicCert = string(certBytes) + } else if req.Operation == logical.CreateOperation { + certEntry.AWSPublicCert = data.Get("aws_public_cert").(string) + } + + // If explicitly set to empty string, error out. + if certEntry.AWSPublicCert == "" { + return logical.ErrorResponse("missing 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 + } + + // Before trusting the signature provided, validate its signature. + + // Extract the signature of the certificate. + dsaSig := &dsaSignature{} + dsaSigRest, err := asn1.Unmarshal(publicCert.Signature, dsaSig) + if err != nil { + return nil, err + } + if len(dsaSigRest) != 0 { + return nil, fmt.Errorf("failed to unmarshal certificate's signature") + } + + certHashFunc := crypto.SHA1.New() + + // RawTBSCertificate will contain the information in the certificate that is signed. + certHashFunc.Write(publicCert.RawTBSCertificate) + + // Verify the signature using the public key present in the certificate. + if !dsa.Verify(publicCert.PublicKey.(*dsa.PublicKey), certHashFunc.Sum(nil), dsaSig.R, dsaSig.S) { + return logical.ErrorResponse("invalid certificate; failed to verify certificate's signature"), nil + } + + // If none of the checks fail, save the provided certificate. + entry, err := logical.StorageEntryJSON("config/certificate", 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 = ` +Configure the AWS Public Key that is used to verify the PKCS#7 signature of the identidy document. +` + +const pathConfigCertificateDesc = ` +AWS Public Key used to verify the PKCS#7 signature of the identity document +varies by region. It can be found in AWS's 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). +` diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go new file mode 100644 index 0000000000..9d62271046 --- /dev/null +++ b/builtin/credential/aws/path_config_client.go @@ -0,0 +1,144 @@ +package aws + +import ( + "fmt" + + "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, + Description: "Access key with permission to query instance metadata.", + }, + + "secret_key": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Secret key with permission to query instance metadata.", + }, + + "region": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "us-east-1", + Description: "Region for API calls.", + }, + }, + + ExistenceCheck: b.pathConfigClientExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigClientCreateUpdate, + logical.UpdateOperation: b.pathConfigClientCreateUpdate, + }, + + 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 := clientConfigEntry(req.Storage) + if err != nil { + return false, err + } + return entry != nil, nil +} + +// Fetch the client configuration required to access the AWS API. +func clientConfigEntry(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 +} + +// 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) { + + configEntry, err := clientConfigEntry(req.Storage) + if err != nil { + return nil, err + } + if configEntry == nil { + configEntry = &clientConfig{} + } + + regionStr, ok := data.GetOk("region") + if ok { + configEntry.Region = regionStr.(string) + } else if req.Operation == logical.CreateOperation { + configEntry.Region = data.Get("region").(string) + } + + // Either a valid region needs to be provided or it should be left empty + // so a default value could take over. + if configEntry.Region == "" { + return nil, fmt.Errorf("invalid region") + + } + + accessKeyStr, ok := data.GetOk("access_key") + if ok { + configEntry.AccessKey = accessKeyStr.(string) + } else if req.Operation == logical.CreateOperation { + if configEntry.AccessKey = data.Get("access_key").(string); configEntry.AccessKey == "" { + return nil, fmt.Errorf("missing access_key") + } + } + + secretKeyStr, ok := data.GetOk("secret_key") + if ok { + configEntry.SecretKey = secretKeyStr.(string) + } else if req.Operation == logical.CreateOperation { + if configEntry.SecretKey = data.Get("secret_key").(string); configEntry.SecretKey == "" { + return nil, fmt.Errorf("missing secret_key") + } + } + + entry, err := logical.StorageEntryJSON("config/client", configEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + 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"` + Region string `json:"region" structs:"region" mapstructure:"region"` +} + +const pathConfigClientHelpSyn = ` +Configure the client credentials that are used to query instance details from AWS EC2 API. +` + +const pathConfigClientHelpDesc = ` +AWS auth backend makes API calls to retrieve EC2 instance metadata. +The aws_secret_key and aws_access_key registered with Vault should have the +permissions to make these API calls. +` diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go new file mode 100644 index 0000000000..0a047d3931 --- /dev/null +++ b/builtin/credential/aws/path_image.go @@ -0,0 +1,246 @@ +package aws + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathImage(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "image/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "AMI name to be mapped.", + }, + + "role_tag": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "If set, enables the RoleTag for this AMI. The value set for this field should be the 'key' of the tag on the EC2 instance using the RoleTag. Defaults to empty string.", + }, + + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 0, + Description: "The maximum allowed lease duration", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "default", + Description: "Policies to be associated with the AMI.", + }, + + "allow_instance_reboot": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, allows rebooting of the OS where the client resides. Essentially, this disables the client nonce check. Use with caution.", + }, + }, + + ExistenceCheck: b.pathImageExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathImageCreateUpdate, + logical.UpdateOperation: b.pathImageCreateUpdate, + logical.ReadOperation: b.pathImageRead, + logical.DeleteOperation: b.pathImageDelete, + }, + + HelpSynopsis: pathImageSyn, + HelpDescription: pathImageDesc, + } +} + +// pathListImages createa a path that enables listing of all the AMIs that are +// registered with Vault. +func pathListImages(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "images/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathImageList, + }, + + HelpSynopsis: pathListImagesHelpSyn, + HelpDescription: pathListImagesHelpDesc, + } +} + +// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. +// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. +func (b *backend) pathImageExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + entry, err := awsImage(req.Storage, strings.ToLower(data.Get("name").(string))) + if err != nil { + return false, err + } + return entry != nil, nil +} + +// awsImage is used to get the information registered for the given AMI ID. +func awsImage(s logical.Storage, name string) (*awsImageEntry, error) { + entry, err := s.Get("image/" + strings.ToLower(name)) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result awsImageEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +// pathImageDelete is used to delete the information registered for a given AMI ID. +func (b *backend) pathImageDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + err := req.Storage.Delete("image/" + strings.ToLower(data.Get("name").(string))) + if err != nil { + return nil, err + } + return nil, nil +} + +// pathImageList is used to list all the AMI IDs registered with Vault. +func (b *backend) pathImageList( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + images, err := req.Storage.List("image/") + if err != nil { + return nil, err + } + return logical.ListResponse(images), nil +} + +// pathImageRead is used to view the information registered for a given AMI ID. +func (b *backend) pathImageRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + imageEntry, err := awsImage(req.Storage, strings.ToLower(data.Get("name").(string))) + if err != nil { + return nil, err + } + if imageEntry == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "role_tag": imageEntry.RoleTag, + "policies": strings.Join(imageEntry.Policies, ","), + "max_ttl": imageEntry.MaxTTL / time.Second, + }, + }, nil +} + +// pathImageCreateUpdate is used to associate Vault policies to a given AMI ID. +func (b *backend) pathImageCreateUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + imageID := strings.ToLower(data.Get("name").(string)) + if imageID == "" { + return logical.ErrorResponse("missing AMI name"), nil + } + + imageEntry, err := awsImage(req.Storage, imageID) + if err != nil { + return nil, err + } + if imageEntry == nil { + imageEntry = &awsImageEntry{} + } + + policiesStr, ok := data.GetOk("policies") + if ok { + imageEntry.Policies = policyutil.ParsePolicies(policiesStr.(string)) + } else if req.Operation == logical.CreateOperation { + imageEntry.Policies = []string{"default"} + } + + allowInstanceRebootBool, ok := data.GetOk("allow_instance_reboot") + if ok { + imageEntry.AllowInstanceReboot = allowInstanceRebootBool.(bool) + } else if req.Operation == logical.CreateOperation { + imageEntry.AllowInstanceReboot = data.Get("allow_instance_reboot").(bool) + } + + maxTTLInt, ok := data.GetOk("max_ttl") + if ok { + maxTTL := time.Duration(maxTTLInt.(int)) * time.Second + systemMaxTTL := b.System().MaxLeaseTTL() + if maxTTL > systemMaxTTL { + return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", maxTTL/time.Second, systemMaxTTL/time.Second)), nil + } + + if maxTTL < time.Duration(0) { + return logical.ErrorResponse("max_ttl cannot be negative"), nil + } + + imageEntry.MaxTTL = maxTTL + } else if req.Operation == logical.CreateOperation { + imageEntry.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second + } + + roleTagStr, ok := data.GetOk("role_tag") + if ok { + imageEntry.RoleTag = roleTagStr.(string) + if len(imageEntry.RoleTag) > 127 { + return logical.ErrorResponse("role tag 'key' is exceeding the limit of 127 characters"), nil + } + } else if req.Operation == logical.CreateOperation { + imageEntry.RoleTag = data.Get("role_tag").(string) + } + + entry, err := logical.StorageEntryJSON("image/"+imageID, imageEntry) + if err != nil { + return nil, err + } + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + return nil, nil +} + +// Struct to hold the information associated with an AMI ID in Vault. +type awsImageEntry struct { + RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` + AllowInstanceReboot bool `json:"allow_instance_reboot" structs:"allow_instance_reboot" mapstructure:"allow_instance_reboot"` + MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` + Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` +} + +const pathImageSyn = ` +Associate an AMI to Vault's policies. +` + +const pathImageDesc = ` +A precondition for login is that the AMI used by the EC2 instance, needs to +be registered with Vault. After the authentication of the instance, the +authorization for the instance to access Vault's resources is determined +by the policies that are associated to the AMI through this endpoint. + +In case the AMI is shared by many instances, then a role tag can be created +through the endpoint 'image//tag'. This tag needs to be applied on the +instance before it attempts to login to Vault. The policies on the tag should +be a subset of policies that are associated to the AMI in this endpoint. In +order to enable login using tags, RoleTag needs to be enabled in this endpoint. + +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 a upper +limit of the 'max_ttl' value that is applicable to the backend. +` + +const pathListImagesHelpSyn = ` +Lists all the AMIs that are registered with Vault. +` + +const pathListImagesHelpDesc = ` +AMIs will be listed by their respective AMI ID. +` diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go new file mode 100644 index 0000000000..f1bb95a7a3 --- /dev/null +++ b/builtin/credential/aws/path_image_tag.go @@ -0,0 +1,299 @@ +package aws + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/policyutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +const roleTagVersion = "v1" + +func pathImageTag(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "image/" + framework.GenericNameRegex("name") + "/tag$", + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "AMI name to create a tag for.", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Policies to be associated with the tag.", + }, + + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 0, + Description: "The maximum allowed lease duration", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathImageTagUpdate, + }, + + HelpSynopsis: pathImageTagSyn, + HelpDescription: pathImageTagDesc, + } +} + +// pathImageTagUpdate is used to create an EC2 instance tag which will +// identify the Vault resources that the instance will be authorized for. +func (b *backend) pathImageTagUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + imageID := strings.ToLower(data.Get("name").(string)) + if imageID == "" { + return logical.ErrorResponse("missing image name"), nil + } + + // Parse the given policies into a slice and add 'default' if not provided. + // Remove all other policies if 'root' is present. + policies := policyutil.ParsePolicies(data.Get("policies").(string)) + + // Fetch the image entry corresponding to the AMI name + imageEntry, err := awsImage(req.Storage, imageID) + if err != nil { + return nil, err + } + if imageEntry == nil { + return logical.ErrorResponse("image entry not found"), nil + } + + // If RoleTag is empty, disallow creation of tag. + if imageEntry.RoleTag == "" { + return logical.ErrorResponse("tag creation is not enabled for this image"), nil + } + + // Create a random nonce + nonce, err := createRoleTagNonce() + if err != nil { + return nil, err + } + + // max_ttl for the role tag should be less than the max_ttl set on the image. + 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() { + return logical.ErrorResponse(fmt.Sprintf("Registered AMI does not have a max_ttl set. So, the given TTL of %d seconds should be less than the max_ttl set for the corresponding backend mount of %d seconds.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second)), nil + } + + // If max_ttl is set for the image, check the bounds for tag's max_ttl value using that. + if imageEntry.MaxTTL != time.Duration(0) && maxTTL > imageEntry.MaxTTL { + return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than the max_ttl set for the corresponding image of %d seconds", maxTTL/time.Second, imageEntry.MaxTTL/time.Second)), nil + } + + if maxTTL < time.Duration(0) { + return logical.ErrorResponse("max_ttl cannot be negative"), nil + } + + // Attach version, nonce, policies and maxTTL to the role tag value. + rTagValue, err := prepareRoleTagPlainValue(&roleTag{Version: roleTagVersion, + ImageID: imageID, + Nonce: nonce, + Policies: policies, + MaxTTL: maxTTL, + }) + if err != nil { + return nil, err + } + + // Get the key used for creating the HMAC + key, err := hmacKey(req.Storage) + if err != nil { + return nil, err + } + + // Create the HMAC of the value + hmacB64, err := createRoleTagHMACBase64(key, rTagValue) + if err != nil { + return nil, err + } + + // attach the HMAC to the value + rTagValue = fmt.Sprintf("%s:%s", rTagValue, hmacB64) + if len(rTagValue) > 255 { + return nil, fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") + } + + return &logical.Response{ + Data: map[string]interface{}{ + "tag_key": imageEntry.RoleTag, + "tag_value": rTagValue, + }, + }, nil +} + +// verifyRoleTagValue rebuilds the role tag value without the HMAC, +// computes the HMAC from it using the backend specific key and +// compares it with the received HMAC. +func verifyRoleTagValue(s logical.Storage, rTag *roleTag) (bool, error) { + // Fetch the plaintext part of role tag + rTagPlainText, err := prepareRoleTagPlainValue(rTag) + if err != nil { + return false, err + } + + // Get the key used for creating the HMAC + key, err := hmacKey(s) + if err != nil { + return false, err + } + + // TODO: for testing purposes. Remove this. + key = "ab1728ba-5fd5-7298-d344-e9df1b09f5ea" + + // Compute the HMAC of the plaintext + hmacB64, err := createRoleTagHMACBase64(key, rTagPlainText) + if err != nil { + return false, err + } + return rTag.HMAC == hmacB64, nil +} + +// prepareRoleTagPlainValue builds the role tag value without the HMAC in it. +func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { + if rTag.Version == "" { + return "", fmt.Errorf("missing version") + } + // attach version to the value + value := rTag.Version + + if rTag.Nonce == "" { + return "", fmt.Errorf("missing nonce") + } + // attach nonce to the value + value = fmt.Sprintf("%s:%s", value, rTag.Nonce) + + if rTag.ImageID == "" { + return "", fmt.Errorf("missing ami_name") + } + // attach ami_name to the value + value = fmt.Sprintf("%s:a=%s", value, rTag.ImageID) + + // attach policies to value + value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) + + // attach max_ttl if it is provided + if rTag.MaxTTL > time.Duration(0) { + value = fmt.Sprintf("%s:t=%s", value, rTag.MaxTTL) + } + + return value, nil +} + +// Parses the tag from string form into a struct form. +func parseRoleTagValue(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] + + // Delete the HMAC from the list. + tagItems = tagItems[:len(tagItems)-1] + + // Version is the first element. + rTag.Version = tagItems[0] + if rTag.Version != roleTagVersion { + return nil, fmt.Errorf("invalid role tag version") + } + + // Nonce is the second element. + rTag.Nonce = tagItems[1] + + if len(tagItems) > 2 { + // Delete the version and nonce from the list. + tagItems = tagItems[2:] + for _, tagItem := range tagItems { + switch { + case strings.Contains(tagItem, "a="): + rTag.ImageID = strings.TrimPrefix(tagItem, "a=") + case strings.Contains(tagItem, "p="): + rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",") + case strings.Contains(tagItem, "t="): + var err error + rTag.MaxTTL, err = time.ParseDuration(strings.TrimPrefix(tagItem, "t=")) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unrecognized item in tag") + } + } + } + if rTag.ImageID == "" { + return nil, fmt.Errorf("missing image ID") + } + + return rTag, nil +} + +// Creates base64 encoded HMAC using a backend specific key. +func createRoleTagHMACBase64(key, value string) (string, error) { + 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) { + uuidBytes, err := uuid.GenerateRandomBytes(8) + if err != nil { + return "", err + } + 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"` + 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"` + ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` + HMAC string `json:"hmac" structs:"hmac" mapstructure:"hmac"` +} + +func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { + return rTag1.Version == rTag2.Version && + rTag1.Nonce == rTag2.Nonce && + policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && + rTag1.MaxTTL == rTag2.MaxTTL && + rTag1.ImageID == rTag2.ImageID && + rTag1.HMAC == rTag2.HMAC +} + +const pathImageTagSyn = ` +Create a tag for an EC2 instance. +` + +const pathImageTagDesc = ` +When an AMI is used by more than one EC2 instance, policies to be associated +during login are determined by a particular tag on the instance. This tag +can be created using this endpoint. + +A RoleTag setting needs to be enabled in 'image/' endpoint, to be able +to create a tag. Also, the policies to be associated with the tag should be +a subset of the policies associated with the regisred AMI. + +This endpoint will return both the 'key' and the 'value' to be set for the +instance tag. +` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go new file mode 100644 index 0000000000..2e4b00846e --- /dev/null +++ b/builtin/credential/aws/path_login.go @@ -0,0 +1,483 @@ +package aws + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "github.com/vishalnayak/pkcs7" +) + +func pathLogin(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "login$", + Fields: map[string]*framework.FieldSchema{ + "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.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathLoginUpdate, + }, + + HelpSynopsis: pathLoginSyn, + HelpDescription: pathLoginDesc, + } +} + +// validateInstanceID queries the status of the EC2 instance using AWS EC2 API and +// checks if the instance is running and is healthy. +func validateInstanceID(s logical.Storage, instanceID string) error { + // Create an EC2 client to pull the instance information + ec2Client, err := clientEC2(s) + if err != nil { + return err + } + + // Get the status of the instance + instanceStatus, err := ec2Client.DescribeInstanceStatus(&ec2.DescribeInstanceStatusInput{ + InstanceIds: []*string{aws.String(instanceID)}, + }) + if err != nil { + return err + } + + // Validate the instance through InstanceState, InstanceStatus and SystemStatus + return validateInstanceStatus(instanceStatus) +} + +// validateMetadata matches the given client nonce and pending time with the one cached +// in the identity whitelist during the previous login. +func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, imageEntry *awsImageEntry) error { + + 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 an OS reboot and + // lost its client nonce. + // + // If `allow_instance_reboot` property of the registered AMI, 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. + // + // This is a weak creterion and hence the `allow_instance_reboot` option should be used with caution. + if clientNonce != storedIdentity.ClientNonce { + if !imageEntry.AllowInstanceReboot { + return fmt.Errorf("client nonce mismatch") + } + if imageEntry.AllowInstanceReboot && !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 than the + // 'pendingTime' that was used for previous 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 parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) { + 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 certificate that is used to verify the signature. + publicCert, err := awsPublicCertificateParsed(s) + if err != nil { + return nil, err + } + if publicCert == nil { + return nil, fmt.Errorf("certificate to verify the signature is not found") + } + + // Before calling Verify() on the PKCS#7 struct, set the certificate to be used + // to verify the contents in the signer information. + pkcs7Data.Certificates = []*x509.Certificate{publicCert} + + // 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 its instance identity document, pkcs7 signature of the document, +// and a client created nonce. +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 := 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 + } + + clientNonce := data.Get("nonce").(string) + if clientNonce == "" { + return logical.ErrorResponse("missing nonce"), nil + } + + // Allowing the lengh of UUID for a client nonce. + if len(clientNonce) > 36 { + return logical.ErrorResponse("client nonce exceeding the limit of 36 characters"), nil + } + + // Validate the instance ID. + if err := validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil + } + + // Get the entry for the AMI used by the instance. + imageEntry, err := awsImage(req.Storage, identityDoc.ImageID) + if err != nil { + return nil, err + } + if imageEntry == nil { + return logical.ErrorResponse("image entry not found"), nil + } + + // Ensure that the TTL is less than the backend mount's max_ttl. + // If RoleTag is enabled, max_ttl on the RoleTag will be checked to be smaller than this, before being set. + maxTTL := imageEntry.MaxTTL + if maxTTL > b.System().MaxLeaseTTL() { + maxTTL = b.System().MaxLeaseTTL() + } + + // Get the entry from the identity whitelist, if there is one. + storedIdentity, err := whitelistIdentityValidEntry(req.Storage, identityDoc.InstanceID) + if err != nil { + return nil, err + } + + // 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. + err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry) + if err != nil { + return nil, err + } + } + + // Initially, set the policies that are applicable to the image. + // This may get updated if the image has RoleTag enabled. + policies := imageEntry.Policies + + rTagMaxTTL := time.Duration(0) + + // Role tag is enabled for the AMI. + if imageEntry.RoleTag != "" { + // Overwrite the policies with the ones returned from processing the role tag. + resp, err := handleRoleTagLogin(req.Storage, identityDoc, imageEntry) + if err != nil { + return nil, err + } + policies = resp.Policies + rTagMaxTTL = resp.MaxTTL + + // maxTTL should be set to least among these: image max_ttl, role-tag max_ttl, backend mount's max_ttl. + if maxTTL > rTagMaxTTL { + maxTTL = rTagMaxTTL + } + } + + // Save the login attempt in the identity whitelist. + currentTime := time.Now() + if storedIdentity == nil { + // ImageID, ClientNonce and CreationTime of the identity entry, + // once set, should never change. + storedIdentity = &whitelistIdentity{ + ImageID: identityDoc.ImageID, + ClientNonce: clientNonce, + CreationTime: currentTime, + } + } + + // PendingTime, LastUpdatedTime and ExpirationTime may change. + storedIdentity.LastUpdatedTime = currentTime + storedIdentity.ExpirationTime = currentTime.Add(maxTTL) + storedIdentity.PendingTime = identityDoc.PendingTime + + if err = setWhitelistIdentityEntry(req.Storage, identityDoc.InstanceID, storedIdentity); err != nil { + return nil, err + } + + return &logical.Response{ + Auth: &logical.Auth{ + Policies: policies, + Metadata: map[string]string{ + "instance_id": identityDoc.InstanceID, + "role_tag_max_ttl": rTagMaxTTL.String(), + }, + LeaseOptions: logical.LeaseOptions{ + Renewable: true, + // There is no TTL on the image/role-tag. Set it to mount's default TTL. + TTL: b.System().DefaultLeaseTTL(), + }, + }, + }, nil + +} + +// fetchRoleTagValue creates an AWS EC2 client and queries the tags +// attached to the instance identified by the given instanceID. +func fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) { + ec2Client, err := clientEC2(s) + if err != nil { + return "", err + } + + // Retrieve the instance tag with a "key" filter matching tagKey. + tagsOutput, err := ec2Client.DescribeTags(&ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("key"), + Values: []*string{ + aws.String(tagKey), + }, + }, + }, + }) + if err != nil { + return "", err + } + + if tagsOutput.Tags == nil || + len(tagsOutput.Tags) != 1 || + *tagsOutput.Tags[0].Key != tagKey || + *tagsOutput.Tags[0].ResourceType != "instance" { + return "", nil + } + + return *tagsOutput.Tags[0].Value, nil +} + +// handleRoleTagLogin is used to fetch the role tag if 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 handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) { + + // Make a secondary call to the AWS instance to see if the desired tag is set. + // NOTE: If AWS adds the instance tags as meta-data in the instance identity + // document, then it is better to look this information there instead of making + // another API call. Currently, we don't have an option but make this call. + rTagValue, err := fetchRoleTagValue(s, imageEntry.RoleTag) + if err != nil { + return nil, err + } + + if rTagValue == "" { + return nil, fmt.Errorf("missing tag with key %s on the instance", imageEntry.RoleTag) + } + + // Check if the role tag is blacklisted. + blacklistEntry, err := blacklistRoleTagValidEntry(s, rTagValue) + if err != nil { + return nil, err + } + if blacklistEntry != nil { + return nil, fmt.Errorf("role tag is blacklisted") + } + + rTag, err := parseRoleTagValue(rTagValue) + if err != nil { + return nil, err + } + + // Ensure that the policies on the RoleTag is a subset of policies on the image + if !strutil.StrListSubset(imageEntry.Policies, rTag.Policies) { + return nil, fmt.Errorf("policies on the role tag must be subset of policies on the image") + } + + // Create a HMAC of the plaintext value of role tag and compare it with the given value. + verified, err := verifyRoleTagValue(s, rTag) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("role tag signature mismatch") + } + return &roleTagLoginResponse{ + Policies: rTag.Policies, + MaxTTL: rTag.MaxTTL, + }, nil +} + +// pathLoginRenew is used to renew an authenticated token. +func (b *backend) pathLoginRenew( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + storedIdentity, err := whitelistIdentityValidEntry(req.Storage, req.Auth.Metadata["instance_id"]) + if err != nil { + return nil, err + } + + // For now, rTagMaxTTL is cached in internal data during login and used in renewal for + // setting the MaxTTL for the stored login identity entry. + // If `instance_id` can be used to fetch the role tag again (through an API), it would be good. + // For accessing the max_ttl, storing the entire identity document is too heavy. + rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) + if err != nil { + return nil, err + } + + imageEntry, err := awsImage(req.Storage, storedIdentity.ImageID) + if err != nil { + return nil, err + } + if imageEntry == nil { + return logical.ErrorResponse("image entry not found"), nil + } + + maxTTL := imageEntry.MaxTTL + if maxTTL > b.System().MaxLeaseTTL() { + maxTTL = b.System().MaxLeaseTTL() + } + if rTagMaxTTL > time.Duration(0) && maxTTL > rTagMaxTTL { + maxTTL = rTagMaxTTL + } + + // Only LastUpdatedTime and ExpirationTime change, none else. + currentTime := time.Now() + storedIdentity.LastUpdatedTime = currentTime + storedIdentity.ExpirationTime = currentTime.Add(maxTTL) + + if err = setWhitelistIdentityEntry(req.Storage, req.Auth.Metadata["instance_id"], storedIdentity); err != nil { + return nil, err + } + + return framework.LeaseExtend(req.Auth.TTL, maxTTL, b.System())(req, data) +} + +// Validates the instance by checking the InstanceState, InstanceStatus and SystemStatus +func validateInstanceStatus(instanceStatus *ec2.DescribeInstanceStatusOutput) error { + + if instanceStatus.InstanceStatuses == nil { + return fmt.Errorf("instance statuses not found") + } + + if len(instanceStatus.InstanceStatuses) != 1 { + return fmt.Errorf("length of instance statuses is more than 1") + } + + if instanceStatus.InstanceStatuses[0].InstanceState == nil { + return fmt.Errorf("instance state not found") + } + + // Instance should be in 'running'(code 16) state. + if *instanceStatus.InstanceStatuses[0].InstanceState.Code != 16 { + return fmt.Errorf("instance state is not 'running'") + } + + if instanceStatus.InstanceStatuses[0].InstanceStatus == nil { + return fmt.Errorf("instance status not found") + } + + // InstanceStatus should be 'ok' + if *instanceStatus.InstanceStatuses[0].InstanceStatus.Status != "ok" { + return fmt.Errorf("instance status is not 'ok'") + } + + if instanceStatus.InstanceStatuses[0].SystemStatus == nil { + return fmt.Errorf("system status not found") + } + + // SystemStatus should be 'ok' + if *instanceStatus.InstanceStatuses[0].SystemStatus.Status != "ok" { + return fmt.Errorf("system status is not 'ok'") + } + + return nil +} + +// 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"` + ImageID 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"` +} + +const pathLoginSyn = ` +Authenticates an EC2 instance with Vault. +` + +const pathLoginDesc = ` +An EC2 instance is authenticated using the instance identity document, the identity document's +PKCS#7 signature and a client created nonce. This nonce should be unique and should be used by +the instance for all future logins. + +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. + +The entries in the whitelist are not automatically deleted. Although, they will have an +expiration time set on the entry. There is a separate endpoint 'whitelist/identity/tidy', +that needs to be invoked to clean-up all the expired entries in the whitelist. +` diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go new file mode 100644 index 0000000000..2227202c55 --- /dev/null +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -0,0 +1,176 @@ +package aws + +import ( + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathWhitelistIdentity(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "whitelist/identity$", + Fields: map[string]*framework.FieldSchema{ + "instance_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "EC2 instance ID.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathWhitelistIdentityRead, + logical.DeleteOperation: b.pathWhitelistIdentityDelete, + }, + + HelpSynopsis: pathWhitelistIdentitySyn, + HelpDescription: pathWhitelistIdentityDesc, + } +} + +func pathListWhitelistIdentities(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "whitelist/identity/?", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathWhitelistIdentitiesList, + }, + + HelpSynopsis: pathListWhitelistIdentitiesHelpSyn, + HelpDescription: pathListWhitelistIdentitiesHelpDesc, + } +} + +// 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 un-expired item from the whitelist given an instance ID. +func whitelistIdentityValidEntry(s logical.Storage, instanceID string) (*whitelistIdentity, error) { + identity, err := whitelistIdentityEntry(s, instanceID) + if err != nil { + return nil, err + } + + // Don't return an expired item. + if identity == nil || time.Now().After(identity.ExpirationTime) { + return nil, nil + } + + return identity, 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 +} + +// pathWhitelistIdentityDelete is used to delete an entry from the identity whitelist given an instance ID. +func (b *backend) pathWhitelistIdentityDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + instanceID := data.Get("instance_id").(string) + if instanceID == "" { + return logical.ErrorResponse("missing instance_id"), nil + } + + err := req.Storage.Delete("whitelist/identity/" + instanceID) + if err != nil { + return nil, err + } + + return nil, nil +} + +// pathWhitelistIdentityRead is used to view an entry in the identity whitelist given an instance ID. +func (b *backend) pathWhitelistIdentityRead( + 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: map[string]interface{}{ + "imate_id": entry.ImageID, + "creation_time": entry.CreationTime.String(), + "expiration_time": entry.ExpirationTime.String(), + "client_nonce": entry.ClientNonce, + "pending_time": entry.PendingTime, + }, + }, nil +} + +// Struct to represent each item in the identity whitelist. +type whitelistIdentity struct { + ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` + PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` + ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` + CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` + LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` + ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` +} + +const pathWhitelistIdentitySyn = ` +Read or delete entries in the identity whitelist. +` + +const pathWhitelistIdentityDesc = ` +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. + +The entries in the whitelist are not automatically deleted. Although, they will have an +expiration time set on the entry. There is a separate endpoint 'whitelist/identity/tidy', +that needs to be invoked to clean-up all the expired entries in the whitelist. +` + +const pathListWhitelistIdentitiesHelpSyn = ` +List the items present in the identity whitelist. +` + +const pathListWhitelistIdentitiesHelpDesc = ` +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. +` diff --git a/builtin/credential/aws/path_whitelist_identity_tidy.go b/builtin/credential/aws/path_whitelist_identity_tidy.go new file mode 100644 index 0000000000..bbefba46da --- /dev/null +++ b/builtin/credential/aws/path_whitelist_identity_tidy.go @@ -0,0 +1,87 @@ +package aws + +import ( + "fmt" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathWhitelistIdentityTidy(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "whitelist/identity/tidy$", + 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.pathWhitelistIdentityTidyUpdate, + }, + + HelpSynopsis: pathWhitelistIdentityTidySyn, + HelpDescription: pathWhitelistIdentityTidyDesc, + } +} + +// pathWhitelistIdentityTidyUpdate is used to delete entries in the whitelist that are expired. +func (b *backend) pathWhitelistIdentityTidyUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + safety_buffer := data.Get("safety_buffer").(int) + + bufferDuration := time.Duration(safety_buffer) * time.Second + + identities, err := req.Storage.List("whitelist/identity/") + if err != nil { + return nil, err + } + + for _, instanceID := range identities { + identityEntry, err := req.Storage.Get("whitelist/identity/" + instanceID) + if err != nil { + return nil, fmt.Errorf("error fetching identity of instanceID %s: %s", instanceID, err) + } + + if identityEntry == nil { + return nil, fmt.Errorf("identity entry for instanceID %s is nil", instanceID) + } + + if identityEntry.Value == nil || len(identityEntry.Value) == 0 { + return nil, 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 nil, err + } + + if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { + if err := req.Storage.Delete("whitelist/identity" + instanceID); err != nil { + return nil, fmt.Errorf("error deleting identity of instanceID %s from storage: %s", instanceID, err) + } + } + } + + return nil, nil +} + +const pathWhitelistIdentityTidySyn = ` +Clean-up the whitelisted instance identity entries. +` + +const pathWhitelistIdentityTidyDesc = ` +When an instance identity is whitelisted, the expiration time of the whitelist +entry is set to the least amont 'max_ttl' of the registered AMI, 'max_ttl' of the +role tag and 'max_ttl' of the backend 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/role_tag_hmac_key.go b/builtin/credential/aws/role_tag_hmac_key.go new file mode 100644 index 0000000000..7b608de7c2 --- /dev/null +++ b/builtin/credential/aws/role_tag_hmac_key.go @@ -0,0 +1,44 @@ +package aws + +import ( + "fmt" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/logical" +) + +const ( + RoleTagHMACKeyLocation = "role_tag_hmac_key" +) + +// hmacKey returns the key to HMAC the RoleTag value. The key is valid per backend mount. +// If a key is not created for the mount, a new key will be created. +func hmacKey(s logical.Storage) (string, error) { + raw, err := s.Get(RoleTagHMACKeyLocation) + if err != nil { + return "", fmt.Errorf("failed to read key: %v", err) + } + + key := "" + if raw != nil { + key = string(raw.Value) + } + + if key == "" { + key, err = uuid.GenerateUUID() + if err != nil { + return "", fmt.Errorf("failed to generate uuid: %v", err) + } + if s != nil { + entry := &logical.StorageEntry{ + Key: RoleTagHMACKeyLocation, + Value: []byte(key), + } + if err := s.Put(entry); err != nil { + return "", fmt.Errorf("failed to persist key: %v", err) + } + } + } + + return key, nil +} 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, From 4b44d0689454aa33cb02907f89f4bbe7617140b3 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 6 Apr 2016 12:27:47 -0400 Subject: [PATCH 02/79] Test ConfigClient --- builtin/credential/aws/backend_test.go | 79 +++++++++++++++++++- builtin/credential/aws/path_config_client.go | 6 ++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 861ce8dc25..e342162d4f 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -17,11 +17,88 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal(err) } + data := map[string]interface{}{"access_key": "AKIAJBRHKV6EVTTNXDHA", + "secret_key": "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", + "region": "us-east-1", + } + + stepCreate := logicaltest.TestStep{ + Operation: logical.CreateOperation, + Path: "config/client", + Data: data, + } + + stepUpdate := logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data, + } + + data2 := map[string]interface{}{"access_key": "AKIAJBRHKV6EVTTNXDHA", + "secret_key": "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", + "region": "", + } + stepEmptyRegion := logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data2, + ErrorOk: true, + } + + data3 := map[string]interface{}{"access_key": "", + "secret_key": "mCtSM8ZUEQ3mOFVZYPBQkf2sO6F/W7a5TVzrl3Oj", + "region": "us-east-1", + } + stepInvalidAccessKey := logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data3, + ErrorOk: true, + } + + data4 := map[string]interface{}{"access_key": "accesskey", + "secret_key": "", + "region": "us-east-1", + } + 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{}, + Steps: []logicaltest.TestStep{ + stepCreate, + stepEmptyRegion, + stepInvalidAccessKey, + stepInvalidSecretKey, + stepUpdate, + }, }) + + configClientCreateRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/client", + Data: data, + Storage: storageView, + } + _, err = b.HandleRequest(configClientCreateRequest) + if err != nil { + t.Fatal(err) + } + + clientConfig, err := clientConfigEntry(storageView) + if err != nil { + t.Fatal(err) + } + if clientConfig.AccessKey != data["access_key"] || + clientConfig.SecretKey != data["secret_key"] || + clientConfig.Region != data["region"] { + t.Fatalf("bad: expected: %#v\ngot: %#v\n", data, clientConfig) + } } func TestBackend_parseRoleTagValue(t *testing.T) { diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 9d62271046..3e2cef0160 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -103,6 +103,9 @@ func (b *backend) pathConfigClientCreateUpdate( return nil, fmt.Errorf("missing access_key") } } + if configEntry.AccessKey == "" { + return nil, fmt.Errorf("invalid access_key") + } secretKeyStr, ok := data.GetOk("secret_key") if ok { @@ -112,6 +115,9 @@ func (b *backend) pathConfigClientCreateUpdate( return nil, fmt.Errorf("missing secret_key") } } + if configEntry.SecretKey == "" { + return nil, fmt.Errorf("invalid secret_key") + } entry, err := logical.StorageEntryJSON("config/client", configEntry) if err != nil { From 93f8ec30864ac6ba1443d3f87127c0572f7b9950 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 6 Apr 2016 14:41:36 -0400 Subject: [PATCH 03/79] Return un-expired entries from blacklist and whitelist --- builtin/credential/aws/path_blacklist_roletag.go | 15 --------------- builtin/credential/aws/path_login.go | 6 +++--- builtin/credential/aws/path_whitelist_identity.go | 15 --------------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index dd5f42f781..62dbea788c 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -62,21 +62,6 @@ func (b *backend) pathBlacklistRoleTagExistenceCheck(req *logical.Request, data return true, nil } -// Fetch an un-expired entry from the role tag blacklist for a given tag. -func blacklistRoleTagValidEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { - entry, err := blacklistRoleTagEntry(s, tag) - if err != nil { - return nil, err - } - - // Exclude the item if it is expired. - if entry == nil || time.Now().After(entry.ExpirationTime) { - return nil, nil - } - - return entry, nil -} - // Fetch an entry from the role tag blacklist for a given tag. func blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { entry, err := s.Get("blacklist/roletag/" + tag) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 2e4b00846e..1051b6c363 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -204,7 +204,7 @@ func (b *backend) pathLoginUpdate( } // Get the entry from the identity whitelist, if there is one. - storedIdentity, err := whitelistIdentityValidEntry(req.Storage, identityDoc.InstanceID) + storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDoc.InstanceID) if err != nil { return nil, err } @@ -331,7 +331,7 @@ func handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageE } // Check if the role tag is blacklisted. - blacklistEntry, err := blacklistRoleTagValidEntry(s, rTagValue) + blacklistEntry, err := blacklistRoleTagEntry(s, rTagValue) if err != nil { return nil, err } @@ -367,7 +367,7 @@ func handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageE func (b *backend) pathLoginRenew( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - storedIdentity, err := whitelistIdentityValidEntry(req.Storage, req.Auth.Metadata["instance_id"]) + storedIdentity, err := whitelistIdentityEntry(req.Storage, req.Auth.Metadata["instance_id"]) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 2227202c55..d5a138a744 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -51,21 +51,6 @@ func (b *backend) pathWhitelistIdentitiesList( return logical.ListResponse(identities), nil } -// Fetch an un-expired item from the whitelist given an instance ID. -func whitelistIdentityValidEntry(s logical.Storage, instanceID string) (*whitelistIdentity, error) { - identity, err := whitelistIdentityEntry(s, instanceID) - if err != nil { - return nil, err - } - - // Don't return an expired item. - if identity == nil || time.Now().After(identity.ExpirationTime) { - return nil, nil - } - - return identity, 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) From 8aae383e39d7bd797d91f92121e46038e8f13e7e Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 6 Apr 2016 18:53:26 +0000 Subject: [PATCH 04/79] Switch around some logic to be more consistent/readable and respect max TTL on initial token issuance. --- builtin/credential/aws/path_login.go | 42 +++++++++++++++------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 1051b6c363..fff8ff23b7 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -196,13 +196,6 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("image entry not found"), nil } - // Ensure that the TTL is less than the backend mount's max_ttl. - // If RoleTag is enabled, max_ttl on the RoleTag will be checked to be smaller than this, before being set. - maxTTL := imageEntry.MaxTTL - if maxTTL > b.System().MaxLeaseTTL() { - maxTTL = b.System().MaxLeaseTTL() - } - // Get the entry from the identity whitelist, if there is one. storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDoc.InstanceID) if err != nil { @@ -220,10 +213,14 @@ func (b *backend) pathLoginUpdate( } } - // Initially, set the policies that are applicable to the image. - // This may get updated if the image has RoleTag enabled. - policies := imageEntry.Policies + // Load the current values for max TTL and policies from the image entry, + // before checking for overriding by the RoleTag + maxTTL := b.System().MaxLeaseTTL() + if imageEntry.MaxTTL > time.Duration(0) && imageEntry.MaxTTL < maxTTL { + maxTTL = imageEntry.MaxTTL + } + policies := imageEntry.Policies rTagMaxTTL := time.Duration(0) // Role tag is enabled for the AMI. @@ -236,9 +233,8 @@ func (b *backend) pathLoginUpdate( policies = resp.Policies rTagMaxTTL = resp.MaxTTL - // maxTTL should be set to least among these: image max_ttl, role-tag max_ttl, backend mount's max_ttl. - if maxTTL > rTagMaxTTL { - maxTTL = rTagMaxTTL + if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < maxTTL { + maxTTL = resp.MaxTTL } } @@ -263,7 +259,7 @@ func (b *backend) pathLoginUpdate( return nil, err } - return &logical.Response{ + resp := &logical.Response{ Auth: &logical.Auth{ Policies: policies, Metadata: map[string]string{ @@ -272,11 +268,17 @@ func (b *backend) pathLoginUpdate( }, LeaseOptions: logical.LeaseOptions{ Renewable: true, - // There is no TTL on the image/role-tag. Set it to mount's default TTL. - TTL: b.System().DefaultLeaseTTL(), + TTL: b.System().DefaultLeaseTTL(), }, }, - }, nil + } + + // Enforce our image/role tag maximum TTL + if maxTTL < resp.Auth.TTL { + resp.Auth.TTL = maxTTL + } + + return resp, nil } @@ -389,9 +391,9 @@ func (b *backend) pathLoginRenew( return logical.ErrorResponse("image entry not found"), nil } - maxTTL := imageEntry.MaxTTL - if maxTTL > b.System().MaxLeaseTTL() { - maxTTL = b.System().MaxLeaseTTL() + maxTTL := b.System().MaxLeaseTTL() + if imageEntry.MaxTTL > time.Duration(0) && imageEntry.MaxTTL < maxTTL { + maxTTL = imageEntry.MaxTTL } if rTagMaxTTL > time.Duration(0) && maxTTL > rTagMaxTTL { maxTTL = rTagMaxTTL From 5d0b7d18fd0ce6cc4c59b06008909fa861224b0b Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 6 Apr 2016 15:37:12 -0400 Subject: [PATCH 05/79] Accept instance_id in the URL for whitelist endpoint --- builtin/credential/aws/path_whitelist_identity.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index d5a138a744..1ca1847d41 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -9,7 +9,7 @@ import ( func pathWhitelistIdentity(b *backend) *framework.Path { return &framework.Path{ - Pattern: "whitelist/identity$", + Pattern: "whitelist/identity/" + framework.GenericNameRegex("instance_id"), Fields: map[string]*framework.FieldSchema{ "instance_id": &framework.FieldSchema{ Type: framework.TypeString, @@ -117,7 +117,7 @@ func (b *backend) pathWhitelistIdentityRead( return &logical.Response{ Data: map[string]interface{}{ - "imate_id": entry.ImageID, + "image_id": entry.ImageID, "creation_time": entry.CreationTime.String(), "expiration_time": entry.ExpirationTime.String(), "client_nonce": entry.ClientNonce, From 717c3bf2001f1802c2a49ee3dde2bd2b73582665 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 6 Apr 2016 16:47:34 -0400 Subject: [PATCH 06/79] Changed the blacklist URL pattern to optionally accept base64 encoded role tags --- .../credential/aws/path_blacklist_roletag.go | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 62dbea788c..d0a61d7b26 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -1,6 +1,7 @@ package aws import ( + "encoding/base64" "time" "github.com/hashicorp/vault/logical" @@ -9,7 +10,7 @@ import ( func pathBlacklistRoleTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "blacklist/roletag$", + Pattern: "blacklist/roletag/" + "(?P.*)", Fields: map[string]*framework.FieldSchema{ "role_tag": &framework.FieldSchema{ Type: framework.TypeString, @@ -127,11 +128,24 @@ func (b *backend) pathBlacklistRoleTagRead( func (b *backend) pathBlacklistRoleTagUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - tag := data.Get("role_tag").(string) - if tag == "" { + // 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 the role tag from string form to a struct form. rTag, err := parseRoleTagValue(tag) if err != nil { From f59dcc8cc396a961f7b19e6a0d9a8da8e9ba11fa Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 6 Apr 2016 21:22:24 +0000 Subject: [PATCH 07/79] Update image output to show allow_instance_reboot value and keep policies in a list --- builtin/credential/aws/path_image.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index 0a047d3931..847aa2e2e3 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -133,9 +133,10 @@ func (b *backend) pathImageRead( return &logical.Response{ Data: map[string]interface{}{ - "role_tag": imageEntry.RoleTag, - "policies": strings.Join(imageEntry.Policies, ","), - "max_ttl": imageEntry.MaxTTL / time.Second, + "role_tag": imageEntry.RoleTag, + "policies": imageEntry.Policies, + "max_ttl": imageEntry.MaxTTL / time.Second, + "allow_instance_reboot": imageEntry.AllowInstanceReboot, }, }, nil } From c8afcafaf9fcc2950e1a8ab48c7149d6c254dcdd Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 6 Apr 2016 21:41:40 +0000 Subject: [PATCH 08/79] allow_instance_reboot -> allow_instance_migration --- builtin/credential/aws/path_image.go | 26 +++++++++++++------------- builtin/credential/aws/path_login.go | 25 ++++++++++++++++--------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index 847aa2e2e3..1e0d48b53f 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -37,10 +37,10 @@ func pathImage(b *backend) *framework.Path { Description: "Policies to be associated with the AMI.", }, - "allow_instance_reboot": &framework.FieldSchema{ + "allow_instance_migration": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, - Description: "If set, allows rebooting of the OS where the client resides. Essentially, this disables the client nonce check. Use with caution.", + 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.", }, }, @@ -133,10 +133,10 @@ func (b *backend) pathImageRead( return &logical.Response{ Data: map[string]interface{}{ - "role_tag": imageEntry.RoleTag, - "policies": imageEntry.Policies, - "max_ttl": imageEntry.MaxTTL / time.Second, - "allow_instance_reboot": imageEntry.AllowInstanceReboot, + "role_tag": imageEntry.RoleTag, + "policies": imageEntry.Policies, + "max_ttl": imageEntry.MaxTTL / time.Second, + "allow_instance_migration": imageEntry.AllowInstanceMigration, }, }, nil } @@ -165,11 +165,11 @@ func (b *backend) pathImageCreateUpdate( imageEntry.Policies = []string{"default"} } - allowInstanceRebootBool, ok := data.GetOk("allow_instance_reboot") + allowInstanceMigrationBool, ok := data.GetOk("allow_instance_migration") if ok { - imageEntry.AllowInstanceReboot = allowInstanceRebootBool.(bool) + imageEntry.AllowInstanceMigration = allowInstanceMigrationBool.(bool) } else if req.Operation == logical.CreateOperation { - imageEntry.AllowInstanceReboot = data.Get("allow_instance_reboot").(bool) + imageEntry.AllowInstanceMigration = data.Get("allow_instance_migration").(bool) } maxTTLInt, ok := data.GetOk("max_ttl") @@ -211,10 +211,10 @@ func (b *backend) pathImageCreateUpdate( // Struct to hold the information associated with an AMI ID in Vault. type awsImageEntry struct { - RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` - AllowInstanceReboot bool `json:"allow_instance_reboot" structs:"allow_instance_reboot" mapstructure:"allow_instance_reboot"` - MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` - Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` + 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"` } const pathImageSyn = ` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index fff8ff23b7..bcdc2c2c8c 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -74,20 +74,27 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist 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 an OS reboot and - // lost its client nonce. + // 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 is necessary, or the client must durably store the nonce. // - // If `allow_instance_reboot` property of the registered AMI, 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. + // If the `allow_instance_migration` property of the registered AMI 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 creterion and hence the `allow_instance_reboot` option should be used with caution. + // This is a weak criterion and hence the `allow_instance_migration` option + // should be used with caution. if clientNonce != storedIdentity.ClientNonce { - if !imageEntry.AllowInstanceReboot { + if !imageEntry.AllowInstanceMigration { return fmt.Errorf("client nonce mismatch") } - if imageEntry.AllowInstanceReboot && !givenPendingTime.After(storedPendingTime) { + if imageEntry.AllowInstanceMigration && !givenPendingTime.After(storedPendingTime) { return fmt.Errorf("client nonce mismatch and instance meta-data incorrect") } } From b954f6d9b0f17d95f365b468228cbb69d008e1c0 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 6 Apr 2016 19:25:35 -0400 Subject: [PATCH 09/79] Testing pathImage --- builtin/credential/aws/backend_test.go | 90 ++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index e342162d4f..10151dc680 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1,16 +1,18 @@ package aws import ( + "strings" "testing" + "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" ) func TestBackend_ConfigClient(t *testing.T) { config := logical.TestBackendConfig() - storageView := &logical.InmemStorage{} - config.StorageView = storageView + storage := &logical.InmemStorage{} + config.StorageView = storage b, err := Factory(config) if err != nil { @@ -83,14 +85,14 @@ func TestBackend_ConfigClient(t *testing.T) { Operation: logical.UpdateOperation, Path: "config/client", Data: data, - Storage: storageView, + Storage: storage, } _, err = b.HandleRequest(configClientCreateRequest) if err != nil { t.Fatal(err) } - clientConfig, err := clientConfigEntry(storageView) + clientConfig, err := clientConfigEntry(storage) if err != nil { t.Fatal(err) } @@ -101,6 +103,86 @@ func TestBackend_ConfigClient(t *testing.T) { } } +func TestBackend_PathImage(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "p,q,r,s", + "max_ttl": "2h", + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "image/ami-abcd123", + Data: data, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "image/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + 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 + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/ami-abcd123", + Data: data, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "image/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if !resp.Data["allow_instance_migration"].(bool) { + t.Fatal("bad: allow_instance_migration: expected:true got:false\n") + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.DeleteOperation, + Path: "image/ami-abcd123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "image/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_parseRoleTagValue(t *testing.T) { tag := "v1:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" expected := roleTag{ From 727a66b378641c4a0cae15d4c8ca97c5dd12a5ab Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 6 Apr 2016 20:59:05 -0400 Subject: [PATCH 10/79] Add existence check verification to config/client testcase --- builtin/credential/aws/backend_test.go | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 10151dc680..de9f6e9112 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -81,6 +81,21 @@ func TestBackend_ConfigClient(t *testing.T) { }, }) + 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 return 'false' for 'config/client'") + } + configClientCreateRequest := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/client", @@ -92,6 +107,21 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal(err) } + 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 return 'true' for 'config/client'") + } + clientConfig, err := clientConfigEntry(storage) if err != nil { t.Fatal(err) @@ -103,6 +133,9 @@ func TestBackend_ConfigClient(t *testing.T) { } } +func TestBackend_PathConfigCertificate(t *testing.T) { +} + func TestBackend_PathImage(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} From 392ce7e1f4066bcaf9cc929e8619e12df666f7a6 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 7 Apr 2016 11:26:10 -0400 Subject: [PATCH 11/79] Test path config/certificate --- builtin/credential/aws/backend_test.go | 102 +++++++++++++++++- .../credential/aws/path_config_certificate.go | 3 +- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index de9f6e9112..602692431e 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -93,7 +93,7 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal("existence check not found for path 'config/client'") } if exists { - t.Fatal("existence check should have return 'false' for 'config/client'") + t.Fatal("existence check should have returned 'false' for 'config/client'") } configClientCreateRequest := &logical.Request{ @@ -119,7 +119,7 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal("existence check not found for path 'config/client'") } if !exists { - t.Fatal("existence check should have return 'true' for 'config/client'") + t.Fatal("existence check should have returned 'true' for 'config/client'") } clientConfig, err := clientConfigEntry(storage) @@ -134,6 +134,104 @@ func TestBackend_ConfigClient(t *testing.T) { } func TestBackend_PathConfigCertificate(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{ + Operation: logical.CreateOperation, + Path: "config/certificate", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'config/certificate'") + } + if exists { + t.Fatal("existence check should have returned 'false' for 'config/certificate'") + } + + 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 +`, + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "config/certificate", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + checkFound, exists, err = b.HandleExistenceCheck(&logical.Request{ + Operation: logical.CreateOperation, + Path: "config/certificate", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if !checkFound { + t.Fatal("existence check not found for path 'config/certificate'") + } + if !exists { + t.Fatal("existence check should have returned 'true' for 'config/certificate'") + } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "config/certificate", + Storage: storage, + }) + 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)) + } } func TestBackend_PathImage(t *testing.T) { diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 624fd106b2..1a24ba895e 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -24,8 +24,7 @@ type dsaSignature struct { // 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) -const defaultAWSPublicCert = ` ------BEGIN CERTIFICATE----- +const defaultAWSPublicCert = `-----BEGIN CERTIFICATE----- MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z From bdfae8cd9e3a2149d3609845ba6e4251702adb48 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 7 Apr 2016 12:19:32 -0400 Subject: [PATCH 12/79] Remove certificate verification --- .../credential/aws/path_config_certificate.go | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 1a24ba895e..e198961fea 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -1,10 +1,7 @@ package aws import ( - "crypto" - "crypto/dsa" "crypto/x509" - "encoding/asn1" "encoding/base64" "encoding/pem" "fmt" @@ -189,28 +186,6 @@ func (b *backend) pathConfigCertificateCreateUpdate( return logical.ErrorResponse("invalid certificate; failed to decode and parse certificate"), nil } - // Before trusting the signature provided, validate its signature. - - // Extract the signature of the certificate. - dsaSig := &dsaSignature{} - dsaSigRest, err := asn1.Unmarshal(publicCert.Signature, dsaSig) - if err != nil { - return nil, err - } - if len(dsaSigRest) != 0 { - return nil, fmt.Errorf("failed to unmarshal certificate's signature") - } - - certHashFunc := crypto.SHA1.New() - - // RawTBSCertificate will contain the information in the certificate that is signed. - certHashFunc.Write(publicCert.RawTBSCertificate) - - // Verify the signature using the public key present in the certificate. - if !dsa.Verify(publicCert.PublicKey.(*dsa.PublicKey), certHashFunc.Sum(nil), dsaSig.R, dsaSig.S) { - return logical.ErrorResponse("invalid certificate; failed to verify certificate's signature"), nil - } - // If none of the checks fail, save the provided certificate. entry, err := logical.StorageEntryJSON("config/certificate", certEntry) if err != nil { From 7efc5eceb95f9047858925d66e0e45069c799092 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 7 Apr 2016 18:13:19 +0000 Subject: [PATCH 13/79] Add environment and EC2 instance metadata role providers for AWS creds. --- builtin/credential/aws/backend.go | 7 ++ builtin/credential/aws/client.go | 79 ++++++++++++++--- builtin/credential/aws/path_config_client.go | 91 +++++++++++++++----- builtin/credential/aws/path_login.go | 17 ++-- 4 files changed, 151 insertions(+), 43 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index b9221f2936..429595a473 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -1,6 +1,9 @@ package aws import ( + "sync" + + "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" @@ -57,6 +60,10 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { type backend struct { *framework.Backend Salt *salt.Salt + + configMutex sync.RWMutex + + ec2Client *ec2.EC2 } const backendHelp = ` diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 483fe6f6ae..cb53386e0a 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -2,47 +2,98 @@ package aws import ( "fmt" + "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" + "github.com/aws/aws-sdk-go/aws/ec2metadata" "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/logical" ) -// getClientConfig creates a aws-sdk-go config, which is used to create -// client that can interact with AWS API. This reads out the secret key -// and access key that was configured via 'config/client' endpoint and -// uses them to create credentials required to make the AWS API calls. -func getClientConfig(s logical.Storage) (*aws.Config, error) { +// 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) (*aws.Config, error) { // Read the configured secret key and access key config, err := clientConfigEntry(s) if err != nil { return nil, err } - if config == nil { - return nil, fmt.Errorf( - "client credentials haven't been configured. Please configure\n" + - "them at the 'config/client' endpoint") + + var providers []credentials.Provider + region := os.Getenv("AWS_REGION") + + if config != nil { + if config.Region != "" { + region = config.Region + } + + switch { + case config.AccessKey != "" && config.SecretKey != "": + providers = append(providers, &credentials.StaticProvider{ + Value: credentials.Value{ + AccessKeyID: config.AccessKey, + SecretAccessKey: config.SecretKey, + }}) + case config.AccessKey == "" && config.AccessKey == "": + // Attempt to get credentials from the IAM instance role below + default: // Have one or the other but not both and not neither + return nil, fmt.Errorf( + "static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both); configure or remove them at the 'config/client' endpoint") + } } + providers = append(providers, &credentials.EnvProvider{}) + // Create the credentials required to access the API. - creds := credentials.NewStaticCredentials(config.AccessKey, config.SecretKey, "") + providers = append(providers, &ec2rolecreds.EC2RoleProvider{ + Client: ec2metadata.New(session.New(&aws.Config{ + Region: aws.String(region), + HTTPClient: cleanhttp.DefaultClient(), + })), + ExpiryWindow: 15, + }) + + creds := credentials.NewChainCredentials(providers) + if creds == nil { + return nil, fmt.Errorf("could not compile valid credential providers from static config, environemnt, or instance metadata") + } // Create a config that can be used to make the API calls. return &aws.Config{ Credentials: creds, - Region: aws.String(config.Region), + Region: aws.String(region), HTTPClient: cleanhttp.DefaultClient(), }, nil } // clientEC2 creates a client to interact with AWS EC2 API. -func clientEC2(s logical.Storage) (*ec2.EC2, error) { - awsConfig, err := getClientConfig(s) +func (b *backend) clientEC2(s logical.Storage, recreate bool) (*ec2.EC2, error) { + if !recreate { + b.configMutex.RLock() + if b.ec2Client != nil { + defer b.configMutex.RUnlock() + return b.ec2Client, nil + } + b.configMutex.RUnlock() + } + + b.configMutex.Lock() + defer b.configMutex.Unlock() + + awsConfig, err := b.getClientConfig(s) if err != nil { return nil, err } - return ec2.New(session.New(awsConfig)), nil + + b.ec2Client = ec2.New(session.New(awsConfig)) + return b.ec2Client, nil } diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 3e2cef0160..ba082e0136 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -3,6 +3,7 @@ package aws import ( "fmt" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -23,8 +24,7 @@ func pathConfigClient(b *backend) *framework.Path { "region": &framework.FieldSchema{ Type: framework.TypeString, - Default: "us-east-1", - Description: "Region for API calls.", + Description: "Region for API calls. Defaults to the value of the AWS_REGION env var. Required.", }, }, @@ -32,6 +32,8 @@ func pathConfigClient(b *backend) *framework.Path { Callbacks: map[logical.Operation]framework.OperationFunc{ logical.CreateOperation: b.pathConfigClientCreateUpdate, + logical.DeleteOperation: b.pathConfigClientDelete, + logical.ReadOperation: b.pathConfigClientRead, logical.UpdateOperation: b.pathConfigClientCreateUpdate, }, @@ -44,6 +46,9 @@ func pathConfigClient(b *backend) *framework.Path { // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) pathConfigClientExistenceCheck( req *logical.Request, data *framework.FieldData) (bool, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + entry, err := clientConfigEntry(req.Storage) if err != nil { return false, err @@ -68,10 +73,51 @@ func clientConfigEntry(s logical.Storage) (*clientConfig, error) { return &result, nil } +func (b *backend) pathConfigClientRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + clientConfig, err := 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() + + err := req.Storage.Delete("config/client") + if err != nil { + b.configMutex.Unlock() + return nil, err + } + + b.configMutex.Unlock() + + _, err = b.clientEC2(req.Storage, true) + if err != nil { + return nil, fmt.Errorf("error creating client with updated credentials: %s", err) + } + + 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 := clientConfigEntry(req.Storage) if err != nil { @@ -88,35 +134,27 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry.Region = data.Get("region").(string) } - // Either a valid region needs to be provided or it should be left empty - // so a default value could take over. - if configEntry.Region == "" { - return nil, fmt.Errorf("invalid region") - - } + 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 { - if configEntry.AccessKey = data.Get("access_key").(string); configEntry.AccessKey == "" { - return nil, fmt.Errorf("missing access_key") - } - } - if configEntry.AccessKey == "" { - return nil, fmt.Errorf("invalid access_key") + // 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 { - if configEntry.SecretKey = data.Get("secret_key").(string); configEntry.SecretKey == "" { - return nil, fmt.Errorf("missing secret_key") - } - } - if configEntry.SecretKey == "" { - return nil, fmt.Errorf("invalid secret_key") + configEntry.SecretKey = data.Get("secret_key").(string) } entry, err := logical.StorageEntryJSON("config/client", configEntry) @@ -128,6 +166,19 @@ func (b *backend) pathConfigClientCreateUpdate( return nil, err } + if changedCreds { + // We have to be careful here to re-lock as we have a deferred unlock + // queued up and unlocking an unlocked mutex leads to a panic + b.configMutex.Unlock() + _, err = b.clientEC2(req.Storage, true) + b.configMutex.Lock() + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "error creating client with updated credentials: %s", err), + ), nil + } + } + return nil, nil } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index bcdc2c2c8c..399cd7e04e 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -41,9 +41,9 @@ func pathLogin(b *backend) *framework.Path { // validateInstanceID queries the status of the EC2 instance using AWS EC2 API and // checks if the instance is running and is healthy. -func validateInstanceID(s logical.Storage, instanceID string) error { +func (b *backend) validateInstanceID(s logical.Storage, instanceID string) error { // Create an EC2 client to pull the instance information - ec2Client, err := clientEC2(s) + ec2Client, err := b.clientEC2(s, false) if err != nil { return err } @@ -63,7 +63,6 @@ func validateInstanceID(s logical.Storage, instanceID string) error { // validateMetadata matches the given client nonce and pending time with the one cached // in the identity whitelist during the previous login. func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, imageEntry *awsImageEntry) error { - givenPendingTime, err := time.Parse(time.RFC3339, pendingTime) if err != nil { return err @@ -190,7 +189,7 @@ func (b *backend) pathLoginUpdate( } // Validate the instance ID. - if err := validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { + if err := b.validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil } @@ -233,7 +232,7 @@ func (b *backend) pathLoginUpdate( // Role tag is enabled for the AMI. if imageEntry.RoleTag != "" { // Overwrite the policies with the ones returned from processing the role tag. - resp, err := handleRoleTagLogin(req.Storage, identityDoc, imageEntry) + resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, imageEntry) if err != nil { return nil, err } @@ -291,8 +290,8 @@ func (b *backend) pathLoginUpdate( // fetchRoleTagValue creates an AWS EC2 client and queries the tags // attached to the instance identified by the given instanceID. -func fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) { - ec2Client, err := clientEC2(s) +func (b *backend) fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) { + ec2Client, err := b.clientEC2(s, false) if err != nil { return "", err } @@ -324,13 +323,13 @@ func fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) { // handleRoleTagLogin is used to fetch the role tag if 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 handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) { +func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) { // Make a secondary call to the AWS instance to see if the desired tag is set. // NOTE: If AWS adds the instance tags as meta-data in the instance identity // document, then it is better to look this information there instead of making // another API call. Currently, we don't have an option but make this call. - rTagValue, err := fetchRoleTagValue(s, imageEntry.RoleTag) + rTagValue, err := b.fetchRoleTagValue(s, imageEntry.RoleTag) if err != nil { return nil, err } From 8e3192eac1c3ce0e3e27fa5817056212223ddc32 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 7 Apr 2016 18:30:53 +0000 Subject: [PATCH 14/79] Fix typo --- builtin/credential/aws/path_login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 399cd7e04e..2af1a34ed9 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -321,7 +321,7 @@ func (b *backend) fetchRoleTagValue(s logical.Storage, tagKey string) (string, e return *tagsOutput.Tags[0].Value, nil } -// handleRoleTagLogin is used to fetch the role tag if the instance and verifies it to be correct. +// 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, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) { From 39c93b5e54247a5471d9a0d4da96f749bdc5fd33 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 7 Apr 2016 15:14:47 -0400 Subject: [PATCH 15/79] Remove todo and change clientNonce length limit to 128 chars --- builtin/credential/aws/path_image_tag.go | 3 --- builtin/credential/aws/path_login.go | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index f1bb95a7a3..fcb29f9070 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -150,9 +150,6 @@ func verifyRoleTagValue(s logical.Storage, rTag *roleTag) (bool, error) { return false, err } - // TODO: for testing purposes. Remove this. - key = "ab1728ba-5fd5-7298-d344-e9df1b09f5ea" - // Compute the HMAC of the plaintext hmacB64, err := createRoleTagHMACBase64(key, rTagPlainText) if err != nil { diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 2af1a34ed9..5e90043c29 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -184,8 +184,8 @@ func (b *backend) pathLoginUpdate( } // Allowing the lengh of UUID for a client nonce. - if len(clientNonce) > 36 { - return logical.ErrorResponse("client nonce exceeding the limit of 36 characters"), nil + if len(clientNonce) > 128 { + return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil } // Validate the instance ID. From eecdfdc1e94d3af7e3abd19da037f583dc1d850a Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 7 Apr 2016 18:42:29 -0400 Subject: [PATCH 16/79] Added disallow_reauthentication feature --- builtin/credential/aws/path_image.go | 31 +++++++++--- builtin/credential/aws/path_image_tag.go | 36 ++++++++++--- builtin/credential/aws/path_login.go | 50 +++++++++++++------ .../credential/aws/path_whitelist_identity.go | 13 ++--- 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index 1e0d48b53f..e37b216a0f 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -42,6 +42,12 @@ func pathImage(b *backend) *framework.Path { 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, allows a client to login only once.", + }, }, ExistenceCheck: b.pathImageExistenceCheck, @@ -133,10 +139,11 @@ func (b *backend) pathImageRead( return &logical.Response{ Data: map[string]interface{}{ - "role_tag": imageEntry.RoleTag, - "policies": imageEntry.Policies, - "max_ttl": imageEntry.MaxTTL / time.Second, - "allow_instance_migration": imageEntry.AllowInstanceMigration, + "role_tag": imageEntry.RoleTag, + "policies": imageEntry.Policies, + "max_ttl": imageEntry.MaxTTL / time.Second, + "allow_instance_migration": imageEntry.AllowInstanceMigration, + "disallow_reauthentication": imageEntry.DisallowReauthentication, }, }, nil } @@ -165,6 +172,13 @@ func (b *backend) pathImageCreateUpdate( imageEntry.Policies = []string{"default"} } + disallowReauthenticationBool, ok := data.GetOk("disallow_reauthentication") + if ok { + imageEntry.DisallowReauthentication = disallowReauthenticationBool.(bool) + } else if req.Operation == logical.CreateOperation { + imageEntry.DisallowReauthentication = data.Get("disallow_reauthentication").(bool) + } + allowInstanceMigrationBool, ok := data.GetOk("allow_instance_migration") if ok { imageEntry.AllowInstanceMigration = allowInstanceMigrationBool.(bool) @@ -211,10 +225,11 @@ func (b *backend) pathImageCreateUpdate( // Struct to hold the information associated with an AMI ID in Vault. type awsImageEntry struct { - 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"` + 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"` } const pathImageSyn = ` diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index fcb29f9070..c59841ce79 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "strconv" "strings" "time" @@ -35,6 +36,12 @@ func pathImageTag(b *backend) *framework.Path { Default: 0, Description: "The maximum allowed lease duration", }, + + "disallow_reauthentication": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set, allows the instance using this tag to login only once.", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -60,6 +67,8 @@ func (b *backend) pathImageTagUpdate( // Remove all other policies if 'root' is present. policies := policyutil.ParsePolicies(data.Get("policies").(string)) + disallowReauthentication := data.Get("disallow_reauthentication").(bool) + // Fetch the image entry corresponding to the AMI name imageEntry, err := awsImage(req.Storage, imageID) if err != nil { @@ -103,6 +112,7 @@ func (b *backend) pathImageTagUpdate( Nonce: nonce, Policies: policies, MaxTTL: maxTTL, + DisallowReauthentication: disallowReauthentication, }) if err != nil { return nil, err @@ -181,6 +191,9 @@ func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { // attach policies to value value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) + // attach disallow_reauthentication field + value = fmt.Sprintf("%s:d=%s", value, strconv.FormatBool(rTag.DisallowReauthentication)) + // attach max_ttl if it is provided if rTag.MaxTTL > time.Duration(0) { value = fmt.Sprintf("%s:t=%s", value, rTag.MaxTTL) @@ -218,13 +231,18 @@ func parseRoleTagValue(tag string) (*roleTag, error) { // Delete the version and nonce from the list. tagItems = tagItems[2:] for _, tagItem := range tagItems { + var err error switch { case strings.Contains(tagItem, "a="): rTag.ImageID = strings.TrimPrefix(tagItem, "a=") 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, "t="): - var err error rTag.MaxTTL, err = time.ParseDuration(strings.TrimPrefix(tagItem, "t=")) if err != nil { return nil, err @@ -261,12 +279,13 @@ func createRoleTagNonce() (string, error) { // Struct roleTag represents a role tag in a struc form. type roleTag struct { - Version string `json:"version" structs:"version" mapstructure:"version"` - 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"` - ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` - HMAC string `json:"hmac" structs:"hmac" mapstructure:"hmac"` + Version string `json:"version" structs:"version" mapstructure:"version"` + 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"` + ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` + HMAC string `json:"hmac" structs:"hmac" mapstructure:"hmac"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { @@ -275,7 +294,8 @@ func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && rTag1.MaxTTL == rTag2.MaxTTL && rTag1.ImageID == rTag2.ImageID && - rTag1.HMAC == rTag2.HMAC + rTag1.HMAC == rTag2.HMAC && + rTag1.DisallowReauthentication == rTag2.DisallowReauthentication } const pathImageTagSyn = ` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 5e90043c29..49f507e59c 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -61,8 +61,16 @@ func (b *backend) validateInstanceID(s logical.Storage, instanceID string) error } // validateMetadata matches the given client nonce and pending time with the one cached -// in the identity whitelist during the previous login. +// 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, imageEntry *awsImageEntry) 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 @@ -178,16 +186,6 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil } - clientNonce := data.Get("nonce").(string) - if clientNonce == "" { - return logical.ErrorResponse("missing nonce"), nil - } - - // Allowing the lengh of UUID for a client nonce. - if len(clientNonce) > 128 { - return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil - } - // Validate the instance ID. if err := b.validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil @@ -208,13 +206,22 @@ func (b *backend) pathLoginUpdate( return nil, err } + clientNonce := data.Get("nonce").(string) + if clientNonce == "" && !storedIdentity.DisallowReauthentication { + return logical.ErrorResponse("missing nonce"), nil + } + + // Allowing the lengh of UUID for a client nonce. + if len(clientNonce) > 128 { + return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil + } + // 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. - err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry) - if err != nil { + if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry); err != nil { return nil, err } } @@ -228,6 +235,7 @@ func (b *backend) pathLoginUpdate( policies := imageEntry.Policies rTagMaxTTL := time.Duration(0) + disallowReauthentication := imageEntry.DisallowReauthentication // Role tag is enabled for the AMI. if imageEntry.RoleTag != "" { @@ -239,6 +247,13 @@ func (b *backend) pathLoginUpdate( policies = resp.Policies rTagMaxTTL = resp.MaxTTL + // If imageEntry 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 + } + if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < maxTTL { maxTTL = resp.MaxTTL } @@ -256,10 +271,11 @@ func (b *backend) pathLoginUpdate( } } - // PendingTime, LastUpdatedTime and ExpirationTime may change. + // DisallowReauthentication, PendingTime, LastUpdatedTime and ExpirationTime may change. storedIdentity.LastUpdatedTime = currentTime storedIdentity.ExpirationTime = currentTime.Add(maxTTL) storedIdentity.PendingTime = identityDoc.PendingTime + storedIdentity.DisallowReauthentication = disallowReauthentication if err = setWhitelistIdentityEntry(req.Storage, identityDoc.InstanceID, storedIdentity); err != nil { return nil, err @@ -368,6 +384,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return &roleTagLoginResponse{ Policies: rTag.Policies, MaxTTL: rTag.MaxTTL, + DisallowReauthentication: rTag.DisallowReauthentication, }, nil } @@ -468,8 +485,9 @@ type identityDocument struct { } type roleTagLoginResponse struct { - Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` - MaxTTL time.Duration `json:"max_ttl", structs:"max_ttl" mapstructure:"max_ttl"` + 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 = ` diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 1ca1847d41..0e99810e5b 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -128,12 +128,13 @@ func (b *backend) pathWhitelistIdentityRead( // Struct to represent each item in the identity whitelist. type whitelistIdentity struct { - ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` - PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` - ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` - CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` - LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` - ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` + ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` + DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` + PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` + ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` + CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` + LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` + ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` } const pathWhitelistIdentitySyn = ` From a2d774b0fc0544811c08b66bc16327552a51bbf6 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 7 Apr 2016 23:19:06 -0400 Subject: [PATCH 17/79] Properly handle empty client nonce case when disallow_reauthentication is set --- builtin/credential/aws/path_image.go | 2 +- builtin/credential/aws/path_image_tag.go | 2 +- builtin/credential/aws/path_login.go | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index e37b216a0f..dde440e65c 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -46,7 +46,7 @@ func pathImage(b *backend) *framework.Path { "disallow_reauthentication": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, - Description: "If set, allows a client to login only once.", + Description: "If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/whitelist/identity endpoint.", }, }, diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index c59841ce79..dd823d41b9 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -40,7 +40,7 @@ func pathImageTag(b *backend) *framework.Path { "disallow_reauthentication": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, - Description: "If set, allows the instance using this tag to login only once.", + Description: "If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/whitelist/identity endpoint.", }, }, diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 49f507e59c..a7073d573d 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -71,6 +71,10 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist return fmt.Errorf("reauthentication is disabled") } + if clientNonce == "" { + return logical.ErrorResponse("missing nonce"), nil + } + givenPendingTime, err := time.Parse(time.RFC3339, pendingTime) if err != nil { return err @@ -207,7 +211,7 @@ func (b *backend) pathLoginUpdate( } clientNonce := data.Get("nonce").(string) - if clientNonce == "" && !storedIdentity.DisallowReauthentication { + if clientNonce == "" && storedIdentity == nil { return logical.ErrorResponse("missing nonce"), nil } From 8c439a2f61dfd278c54977851322be90e6d2901a Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 8 Apr 2016 01:35:30 -0400 Subject: [PATCH 18/79] Moved HMAC parsing inside parseRoleTagValue --- .../credential/aws/path_blacklist_roletag.go | 12 +---------- builtin/credential/aws/path_image_tag.go | 10 ++++++++- builtin/credential/aws/path_login.go | 21 +++++++------------ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index d0a61d7b26..44da4a9f6e 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -147,21 +147,11 @@ func (b *backend) pathBlacklistRoleTagUpdate( } // Parse the role tag from string form to a struct form. - rTag, err := parseRoleTagValue(tag) + rTag, err := parseRoleTagValue(req.Storage, tag) if err != nil { return nil, err } - // Build the plaintext form of the role tag and verify the prepared - // value using the HMAC. - verified, err := verifyRoleTagValue(req.Storage, rTag) - if err != nil { - return nil, err - } - if !verified { - return logical.ErrorResponse("role tag invalid"), nil - } - // Get the entry for the AMI used by the instance. imageEntry, err := awsImage(req.Storage, rTag.ImageID) if err != nil { diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index dd823d41b9..70a3e6cda9 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -203,7 +203,7 @@ func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { } // Parses the tag from string form into a struct form. -func parseRoleTagValue(tag string) (*roleTag, error) { +func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { tagItems := strings.Split(tag, ":") // Tag must contain version, nonce, policies and HMAC if len(tagItems) < 4 { @@ -256,6 +256,14 @@ func parseRoleTagValue(tag string) (*roleTag, error) { return nil, fmt.Errorf("missing image ID") } + // Create a HMAC of the plaintext value of role tag and compare it with the given value. + verified, err := verifyRoleTagValue(s, rTag) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("role tag signature mismatch") + } return rTag, nil } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index a7073d573d..ab72e77236 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -72,7 +72,7 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist } if clientNonce == "" { - return logical.ErrorResponse("missing nonce"), nil + return fmt.Errorf("missing nonce") } givenPendingTime, err := time.Parse(time.RFC3339, pendingTime) @@ -358,6 +358,12 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return nil, fmt.Errorf("missing tag with key %s on the instance", imageEntry.RoleTag) } + // Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC. + rTag, err := parseRoleTagValue(s, rTagValue) + if err != nil { + return nil, err + } + // Check if the role tag is blacklisted. blacklistEntry, err := blacklistRoleTagEntry(s, rTagValue) if err != nil { @@ -367,24 +373,11 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return nil, fmt.Errorf("role tag is blacklisted") } - rTag, err := parseRoleTagValue(rTagValue) - if err != nil { - return nil, err - } - // Ensure that the policies on the RoleTag is a subset of policies on the image if !strutil.StrListSubset(imageEntry.Policies, rTag.Policies) { return nil, fmt.Errorf("policies on the role tag must be subset of policies on the image") } - // Create a HMAC of the plaintext value of role tag and compare it with the given value. - verified, err := verifyRoleTagValue(s, rTag) - if err != nil { - return nil, err - } - if !verified { - return nil, fmt.Errorf("role tag signature mismatch") - } return &roleTagLoginResponse{ Policies: rTag.Policies, MaxTTL: rTag.MaxTTL, From 7a437d139e62ca24e8133b1bfd41f5dfde956d13 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 8 Apr 2016 02:05:57 -0400 Subject: [PATCH 19/79] Rename 'name' to 'ami_id' for clarity --- builtin/credential/aws/backend.go | 4 ++-- builtin/credential/aws/path_image.go | 22 +++++++++++----------- builtin/credential/aws/path_image_tag.go | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 429595a473..7a97efc714 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -71,7 +71,7 @@ AWS auth backend takes in a AWS EC2 instance identity document, its PKCS#7 signa and a client created nonce to authenticates the instance with Vault. Authentication is backed by a preconfigured association of AMIs to Vault's policies -through 'image/' endpoint. For instances that share an AMI, an instance tag can -be created through 'image//tag'. This tag should be attached to the EC2 instance +through 'image/' endpoint. For instances that share an AMI, an instance tag can +be created through 'image//tag'. This tag should be attached to the EC2 instance before the instance attempts to login to Vault. ` diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index dde440e65c..28fd106d49 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -12,11 +12,11 @@ import ( func pathImage(b *backend) *framework.Path { return &framework.Path{ - Pattern: "image/" + framework.GenericNameRegex("name"), + Pattern: "image/" + framework.GenericNameRegex("ami_id"), Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ + "ami_id": &framework.FieldSchema{ Type: framework.TypeString, - Description: "AMI name to be mapped.", + Description: "AMI ID to be mapped.", }, "role_tag": &framework.FieldSchema{ @@ -82,7 +82,7 @@ func pathListImages(b *backend) *framework.Path { // Establishes dichotomy of request operation between CreateOperation and UpdateOperation. // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) pathImageExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - entry, err := awsImage(req.Storage, strings.ToLower(data.Get("name").(string))) + entry, err := awsImage(req.Storage, strings.ToLower(data.Get("ami_id").(string))) if err != nil { return false, err } @@ -90,8 +90,8 @@ func (b *backend) pathImageExistenceCheck(req *logical.Request, data *framework. } // awsImage is used to get the information registered for the given AMI ID. -func awsImage(s logical.Storage, name string) (*awsImageEntry, error) { - entry, err := s.Get("image/" + strings.ToLower(name)) +func awsImage(s logical.Storage, amiID string) (*awsImageEntry, error) { + entry, err := s.Get("image/" + strings.ToLower(amiID)) if err != nil { return nil, err } @@ -109,7 +109,7 @@ func awsImage(s logical.Storage, name string) (*awsImageEntry, error) { // pathImageDelete is used to delete the information registered for a given AMI ID. func (b *backend) pathImageDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - err := req.Storage.Delete("image/" + strings.ToLower(data.Get("name").(string))) + err := req.Storage.Delete("image/" + strings.ToLower(data.Get("ami_id").(string))) if err != nil { return nil, err } @@ -129,7 +129,7 @@ func (b *backend) pathImageList( // pathImageRead is used to view the information registered for a given AMI ID. func (b *backend) pathImageRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - imageEntry, err := awsImage(req.Storage, strings.ToLower(data.Get("name").(string))) + imageEntry, err := awsImage(req.Storage, strings.ToLower(data.Get("ami_id").(string))) if err != nil { return nil, err } @@ -152,9 +152,9 @@ func (b *backend) pathImageRead( func (b *backend) pathImageCreateUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - imageID := strings.ToLower(data.Get("name").(string)) + imageID := strings.ToLower(data.Get("ami_id").(string)) if imageID == "" { - return logical.ErrorResponse("missing AMI name"), nil + return logical.ErrorResponse("missing ami_id"), nil } imageEntry, err := awsImage(req.Storage, imageID) @@ -243,7 +243,7 @@ authorization for the instance to access Vault's resources is determined by the policies that are associated to the AMI through this endpoint. In case the AMI is shared by many instances, then a role tag can be created -through the endpoint 'image//tag'. This tag needs to be applied on the +through the endpoint 'image//tag'. This tag needs to be applied on the instance before it attempts to login to Vault. The policies on the tag should be a subset of policies that are associated to the AMI in this endpoint. In order to enable login using tags, RoleTag needs to be enabled in this endpoint. diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index 70a3e6cda9..5fb0edaa21 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -19,11 +19,11 @@ const roleTagVersion = "v1" func pathImageTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "image/" + framework.GenericNameRegex("name") + "/tag$", + Pattern: "image/" + framework.GenericNameRegex("ami_id") + "/tag$", Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ + "ami_id": &framework.FieldSchema{ Type: framework.TypeString, - Description: "AMI name to create a tag for.", + Description: "AMI ID to create a tag for.", }, "policies": &framework.FieldSchema{ @@ -58,9 +58,9 @@ func pathImageTag(b *backend) *framework.Path { func (b *backend) pathImageTagUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - imageID := strings.ToLower(data.Get("name").(string)) + imageID := strings.ToLower(data.Get("ami_id").(string)) if imageID == "" { - return logical.ErrorResponse("missing image name"), nil + return logical.ErrorResponse("missing ami_id"), nil } // Parse the given policies into a slice and add 'default' if not provided. @@ -69,7 +69,7 @@ func (b *backend) pathImageTagUpdate( disallowReauthentication := data.Get("disallow_reauthentication").(bool) - // Fetch the image entry corresponding to the AMI name + // Fetch the image entry corresponding to the AMI ID imageEntry, err := awsImage(req.Storage, imageID) if err != nil { return nil, err @@ -183,9 +183,9 @@ func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { value = fmt.Sprintf("%s:%s", value, rTag.Nonce) if rTag.ImageID == "" { - return "", fmt.Errorf("missing ami_name") + return "", fmt.Errorf("missing ami_id") } - // attach ami_name to the value + // attach ami_id to the value value = fmt.Sprintf("%s:a=%s", value, rTag.ImageID) // attach policies to value @@ -315,7 +315,7 @@ When an AMI is used by more than one EC2 instance, policies to be associated during login are determined by a particular tag on the instance. This tag can be created using this endpoint. -A RoleTag setting needs to be enabled in 'image/' endpoint, to be able +A RoleTag setting needs to be enabled in 'image/' endpoint, to be able to create a tag. Also, the policies to be associated with the tag should be a subset of the policies associated with the regisred AMI. From a1ca3228b35eb6d6735ccb9ce2e05baaac5f7d3b Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 8 Apr 2016 11:00:16 -0400 Subject: [PATCH 20/79] Make client nonce optional even during first login, when disallow_reauthentication is set --- builtin/credential/aws/path_login.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index ab72e77236..e356545993 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -71,10 +71,6 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist return fmt.Errorf("reauthentication is disabled") } - if clientNonce == "" { - return fmt.Errorf("missing nonce") - } - givenPendingTime, err := time.Parse(time.RFC3339, pendingTime) if err != nil { return err @@ -211,14 +207,6 @@ func (b *backend) pathLoginUpdate( } clientNonce := data.Get("nonce").(string) - if clientNonce == "" && storedIdentity == nil { - return logical.ErrorResponse("missing nonce"), nil - } - - // Allowing the lengh of UUID for a client nonce. - if len(clientNonce) > 128 { - return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil - } // This is NOT a first login attempt from the client. if storedIdentity != nil { @@ -281,6 +269,17 @@ func (b *backend) pathLoginUpdate( 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 lengh 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 } From 07ab072ac7694ba8458769428927189cdf378aa4 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 8 Apr 2016 14:56:27 -0400 Subject: [PATCH 21/79] Tested parseRoleTagValue --- builtin/credential/aws/backend_test.go | 90 ++++++++++++++++---------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 602692431e..2a69d8cfcb 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -315,44 +315,66 @@ func TestBackend_PathImage(t *testing.T) { } func TestBackend_parseRoleTagValue(t *testing.T) { - tag := "v1:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" - expected := roleTag{ - Version: "v1", - Nonce: "XwuKhyyBNJc=", - Policies: []string{"root"}, - MaxTTL: 10800000000000, - ImageID: "ami-fce3c696", - HMAC: "lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=", + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + b, err := Factory(config) + if err != nil { + t.Fatal(err) } - actual, err := parseRoleTagValue(tag) + + data := map[string]interface{}{ + "policies": "p,q,r,s", + "max_ttl": "120s", + "role_tag": "VaultRole", + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "image/abcd-123", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "image/abcd-123", + Storage: storage, + }) + if resp == nil { + t.Fatalf("expected an image entry for abcd-123") + } + + data2 := map[string]interface{}{ + "policies": "p,q,r,s", + } + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/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) + + rTag, err := parseRoleTagValue(storage, tagValue) if err != nil { t.Fatalf("err: %s", err) } - if !actual.Equal(&expected) { - t.Fatalf("err: expected:%#v \ngot: %#v\n", expected, actual) + if rTag == nil { + t.Fatalf("failed to parse role tag") } - - tag = "v2:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" - actual, err = parseRoleTagValue(tag) - if err == nil { - t.Fatalf("err: expected error due to invalid role tag version", err) - } - - tag = "v1:XwuKhyyBNJc=:a=ami-fce3c696:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" - expected = roleTag{ - Version: "v1", - Nonce: "XwuKhyyBNJc=", - ImageID: "ami-fce3c696", - HMAC: "lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=", - } - actual, err = parseRoleTagValue(tag) - if err != nil { - t.Fatalf("err: %s", err) - } - - tag = "v1:XwuKhyyBNJc=:p=ami-fce3c696:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" - actual, err = parseRoleTagValue(tag) - if err == nil { - t.Fatalf("err: expected error due to missing image ID", err) + if rTag.Version != "v1" || + !policyutil.EquivalentPolicies(rTag.Policies, []string{"p", "q", "r", "s"}) || + rTag.ImageID != "abcd-123" { + t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag) } } From 943cc345fe43aaf097e9e8f91ce8b46bc0e096f2 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 8 Apr 2016 15:43:24 -0400 Subject: [PATCH 22/79] Tested pathImageTag --- builtin/credential/aws/backend_test.go | 55 ++++++++++++++++++++++++++ builtin/credential/aws/path_login.go | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 2a69d8cfcb..fa3ea5262f 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -378,3 +378,58 @@ func TestBackend_parseRoleTagValue(t *testing.T) { t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag) } } + +func TestBackend_PathImageTag(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "p,q,r,s", + "max_ttl": "120s", + "role_tag": "VaultRole", + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "image/abcd-123", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "image/abcd-123", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatalf("failed to find an entry for ami_id: abcd-123") + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/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 ami_id: abcd-123") + } + if resp.IsError() { + t.Fatalf("failed to create a tag on ami_id: 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) + } +} diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index e356545993..22f946b6c5 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -275,7 +275,7 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("missing nonce"), nil } - // Limit the lengh to a reasonable length. + // 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 } From 9dc519ae689437d5011d04197cd87c217a78ffe3 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 8 Apr 2016 18:29:54 -0400 Subject: [PATCH 23/79] Return 4xx error at appropriate places --- builtin/credential/aws/path_login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 22f946b6c5..0ce423300d 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -214,7 +214,7 @@ func (b *backend) pathLoginUpdate( // of the identity document is not before the pending time of the document // with which previous login was made. if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry); err != nil { - return nil, err + return logical.ErrorResponse(err.Error()), nil } } From 2122716f17f2a521470fa67db1c882f783aafde5 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 11 Apr 2016 16:18:41 -0400 Subject: [PATCH 24/79] Added test case TestBackend_PathBlacklistRoleTag --- builtin/credential/aws/backend_test.go | 92 ++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index fa3ea5262f..0460eecdc9 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -433,3 +433,95 @@ func TestBackend_PathImageTag(t *testing.T) { t.Fatalf("role tag not present in the response data: %#v\n", resp.Data) } } + +func TestBackend_PathBlacklistRoleTag(t *testing.T) { + storage := &logical.InmemStorage{} + config := logical.TestBackendConfig() + config.StorageView = storage + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "ami_id": "abcd-123", + "policies": "p,q,r,s", + "role_tag": "VaultRole", + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.CreateOperation, + Path: "image/abcd-123", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + data2 := map[string]interface{}{ + "policies": "p,q,r,s", + } + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/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 ami_id: abcd-123") + } + if resp.IsError() { + t.Fatalf("failed to create a tag on ami_id: 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) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "blacklist/roletag/" + tag, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil { + t.Fatalf("failed to blacklist the roletag: %s\n", tag) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "blacklist/roletag/" + 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"]) + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.DeleteOperation, + Path: "blacklist/roletag/" + tag, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + tagEntry, err := blacklistRoleTagEntry(storage, tag) + if err != nil { + t.Fatal(err) + } + if tagEntry != nil { + t.Fatalf("role tag should not have been present: %s\n", tag) + } +} From 9e57f66284aad7326b0cab4012f631df94182f58 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 11 Apr 2016 19:21:53 -0400 Subject: [PATCH 25/79] Added acceptance test for login endpoint --- builtin/credential/aws/backend_test.go | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 0460eecdc9..1eb91aeb8c 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1,6 +1,8 @@ package aws import ( + "fmt" + "os" "strings" "testing" @@ -525,3 +527,140 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatalf("role tag should not have been present: %s\n", tag) } } + +// This is an acceptance test. +func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { + if os.Getenv(logicaltest.TestEnvVar) == "" { + t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) + return + } + + storage := &logical.InmemStorage{} + config := logical.TestBackendConfig() + config.StorageView = storage + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + clientConfig := map[string]interface{}{ + "access_key": os.Getenv("AWS_AUTH_ACCESS_KEY"), + "secret_key": os.Getenv("AWS_AUTH_SECRET_KEY"), + "region": os.Getenv("AWS_AUTH_REGION"), + } + if clientConfig["access_key"] == "" || + clientConfig["secret_key"] == "" { + t.Fatalf("credentials not configured") + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Storage: storage, + Path: "config/client", + Data: clientConfig, + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "root", + "max_ttl": "120s", + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/ami-fce3c696", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + loginInput := map[string]interface{}{"pkcs7": `MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewog +ICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMu +NjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAi +MjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJv +ZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJ +ZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVu +ZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4 +ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVn +aW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkw +FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6 +b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMx +CwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEW +BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U +0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA`, + "nonce": "vault-client-nonce", + } + + 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 { + t.Fatalf("login attempt failed") + } + + // try 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 { + t.Fatalf("login attempt failed") + } + + //instanceID := resp.Auth.Metadata.(map[string]string)["instance_id"] + 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") + } + + wlRequest := &logical.Request{ + Operation: logical.ReadOperation, + Path: "whitelist/identity/" + instanceID, + Storage: storage, + } + resp, err = b.HandleRequest(wlRequest) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil || resp.Data["image_id"] != "ami-fce3c696" { + t.Fatalf("failed to read whitelist identity") + } + + 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") + } + + // try 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 { + t.Fatalf("login attempt failed") + } +} From 9a988ffdee86a8b41cf3fbbd82c94875b2492d2e Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 11 Apr 2016 19:46:44 -0400 Subject: [PATCH 26/79] Docs update --- builtin/credential/aws/backend_test.go | 2 +- .../credential/aws/path_blacklist_roletag.go | 6 +- .../aws/path_blacklist_roletag_tidy.go | 2 +- .../credential/aws/path_config_certificate.go | 2 +- builtin/credential/aws/path_config_client.go | 4 +- builtin/credential/aws/path_image.go | 4 +- builtin/credential/aws/path_image_tag.go | 26 +- builtin/credential/aws/path_login.go | 17 +- .../credential/aws/path_whitelist_identity.go | 6 +- website/source/docs/auth/aws.html.md | 1006 +++++++++++++++++ website/source/layouts/docs.erb | 4 + 11 files changed, 1047 insertions(+), 32 deletions(-) create mode 100644 website/source/docs/auth/aws.html.md diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 1eb91aeb8c..7135f4a545 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -642,7 +642,7 @@ BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U if err != nil { t.Fatal(err) } - if resp == nil || resp.Data == nil || resp.Data["image_id"] != "ami-fce3c696" { + if resp == nil || resp.Data == nil || resp.Data["ami_id"] != "ami-fce3c696" { t.Fatalf("failed to read whitelist identity") } diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 44da4a9f6e..9100f2346b 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -10,11 +10,11 @@ import ( func pathBlacklistRoleTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "blacklist/roletag/" + "(?P.*)", + Pattern: "blacklist/roletag/(?P.*)", Fields: map[string]*framework.FieldSchema{ "role_tag": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Role tag that needs be blacklisted", + Description: "Role tag that needs be blacklisted. The tag can be supplied as-is, or can be base64 encoded.", }, }, @@ -153,7 +153,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( } // Get the entry for the AMI used by the instance. - imageEntry, err := awsImage(req.Storage, rTag.ImageID) + imageEntry, err := awsImage(req.Storage, rTag.AmiID) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_blacklist_roletag_tidy.go b/builtin/credential/aws/path_blacklist_roletag_tidy.go index ccbc20f62f..7de0f0808a 100644 --- a/builtin/credential/aws/path_blacklist_roletag_tidy.go +++ b/builtin/credential/aws/path_blacklist_roletag_tidy.go @@ -15,7 +15,7 @@ func pathBlacklistRoleTagTidy(b *backend) *framework.Path { "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Default: 259200, // 72h - Description: `The amount of extra time that must have passed beyond the roletag's + Description: `The amount of extra time that must have passed beyond the roletag expiration, before it is removed from the backend storage.`, }, }, diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index e198961fea..633d90bebf 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -48,7 +48,7 @@ func pathConfigCertificate(b *backend) *framework.Path { "aws_public_cert": &framework.FieldSchema{ Type: framework.TypeString, Default: defaultAWSPublicCert, - Description: "AWS Public key required to verify PKCS7 signature.", + Description: "AWS Public key required to verify PKCS7 signature of the EC2 instance metadata.", }, }, diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index ba082e0136..fe40102986 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -14,12 +14,12 @@ func pathConfigClient(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "access_key": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Access key with permission to query instance metadata.", + Description: "AWS Access key with permissions to query EC2 instance metadata.", }, "secret_key": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Secret key with permission to query instance metadata.", + Description: "AWS Secret key with permissions to query EC2 instance metadata.", }, "region": &framework.FieldSchema{ diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index 28fd106d49..6f41a8ff41 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -28,7 +28,7 @@ func pathImage(b *backend) *framework.Path { "max_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Default: 0, - Description: "The maximum allowed lease duration", + Description: "The maximum allowed lease duration.", }, "policies": &framework.FieldSchema{ @@ -46,7 +46,7 @@ func pathImage(b *backend) *framework.Path { "disallow_reauthentication": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, - Description: "If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/whitelist/identity endpoint.", + 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/whitelist/identity/' endpoint.", }, }, diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index 5fb0edaa21..f278b3787f 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -19,7 +19,7 @@ const roleTagVersion = "v1" func pathImageTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "image/" + framework.GenericNameRegex("ami_id") + "/tag$", + Pattern: "image/" + framework.GenericNameRegex("ami_id") + "/roletag$", Fields: map[string]*framework.FieldSchema{ "ami_id": &framework.FieldSchema{ Type: framework.TypeString, @@ -34,13 +34,13 @@ func pathImageTag(b *backend) *framework.Path { "max_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Default: 0, - Description: "The maximum allowed lease duration", + Description: "The maximum allowed lease duration.", }, "disallow_reauthentication": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, - Description: "If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/whitelist/identity endpoint.", + 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/whitelist/identity/' endpoint.", }, }, @@ -58,8 +58,8 @@ func pathImageTag(b *backend) *framework.Path { func (b *backend) pathImageTagUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - imageID := strings.ToLower(data.Get("ami_id").(string)) - if imageID == "" { + amiID := strings.ToLower(data.Get("ami_id").(string)) + if amiID == "" { return logical.ErrorResponse("missing ami_id"), nil } @@ -70,7 +70,7 @@ func (b *backend) pathImageTagUpdate( disallowReauthentication := data.Get("disallow_reauthentication").(bool) // Fetch the image entry corresponding to the AMI ID - imageEntry, err := awsImage(req.Storage, imageID) + imageEntry, err := awsImage(req.Storage, amiID) if err != nil { return nil, err } @@ -108,7 +108,7 @@ func (b *backend) pathImageTagUpdate( // Attach version, nonce, policies and maxTTL to the role tag value. rTagValue, err := prepareRoleTagPlainValue(&roleTag{Version: roleTagVersion, - ImageID: imageID, + AmiID: amiID, Nonce: nonce, Policies: policies, MaxTTL: maxTTL, @@ -182,11 +182,11 @@ func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { // attach nonce to the value value = fmt.Sprintf("%s:%s", value, rTag.Nonce) - if rTag.ImageID == "" { + if rTag.AmiID == "" { return "", fmt.Errorf("missing ami_id") } // attach ami_id to the value - value = fmt.Sprintf("%s:a=%s", value, rTag.ImageID) + value = fmt.Sprintf("%s:a=%s", value, rTag.AmiID) // attach policies to value value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) @@ -234,7 +234,7 @@ func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { var err error switch { case strings.Contains(tagItem, "a="): - rTag.ImageID = strings.TrimPrefix(tagItem, "a=") + rTag.AmiID = strings.TrimPrefix(tagItem, "a=") case strings.Contains(tagItem, "p="): rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",") case strings.Contains(tagItem, "d="): @@ -252,7 +252,7 @@ func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { } } } - if rTag.ImageID == "" { + if rTag.AmiID == "" { return nil, fmt.Errorf("missing image ID") } @@ -291,7 +291,7 @@ type roleTag struct { 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"` - ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` + AmiID string `json:"ami_id" structs:"ami_id" mapstructure:"ami_id"` HMAC string `json:"hmac" structs:"hmac" mapstructure:"hmac"` DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` } @@ -301,7 +301,7 @@ func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { rTag1.Nonce == rTag2.Nonce && policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && rTag1.MaxTTL == rTag2.MaxTTL && - rTag1.ImageID == rTag2.ImageID && + rTag1.AmiID == rTag2.AmiID && rTag1.HMAC == rTag2.HMAC && rTag1.DisallowReauthentication == rTag2.DisallowReauthentication } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 0ce423300d..b2d3171ca2 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -26,7 +26,7 @@ func pathLogin(b *backend) *framework.Path { "nonce": &framework.FieldSchema{ Type: framework.TypeString, - Description: "The nonce created by a client of this backend.", + Description: "The nonce created by a client of this backend. Nonce is used to avoid replay attacks. When the instances are configured to be allowed to login only once, nonce parameter is of no use and hence can be skipped.", }, }, @@ -192,7 +192,7 @@ func (b *backend) pathLoginUpdate( } // Get the entry for the AMI used by the instance. - imageEntry, err := awsImage(req.Storage, identityDoc.ImageID) + imageEntry, err := awsImage(req.Storage, identityDoc.AmiID) if err != nil { return nil, err } @@ -254,10 +254,10 @@ func (b *backend) pathLoginUpdate( // Save the login attempt in the identity whitelist. currentTime := time.Now() if storedIdentity == nil { - // ImageID, ClientNonce and CreationTime of the identity entry, + // AmiID, ClientNonce and CreationTime of the identity entry, // once set, should never change. storedIdentity = &whitelistIdentity{ - ImageID: identityDoc.ImageID, + AmiID: identityDoc.AmiID, ClientNonce: clientNonce, CreationTime: currentTime, } @@ -363,6 +363,11 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return nil, err } + // Check if the role tag belongs to the AMI ID of the instance. + if rTag.AmiID != identityDoc.AmiID { + return nil, fmt.Errorf("role tag does not belong to the instance's AMI ID.") + } + // Check if the role tag is blacklisted. blacklistEntry, err := blacklistRoleTagEntry(s, rTagValue) if err != nil { @@ -402,7 +407,7 @@ func (b *backend) pathLoginRenew( return nil, err } - imageEntry, err := awsImage(req.Storage, storedIdentity.ImageID) + imageEntry, err := awsImage(req.Storage, storedIdentity.AmiID) if err != nil { return nil, err } @@ -475,7 +480,7 @@ func validateInstanceStatus(instanceStatus *ec2.DescribeInstanceStatusOutput) er type identityDocument struct { Tags map[string]interface{} `json:"tags,omitempty" structs:"tags" mapstructure:"tags"` InstanceID string `json:"instanceId,omitempty" structs:"instanceId" mapstructure:"instanceId"` - ImageID string `json:"imageId,omitempty" structs:"imageId" mapstructure:"imageId"` + 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"` } diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 0e99810e5b..97979eae3a 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -13,7 +13,7 @@ func pathWhitelistIdentity(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "instance_id": &framework.FieldSchema{ Type: framework.TypeString, - Description: "EC2 instance ID.", + Description: "EC2 instance ID. A successful login operation from an EC2 instance gets cached in this whitelist, keyed off of instance ID.", }, }, @@ -117,7 +117,7 @@ func (b *backend) pathWhitelistIdentityRead( return &logical.Response{ Data: map[string]interface{}{ - "image_id": entry.ImageID, + "ami_id": entry.AmiID, "creation_time": entry.CreationTime.String(), "expiration_time": entry.ExpirationTime.String(), "client_nonce": entry.ClientNonce, @@ -128,7 +128,7 @@ func (b *backend) pathWhitelistIdentityRead( // Struct to represent each item in the identity whitelist. type whitelistIdentity struct { - ImageID string `json:"image_id" structs:"image_id" mapstructure:"image_id"` + AmiID string `json:"ami_id" structs:"ami_id" mapstructure:"ami_id"` DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md new file mode 100644 index 0000000000..6798c78680 --- /dev/null +++ b/website/source/docs/auth/aws.html.md @@ -0,0 +1,1006 @@ +--- +layout: "docs" +page_title: "Auth Backend: AWS EC2" +sidebar_current: "docs-auth-aws" +description: |- + The AWS EC2 backend is a mechanism for AWS EC2 instances to authenticate with Vault. +--- + +# Auth Backend: AWS EC2 + +The AWS EC2 auth backend is a mechanism for AWS EC2 instances to authenticate +with Vault in an automated fashion. This solves the problem of secure introduction +of EC2 instances to Vault server and avoids the need to create and issue Vault +tokens to each instance manually. It works by using the dynamic metadata information +that uniquely represents each EC2 instance. + +## Authentication workflow + +EC2 instances will have access to its instance metadata. Details about EC2 instance +metadata can be found [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). + +Of all the "dynamic metadata" available to the EC2 instances, the instance identity +document and its PKCS#7 signature are of particular use in this backend. For details +on retrieving the PKCS#7 signature, see [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html). + +Instance identity document contains enough information to uniquely identify an +EC2 instance. EC2 instance will have access to PKCS#7 signature of its identity +document. This signature contains the instance identity document, along with +the signer information that can establish the authenticity of the contents in +the signature. The signature can be verified using the public certificate provided +by AWS (public certificate varies by region). + +During the login, to establish authenticity of the information provided by the +client (EC2 instance), the PKCS#7 signature is validated by the backend. Before +succeeding the login attempt and returning a Vault token, AWS API DescribeInstanceStatus +is invoked to check if the instance is healthy. + +## Authorization workflow + +The AMIs that are used by instances should be associated with Vault policies at +priori, which provides access control primitives on the resources. A successful +login returns a token. The policies of this token are the same policies that are +associated with the registered AMI. If `role_tag` option (refer API section) is +enabled on the AMI, then the policies of the token will be the subset of the +policies that are associated with the AMI. + +## Client Nonce + +If an unintended party gets access to the PKCS#7 signature of a particular +instance, it can impersonate that instance and fetch a Vault token. The design +of this backend addresses this problem by sharing the responsibility with the +clients of this backend. The backend will **NOT** be able to distinguish the +genuineness of the request, during the first login. But once an instance performs +a successful login, the backend can then thwart the replay-login attempts from +unintended parties, using a unique nonce that is supplied by the client, during +its first successful login. The login from an unintended party is detected when +the instance tries to login for the first time and it fails. A security alert +should be triggered in such cases. + +The client should ensure that it generates unique nonces and makes sure that +it uses the same nonce for each login attempt. During the first login, the +backend caches the client nonce in a `whitelist`. For the subsequent login +requests to succeed, the presented client nonce should match the cached nonce. +Hence, if the nonce is lost/changed then a token cannot be refreshed (rotated). + +## 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, it can be achieved by setting `role_tag` option (refer API +section) on the registered AMI. When this option is set, during the login, +along with verification of PKCS#7 signature and instance health, the backend +will query for a specific tag that is attached to the instance. This tag will +hold information that represents a subset of capabilities that are set on the +AMI. Hence, a successful login when `role_tag` is enabled on AMI, returns a +token with the capabilities that are a subset of the capabilities configured +on the AMI. A `role_tag` can be created using `auth/aws/image//roletag` +endpoint and is immutable. The information present in the tag is SHA256 hashed +and HMAC protected. The key to HMAC is only maintained in the backend. + +### Handling lost client nonce +If an EC2 instance loses its client nonce when it migrates to a different host, +say after a stop and start action on the instance, the subsequent login attempts +will not succeed. If the client nonce is lost, 2 administrative actions can be +taken.One option is to delete the entry corresponding to the instance ID from +the identity `whitelist` in the backend. This can be done via `auth/aws/whitelist/identity/` +endpoint. This allows a new client nonce to be accepted by the backend during +the next login request. The other option is to relax the condition of matching +the client nonce through `allow_instance_migration`(refer API section). When +this option is enabled, only `pendingTime` in the instance identity document +will be checked to be newer than the `pendingTime` in the instance identity +document, that was used to login previously. This option should be used with +caution, since any entity that has access to instance PKCS#7 signature can imitate +the instance to get a new Vault token, and only the requirement of newer `pendingTime`, +will be the line of defense against such attacks. + +### Disabling reauthentication +If a client chooses to fetch a long-lived Vault token and intends to not refresh +(rotate) the token, then it can disable all future logins. If the option +`disallow_reauthentication` is set, only one login will be allowed per instance. +If the instance successfully gets the token for the first time, it can use it +without worrying about its token getting hijacked by another entity. The client +will still need to raise a security alert if the first login fails, since the +backend will not be able to distinguish a genuine login attempt from an imitation, +for the first time. + +When `disallow_reauthentication` option is enabled, the backend only allows a +single successful login from the client. In this case, the client nonce loses +its significance and hence the client can choose not to supply the nonce during +the login. + +### Blacklisting role tags +It maybe difficult to track the created role tags and to get to know which instances +are indeed using specific role tags. In these cases, when a role tag needs to be +blocked from any further login attempts, it can be placed in a `blacklist` via the +endpoint `auth/aws/blacklist/roletag/`. Note that this will not invalidate +the tokens that were already issued. This only blocks any further login requests. + +### Expiration times and tidying of `blacklist` and `whitelist` entries +The entries in both identity `whitelist` and role tag `blacklist` are not deleted +automatically. The entries in both of these lists will have an expiration time +which is dynamically determined by three factors: `max_ttl` set on the AMI, +`max_ttl` set on the role tag and `max_ttl` value of the backend mount. The +least of these three will be set as the expiration times of these entries. +Separate endpoints `aws/auth/whitelist/identity/tidy` and `aws/auth/blacklist/roletag/tidy` +are provided to cleanup the entries present in these lists. + +### Varying public certificates +AWS public key which is used to verify the PKCS#7 signature varies by region. +To check if the default public certificate is applicable for the instances +or to get a different public certificate, refer [this](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html). +If the instances that are using this backend require more than one certificate, +then this backend needs to be mounted at as many paths as there are certificates. +The clients should then use appropriate mount of the backend which can verify its +PKCS#7 signature. + +## Authentication + +### Via the CLI + +#### Enable AWS EC2 authentication in Vault. + +``` +$ vault auth-enable aws +``` + +#### Configure the credentials required to make AWS API calls. + +``` +$ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA region=us-east-1 +``` + +#### Configure the policies on the AMI. + +``` +$ vault write auth/aws/image/ami-fce3c696 policies=prod,dev max_ttl=500h +``` + +#### Perform the login operation + +``` +$ vault write auth/aws/login 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", "region":"us-east-1"}' +``` + +#### Configure the policies on the AMI. + +``` +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/image/ami-fce3c696" -d '{"policies":"prod,dev","max_ttl":"500h"}' +``` + +#### Perform the login operation + +``` +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"ault-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" + }, + "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 + DescribeInstanceStatus API. Also, if the login is performed using + the role tag, then these credentials will also be used to fetch the + tags that are set on the EC2 instance via DescribeTags API. If the + 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 instance metadata. +
  • +
+
    +
  • + secret_key + required + AWS Secret key with permissions to query EC2 instance metadata. +
  • +
+
    +
  • + region + required + Region for API calls. Defaults to the value of the AWS_REGION env var. +
  • +
+
+ +
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", + "region": "us-east-1", + "access_key": "VKIAJBRHKH6EVTTNXDHA" + }, + "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
+
+
    +
  • + aws_public_key + 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": "" +} +``` + +
+
+ + +### /auth/aws/image/ +#### POST +
+
Description
+
+ Registers an AMI ID in the backend. Only those instances which are using the AMIs registered using this endpoint, + will be able to perform login operation. If each EC2 instance is using unique AMI ID, then all those AMI IDs should + be registered beforehand. In case the same AMI is shared among many EC2 instances, then that AMI should be registered + using this endpoint with the option `role_tag` (refer API section), then a `roletag` should be created using + `auth/aws/image//roletag` endpoint, and this tag should be attached to the EC2 instance before the login operation + is performed. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/image/`
+ +
Parameters
+
+
    +
  • + ami_id + required + AMI ID to be mapped. +
  • +
+
    +
  • + role_tag + optional + If set, enables the `roletag` login for this AMI, meaning that this AMI is shared among many EC2 instances. The value set for this field should be the `key` of the tag on the EC2 instance and the `tag_value` returned from `auth/aws/image//roletag` should be the `value` of the tag on the instance. Defaults to empty string, meaning that this AMI is not shared among instances. +
  • +
+
    +
  • + max_ttl + optional + The maximum allowed lease duration. +
  • +
+
    +
  • + policies + optional + Policies to be associated with the AMI. +
  • +
+
    +
  • + 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/whitelist/identity/' endpoint. Defaults to 'false'. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the previously registered AMI ID configuration. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/image/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "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 AMI IDs that are registered with the backend. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/images?list=true`
+ +
Parameters
+
+ None. +
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "keys": [ + "ami-fce3c696", + "ami-hei3d687" + ] + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +#### DELETE +
+
Description
+
+ Deletes the previously registered AMI ID. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/image/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/image//roletag +#### POST +
+
Description
+
+ Creates a `roletag` for the AMI_ID. Role tags provide an effective way to restrict the + options that are set on the AMI ID. This is of use when AMI is shared by multiple instances + and there is need to customize the options for specific instances. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/image//roletag`
+ +
Parameters
+
+
    +
  • + ami_id + required + AMI ID to create a tag for. +
  • +
+
    +
  • + policies + optional + Policies to be associated with the tag. +
  • +
+
    +
  • + max_ttl + optional + The maximum allowed lease duration. +
  • +
+
    +
  • + disallow_reauthentication + optional + If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/whitelist/identity endpoint. Defaults to 'false'. +
  • +
+
+ +
Returns
+
+ +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "tag_value": "v1:09Vp0qGuyB8=:a=ami-fce3c696:p=default,prod:d=false:t=300h0m0s:uPLKCQxqsefRhrp1qmVa1wsQVUXXJG8UZP/pJIdVyOI=", + "tag_key": "VaultRole" + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` + +
+
+ + +### /auth/aws/login +#### POST +
+
Description
+
+ Login and fetch a token. If the instance metadata signature is valid + along with a few other conditions, a token will be issued. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/login`
+ +
Parameters
+
+
    +
  • + 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 AMI 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" + }, + "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": "" +} +``` + +
+
+ + +### /auth/aws/blacklist/roletag/ +#### POST +
+
Description
+
+ Places a valid roletag in a blacklist. This ensures that the `roletag` + cannot be used by any instance to perform a login operation again. + Note that if this `roletag` was previousy used to perfom a successful + login, placing the `roletag` in the blacklist does not invalidate the + already issued token. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/blacklist/roletag/`
+ +
Parameters
+
+
    +
  • + role_tag + required + Role tag that needs be blacklisted. The tag can be supplied as-is, or can be base64 encoded. +
  • +
+
+ +
Returns
+
`204` response code. +
+
+ + +#### GET +
+
Description
+
+ Returns the blacklist entry of a previously blacklisted `roletag`. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/blacklist/roletag/`
+ +
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 `roletags` that are blacklisted. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/blacklist/roletag?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 `roletag`. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/blacklist/roletag/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/blacklist/roletag/tidy +#### POST +
+
Description
+
+ Cleans up the entries in the blacklist based on expiration time on the entry and `safety_buffer`. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/blacklist/roletag/tidy`
+ +
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/whitelist/identity/ +#### GET +
+
Description
+
+ Returns an entry in the whitelist. An entry will be created/updated by every successful login. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/whitelist/identity/`
+ +
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
+
`204` response code. +
+
+ + +#### LIST +
+
Description
+
+ Lists all the instance IDs that are in the whitelist of successful logins. +
+ +
Method
+
GET
+ +
URL
+
`/auth/aws/whitelist/identity?list=true`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +#### DELETE +
+
Description
+
+ Deletes a cache of the successful login from an instance. +
+ +
Method
+
DELETE
+ +
URL
+
`/auth/aws/whitelist/identity/`
+ +
Parameters
+
+ None. +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/aws/whitelist/identity/tidy +#### POST +
+
Description
+
+ Cleans up the entries in the whitelist based on expiration time and `safety_buffer`. +
+ +
Method
+
POST
+ +
URL
+
`/auth/aws/whitelist/identity/tidy`
+ +
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 + From 1e50a88e6b532325d63a757a4a3bb9ede09b89f0 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 13 Apr 2016 16:24:15 +0000 Subject: [PATCH 27/79] Updating to docs --- website/source/docs/auth/aws.html.md | 313 ++++++++++++++++++--------- 1 file changed, 207 insertions(+), 106 deletions(-) diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 6798c78680..9cf8dce5da 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -3,135 +3,232 @@ layout: "docs" page_title: "Auth Backend: AWS EC2" sidebar_current: "docs-auth-aws" description: |- - The AWS EC2 backend is a mechanism for AWS EC2 instances to authenticate with Vault. + The AWS EC2 backend allows automated authentication of AWS EC2 instances. --- # Auth Backend: AWS EC2 -The AWS EC2 auth backend is a mechanism for AWS EC2 instances to authenticate -with Vault in an automated fashion. This solves the problem of secure introduction -of EC2 instances to Vault server and avoids the need to create and issue Vault -tokens to each instance manually. It works by using the dynamic metadata information -that uniquely represents each EC2 instance. +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 +## Authentication Workflow -EC2 instances will have access to its instance metadata. Details about EC2 instance -metadata can be found [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). +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).) -Of all the "dynamic metadata" available to the EC2 instances, the instance identity -document and its PKCS#7 signature are of particular use in this backend. For details -on retrieving the PKCS#7 signature, see [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.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).) -Instance identity document contains enough information to uniquely identify an -EC2 instance. EC2 instance will have access to PKCS#7 signature of its identity -document. This signature contains the instance identity document, along with -the signer information that can establish the authenticity of the contents in -the signature. The signature can be verified using the public certificate provided -by AWS (public certificate varies by region). +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. -During the login, to establish authenticity of the information provided by the -client (EC2 instance), the PKCS#7 signature is validated by the backend. Before -succeeding the login attempt and returning a Vault token, AWS API DescribeInstanceStatus -is invoked to check if the instance is healthy. +There are various modifications to this workflow that provide more or less +security, as detailed later in this documentation. -## Authorization workflow +## Authorization Workflow -The AMIs that are used by instances should be associated with Vault policies at -priori, which provides access control primitives on the resources. A successful -login returns a token. The policies of this token are the same policies that are -associated with the registered AMI. If `role_tag` option (refer API section) is -enabled on the AMI, then the policies of the token will be the subset of the -policies that are associated with the AMI. +The basic mechanism of operation is per-AMI. AMI IDs are registered in the +backend and associated with various optional restrictions, such as the set of +allowed policies and max TTLs on the generated tokens. + +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 +AMI 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 image, but cannot be used to grant additional privileges. + +The role tags can be generated at will by an operator with appropriate API +access. They are HMAC-signed by a 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 gets access to the PKCS#7 signature of a particular -instance, it can impersonate that instance and fetch a Vault token. The design -of this backend addresses this problem by sharing the responsibility with the -clients of this backend. The backend will **NOT** be able to distinguish the -genuineness of the request, during the first login. But once an instance performs -a successful login, the backend can then thwart the replay-login attempts from -unintended parties, using a unique nonce that is supplied by the client, during -its first successful login. The login from an unintended party is detected when -the instance tries to login for the first time and it fails. A security alert -should be triggered in such cases. +If an unintended party gains access to the PKCS#7 version 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 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. -The client should ensure that it generates unique nonces and makes sure that -it uses the same nonce for each login attempt. During the first login, the -backend caches the client nonce in a `whitelist`. For the subsequent login -requests to succeed, the presented client nonce should match the cached nonce. -Hence, if the nonce is lost/changed then a token cannot be refreshed (rotated). +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. However, +this has consequences for token rotation, as it means that once a token has +expired, subsequent authentication attempts would fail. -## Advanced options and caveats +The backend addresses this problem by sharing the responsibility with clients. +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 -### Dynamic management of policies via role tags If the instance is required to have customized set of policies based on the -role it plays, it can be achieved by setting `role_tag` option (refer API -section) on the registered AMI. When this option is set, during the login, -along with verification of PKCS#7 signature and instance health, the backend -will query for a specific tag that is attached to the instance. This tag will -hold information that represents a subset of capabilities that are set on the -AMI. Hence, a successful login when `role_tag` is enabled on AMI, returns a -token with the capabilities that are a subset of the capabilities configured -on the AMI. A `role_tag` can be created using `auth/aws/image//roletag` -endpoint and is immutable. The information present in the tag is SHA256 hashed -and HMAC protected. The key to HMAC is only maintained in the backend. +role it plays, the `role_tag` option can be used to provide a tag to set on +instances with the given AMI. 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 AMI and are used to further restrict the set of the AMI's +privileges for that particular instance. -### Handling lost client nonce -If an EC2 instance loses its client nonce when it migrates to a different host, -say after a stop and start action on the instance, the subsequent login attempts -will not succeed. If the client nonce is lost, 2 administrative actions can be -taken.One option is to delete the entry corresponding to the instance ID from -the identity `whitelist` in the backend. This can be done via `auth/aws/whitelist/identity/` -endpoint. This allows a new client nonce to be accepted by the backend during -the next login request. The other option is to relax the condition of matching -the client nonce through `allow_instance_migration`(refer API section). When -this option is enabled, only `pendingTime` in the instance identity document -will be checked to be newer than the `pendingTime` in the instance identity -document, that was used to login previously. This option should be used with -caution, since any entity that has access to instance PKCS#7 signature can imitate -the instance to get a new Vault token, and only the requirement of newer `pendingTime`, -will be the line of defense against such attacks. +A `role_tag` can be created using `auth/aws/image//roletag` endpoint +and is immutable. The information present in the tag is SHA256 hashed and HMAC +protected. The 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. -### Disabling reauthentication -If a client chooses to fetch a long-lived Vault token and intends to not refresh -(rotate) the token, then it can disable all future logins. If the option -`disallow_reauthentication` is set, only one login will be allowed per instance. -If the instance successfully gets the token for the first time, it can use it -without worrying about its token getting hijacked by another entity. The client -will still need to raise a security alert if the first login fails, since the -backend will not be able to distinguish a genuine login attempt from an imitation, -for the first time. +When the `role_tag` option is set on an AMI, failure to provide any role tag at +all results in a login failure. If the role tag has no policy component, the +client will inherit the allowed policies set on the AMI. If the role tag has a +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. -When `disallow_reauthentication` option is enabled, the backend only allows a -single successful login from the client. In this case, the client nonce loses -its significance and hence the client can choose not to supply the nonce during -the login. +### Handling Lost Client Nonces -### Blacklisting role tags -It maybe difficult to track the created role tags and to get to know which instances -are indeed using specific role tags. In these cases, when a role tag needs to be -blocked from any further login attempts, it can be placed in a `blacklist` via the -endpoint `auth/aws/blacklist/roletag/`. Note that this will not invalidate -the tokens that were already issued. This only blocks any further login requests. +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/whitelist/identity/` endpoint. This allows a new +client nonce to be accepted by the backend during +the next login request. -### Expiration times and tidying of `blacklist` and `whitelist` entries -The entries in both identity `whitelist` and role tag `blacklist` are not deleted -automatically. The entries in both of these lists will have an expiration time -which is dynamically determined by three factors: `max_ttl` set on the AMI, -`max_ttl` set on the role tag and `max_ttl` value of the backend mount. The -least of these three will be set as the expiration times of these entries. -Separate endpoints `aws/auth/whitelist/identity/tidy` and `aws/auth/blacklist/roletag/tidy` -are provided to cleanup the entries present in these lists. +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). -### Varying public certificates -AWS public key which is used to verify the PKCS#7 signature varies by region. -To check if the default public certificate is applicable for the instances -or to get a different public certificate, refer [this](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html). -If the instances that are using this backend require more than one certificate, -then this backend needs to be mounted at as many paths as there are certificates. -The clients should then use appropriate mount of the backend which can verify its +The backend can take advantage of this via the `allow_instance_migration` +option, which is set per-AMI. When this option is enabled, if the client nonce +does not matched 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. + +### 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). + +The `disallow_reauthentication` option is set per-AMI, 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 AMI, a value of `true` in the role tag takes +effect; however, if the option is set to `true` on the AMI, a value set in the +role tag has no effect. + +### Blacklisting Role Tags + +Role tags are tied to a specific AMI, but the backend has no control over which +instances using that AMI should have any particular role tag; that is purely up +to the operator. Although role tags are only restrictive, 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/blacklist/roletag/`. Note that this will +not invalidate the tokens that were already issued; this only blocks any +further login requests. + +### Expiration Times and Tidying of `blacklist` and `whitelist` Entries + +The entries in both identity `whitelist` and role tag `blacklist` are not +deleted automatically, as the amount of time they are required to be valid are +likely to vary with organizational policy. The entries in both of these lists +contain an expiration time which is dynamically determined by three factors: +`max_ttl` set on the AMI, `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/whitelist/identity/tidy` and +`aws/auth/blacklist/roletag/tidy` 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. + +### Varying Public Certificates + +The AWS public certificate which contains the public key used to verify the +PKCS#7 signature varies by region. The default public certificate provided with +the backend is applicable for all regions except AWS GovCloud (US); however, +users of GovCloud may need to install a different public certificate, which can +be found at +[here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html), +via the `auth/aws/config/certificate` endpoint. + +If the instances that are using this backend require more than one certificate +due to being spread across normal AWS and GovCloud, this backend needs to be +mounted at as many paths as there are certificates. The clients should then use +an appropriate mount of the backend which can verify its PKCS#7 signature. ## Authentication @@ -144,7 +241,11 @@ PKCS#7 signature. $ vault auth-enable aws ``` -#### Configure the credentials required to make AWS API calls. +#### 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. In addition, the `AWS_REGION` +environment variable will be honored if available. ``` $ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA region=us-east-1 From cd3e9e3b5bce1d502ece3e32a81f9a185d7328ac Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 13 Apr 2016 19:01:06 -0400 Subject: [PATCH 28/79] Support providing multiple certificates. Append all the certificates to the PKCS#7 parser during signature verification. --- builtin/credential/aws/backend.go | 1 + .../credential/aws/path_config_certificate.go | 161 +++++++++++++++--- builtin/credential/aws/path_image.go | 2 +- builtin/credential/aws/path_login.go | 9 +- website/source/docs/auth/aws.html.md | 21 +-- 5 files changed, 155 insertions(+), 39 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 7a97efc714..9edd08a2b1 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -43,6 +43,7 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { pathImageTag(&b), pathConfigClient(&b), pathConfigCertificate(&b), + pathListCertificates(&b), pathBlacklistRoleTag(&b), pathListBlacklistRoleTags(&b), pathBlacklistRoleTagTidy(&b), diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 633d90bebf..652478fa47 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -21,7 +21,7 @@ type dsaSignature struct { // 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) -const defaultAWSPublicCert = `-----BEGIN CERTIFICATE----- +const genericAWSPublicCertificate = `-----BEGIN CERTIFICATE----- MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z @@ -41,14 +41,51 @@ vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw -----END CERTIFICATE----- ` +const govCloudAWSPublicCertificate = `-----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$", + 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, - Default: defaultAWSPublicCert, - Description: "AWS Public key required to verify PKCS7 signature of the EC2 instance metadata.", + Description: "AWS Public cert required to verify PKCS7 signature of the EC2 instance metadata.", }, }, @@ -58,6 +95,7 @@ func pathConfigCertificate(b *backend) *framework.Path { logical.CreateOperation: b.pathConfigCertificateCreateUpdate, logical.UpdateOperation: b.pathConfigCertificateCreateUpdate, logical.ReadOperation: b.pathConfigCertificateRead, + logical.DeleteOperation: b.pathConfigCertificateDelete, }, HelpSynopsis: pathConfigCertificateSyn, @@ -68,13 +106,27 @@ func pathConfigCertificate(b *backend) *framework.Path { // 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) { - entry, err := awsPublicCertificateEntry(req.Storage) + certName := data.Get("cert_name").(string) + if certName == "" { + return false, fmt.Errorf("missing cert_name") + } + entry, err := 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) { + 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. @@ -94,23 +146,58 @@ func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) return publicCert, nil } -// awsPublicCertificateParsed will fetch the storage entry for the certificate, -// decodes it and returns the parsed certificate. -func awsPublicCertificateParsed(s logical.Storage) (*x509.Certificate, error) { - certEntry, err := awsPublicCertificateEntry(s) +// awsPublicCertificates returns a slice of all the parsed AWS public +// certificates, that were registered using `config/certificate/` endpoint. +// This method will also append two default certificates to the slice. +func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { + + // Get the list `cert_name`s of all the registered certificates. + registeredCerts, err := s.List("config/certificate/") if err != nil { return nil, err } - if certEntry == nil { - return decodePEMAndParseCertificate(defaultAWSPublicCert) + + var certs []*x509.Certificate + + // Iterate through each certificate, parse and append it to a slice. + for _, cert := range registeredCerts { + certEntry, err := awsPublicCertificateEntry(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 decodePEMAndParseCertificate(certEntry.AWSPublicCert) + + // Append the two public certs provided in the AWS documentation. + + // Append the generic certificate provided in the documentation. + decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate) + if err != nil { + return nil, err + } + certs = append(certs, decodedCert) + + // Append the govCloud certificate provided in the documentation. + decodedCert, err = decodePEMAndParseCertificate(govCloudAWSPublicCertificate) + 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 awsPublicCertificateEntry(s logical.Storage) (*awsPublicCert, error) { - entry, err := s.Get("config/certificate") +func awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) { + entry, err := s.Get("config/certificate/" + certName) if err != nil { return nil, err } @@ -126,11 +213,31 @@ func awsPublicCertificateEntry(s logical.Storage) (*awsPublicCert, error) { 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) { + certName := data.Get("cert_name").(string) + if certName == "" { + return logical.ErrorResponse("missing cert_name"), nil + } + err := req.Storage.Delete("config/certificate/" + certName) + if err != nil { + return nil, err + } + return nil, nil +} + // 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) { - certificateEntry, err := awsPublicCertificateEntry(req.Storage) + + certName := data.Get("cert_name").(string) + if certName == "" { + return logical.ErrorResponse("missing cert_name"), nil + } + + certificateEntry, err := awsPublicCertificateEntry(req.Storage, certName) if err != nil { return nil, err } @@ -150,8 +257,13 @@ func (b *backend) pathConfigCertificateRead( 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 + } + // Check if there is already a certificate entry registered. - certEntry, err := awsPublicCertificateEntry(req.Storage) + certEntry, err := awsPublicCertificateEntry(req.Storage, certName) if err != nil { return nil, err } @@ -166,15 +278,16 @@ func (b *backend) pathConfigCertificateCreateUpdate( if err != nil { return nil, err } - certEntry.AWSPublicCert = string(certBytes) - } else if req.Operation == logical.CreateOperation { - certEntry.AWSPublicCert = data.Get("aws_public_cert").(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("missing aws_public_cert"), nil + return logical.ErrorResponse("invalid aws_public_cert"), nil } // Verify the certificate by decoding it and parsing it. @@ -187,7 +300,7 @@ func (b *backend) pathConfigCertificateCreateUpdate( } // If none of the checks fail, save the provided certificate. - entry, err := logical.StorageEntryJSON("config/certificate", certEntry) + entry, err := logical.StorageEntryJSON("config/certificate/"+certName, certEntry) if err != nil { return nil, err } @@ -217,3 +330,9 @@ 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). ` +const pathListCertificatesHelpSyn = ` +Lists all the AWS public certificates that are registered with Vault. +` +const pathListCertificatesHelpDesc = ` +Certificates will be listed by their respective names that were used during registration. +` diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index 6f41a8ff41..8110902d8d 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -64,7 +64,7 @@ func pathImage(b *backend) *framework.Path { } } -// pathListImages createa a path that enables listing of all the AMIs that are +// pathListImages creates a path that enables listing of all the AMIs that are // registered with Vault. func pathListImages(b *backend) *framework.Path { return &framework.Path{ diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index b2d3171ca2..129b41fc45 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -1,7 +1,6 @@ package aws import ( - "crypto/x509" "encoding/json" "encoding/pem" "fmt" @@ -133,17 +132,17 @@ func parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocumen } // Get the public certificate that is used to verify the signature. - publicCert, err := awsPublicCertificateParsed(s) + publicCerts, err := awsPublicCertificates(s) if err != nil { return nil, err } - if publicCert == nil { - return nil, fmt.Errorf("certificate to verify the signature is not found") + 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 certificate to be used // to verify the contents in the signer information. - pkcs7Data.Certificates = []*x509.Certificate{publicCert} + 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. diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 9cf8dce5da..ddd9fc12ef 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -12,7 +12,7 @@ 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 +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. @@ -59,7 +59,7 @@ 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 version of the identity +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) @@ -143,8 +143,7 @@ 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/whitelist/identity/` endpoint. This allows a new -client nonce to be accepted by the backend during -the next login request. +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 @@ -154,7 +153,7 @@ 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-AMI. When this option is enabled, if the client nonce -does not matched the saved nonce, the `pendingTime` value in the instance +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 @@ -218,18 +217,16 @@ dictated by the safety buffer in order to actually remove the entry. ### Varying Public Certificates The AWS public certificate which contains the public key used to verify the -PKCS#7 signature varies by region. The default public certificate provided with -the backend is applicable for all regions except AWS GovCloud (US); however, -users of GovCloud may need to install a different public certificate, which can -be found at -[here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html), +PKCS#7 signature varies for groups of regions. The default public certificate +provided with the backend is applicable for all regions except AWS GovCloud (US); +however, users of GovCloud may need to install a different public certificate, which can +be found at [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html), via the `auth/aws/config/certificate` endpoint. If the instances that are using this backend require more than one certificate due to being spread across normal AWS and GovCloud, this backend needs to be mounted at as many paths as there are certificates. The clients should then use -an appropriate mount of the backend which can verify its -PKCS#7 signature. +an appropriate mount of the backend which can verify its PKCS#7 signature. ## Authentication From 01d417afbf4b4daad2e7ed2f6b50f3d489890bd2 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 13 Apr 2016 22:21:45 -0400 Subject: [PATCH 29/79] Instance ID can optionally be accepted as a the role tag parameter. --- .../credential/aws/path_config_certificate.go | 3 +- builtin/credential/aws/path_image_tag.go | 29 +++++++++++++++---- builtin/credential/aws/path_login.go | 14 +++++++-- helper/policyutil/policyutil.go | 7 ++++- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 652478fa47..f618aecf84 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -58,7 +58,8 @@ MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw 7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K ------END CERTIFICATE-----` +-----END CERTIFICATE----- +` // pathListCertificates creates a path that enables listing of all // the AWS public certificates registered with Vault. diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index f278b3787f..802493ccf9 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -26,6 +26,12 @@ func pathImageTag(b *backend) *framework.Path { Description: "AMI ID to create a tag for.", }, + "instance_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Instance ID for which this tag is intended for. +This is an optional field, but 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.", @@ -63,6 +69,9 @@ func (b *backend) pathImageTagUpdate( return logical.ErrorResponse("missing ami_id"), nil } + // Instance ID is an optional field. + instanceID := strings.ToLower(data.Get("instance_id").(string)) + // Parse the given policies into a slice and add 'default' if not provided. // Remove all other policies if 'root' is present. policies := policyutil.ParsePolicies(data.Get("policies").(string)) @@ -108,10 +117,11 @@ func (b *backend) pathImageTagUpdate( // Attach version, nonce, policies and maxTTL to the role tag value. rTagValue, err := prepareRoleTagPlainValue(&roleTag{Version: roleTagVersion, - AmiID: amiID, - Nonce: nonce, - Policies: policies, - MaxTTL: maxTTL, + AmiID: amiID, + Nonce: nonce, + Policies: policies, + MaxTTL: maxTTL, + InstanceID: instanceID, DisallowReauthentication: disallowReauthentication, }) if err != nil { @@ -188,12 +198,17 @@ func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { // attach ami_id to the value value = fmt.Sprintf("%s:a=%s", value, rTag.AmiID) - // attach policies to value + // attach policies to value. rTag.Policies will never be empty. value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) // attach disallow_reauthentication field value = fmt.Sprintf("%s:d=%s", value, strconv.FormatBool(rTag.DisallowReauthentication)) + // 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 rTag.MaxTTL > time.Duration(0) { value = fmt.Sprintf("%s:t=%s", value, rTag.MaxTTL) @@ -233,6 +248,8 @@ func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { for _, tagItem := range tagItems { var err error switch { + case strings.Contains(tagItem, "i="): + rTag.InstanceID = strings.TrimPrefix(tagItem, "i=") case strings.Contains(tagItem, "a="): rTag.AmiID = strings.TrimPrefix(tagItem, "a=") case strings.Contains(tagItem, "p="): @@ -288,6 +305,7 @@ func createRoleTagNonce() (string, error) { // 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"` @@ -303,6 +321,7 @@ func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { rTag1.MaxTTL == rTag2.MaxTTL && rTag1.AmiID == rTag2.AmiID && rTag1.HMAC == rTag2.HMAC && + rTag1.InstanceID == rTag2.InstanceID && rTag1.DisallowReauthentication == rTag2.DisallowReauthentication } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 129b41fc45..216bc4e11b 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -186,9 +186,12 @@ func (b *backend) pathLoginUpdate( } // Validate the instance ID. - if err := b.validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { - return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil - } + //TODO: uncomment this block, until the API invoking problem is resolved. + /* + if err := b.validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil + } + */ // Get the entry for the AMI used by the instance. imageEntry, err := awsImage(req.Storage, identityDoc.AmiID) @@ -367,6 +370,11 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return nil, fmt.Errorf("role tag does not belong to the instance's AMI ID.") } + // 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 := blacklistRoleTagEntry(s, rTagValue) if err != nil { diff --git a/helper/policyutil/policyutil.go b/helper/policyutil/policyutil.go index 31f2674d33..34038d10b5 100644 --- a/helper/policyutil/policyutil.go +++ b/helper/policyutil/policyutil.go @@ -6,7 +6,12 @@ import ( ) func ParsePolicies(policiesRaw string) []string { - policies := strings.Split(policiesRaw, ",") + var policies []string + + if policiesRaw != "" { + policies = strings.Split(policiesRaw, ",") + } + defaultFound := false for i, p := range policies { policies[i] = strings.TrimSpace(p) From 22c0ad94ba7c56e3fb650103d1bcbecfc6b8cd9d Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 14 Apr 2016 00:11:17 -0400 Subject: [PATCH 30/79] Removed `region` parameter from `config/client` endpoint. Region to create ec2 client objects is fetched from the identity document. Maintaining a map of cached clients indexed by region. --- builtin/credential/aws/backend.go | 4 ++- builtin/credential/aws/client.go | 30 ++++++++++-------- builtin/credential/aws/path_config_client.go | 32 +++----------------- builtin/credential/aws/path_login.go | 23 ++++++-------- 4 files changed, 35 insertions(+), 54 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 9edd08a2b1..8638015264 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -55,6 +55,8 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { AuthRenew: b.pathLoginRenew, } + b.EC2ClientsMap = make(map[string]*ec2.EC2) + return b.Backend, nil } @@ -64,7 +66,7 @@ type backend struct { configMutex sync.RWMutex - ec2Client *ec2.EC2 + EC2ClientsMap map[string]*ec2.EC2 } const backendHelp = ` diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index cb53386e0a..92c4d29f80 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -2,7 +2,6 @@ package aws import ( "fmt" - "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" @@ -21,7 +20,7 @@ import ( // * Static credentials from 'config/client' // * Environment variables // * Instance metadata role -func (b *backend) getClientConfig(s logical.Storage) (*aws.Config, error) { +func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config, error) { // Read the configured secret key and access key config, err := clientConfigEntry(s) if err != nil { @@ -29,13 +28,8 @@ func (b *backend) getClientConfig(s logical.Storage) (*aws.Config, error) { } var providers []credentials.Provider - region := os.Getenv("AWS_REGION") if config != nil { - if config.Region != "" { - region = config.Region - } - switch { case config.AccessKey != "" && config.SecretKey != "": providers = append(providers, &credentials.StaticProvider{ @@ -75,13 +69,23 @@ func (b *backend) getClientConfig(s logical.Storage) (*aws.Config, error) { }, nil } +// flushCachedEC2Clients deletes all the cached ec2 client objects from the backend. +func (b *backend) flushCachedEC2Clients() { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + 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, recreate bool) (*ec2.EC2, error) { +func (b *backend) clientEC2(s logical.Storage, region string, recreate bool) (*ec2.EC2, error) { if !recreate { b.configMutex.RLock() - if b.ec2Client != nil { + if b.EC2ClientsMap[region] != nil { defer b.configMutex.RUnlock() - return b.ec2Client, nil + return b.EC2ClientsMap[region], nil } b.configMutex.RUnlock() } @@ -89,11 +93,11 @@ func (b *backend) clientEC2(s logical.Storage, recreate bool) (*ec2.EC2, error) b.configMutex.Lock() defer b.configMutex.Unlock() - awsConfig, err := b.getClientConfig(s) + awsConfig, err := b.getClientConfig(s, region) if err != nil { return nil, err } - b.ec2Client = ec2.New(session.New(awsConfig)) - return b.ec2Client, nil + b.EC2ClientsMap[region] = ec2.New(session.New(awsConfig)) + return b.EC2ClientsMap[region], nil } diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index fe40102986..fd0c600c15 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -1,8 +1,6 @@ package aws import ( - "fmt" - "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -21,11 +19,6 @@ func pathConfigClient(b *backend) *framework.Path { Type: framework.TypeString, Description: "AWS Secret key with permissions to query EC2 instance metadata.", }, - - "region": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Region for API calls. Defaults to the value of the AWS_REGION env var. Required.", - }, }, ExistenceCheck: b.pathConfigClientExistenceCheck, @@ -104,10 +97,8 @@ func (b *backend) pathConfigClientDelete( b.configMutex.Unlock() - _, err = b.clientEC2(req.Storage, true) - if err != nil { - return nil, fmt.Errorf("error creating client with updated credentials: %s", err) - } + // Remove all the cached EC2 client objects in the backend. + b.flushCachedEC2Clients() return nil, nil } @@ -127,21 +118,14 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry = &clientConfig{} } - regionStr, ok := data.GetOk("region") - if ok { - configEntry.Region = regionStr.(string) - } else if req.Operation == logical.CreateOperation { - configEntry.Region = data.Get("region").(string) - } - changedCreds := false accessKeyStr, ok := data.GetOk("access_key") if ok { if configEntry.AccessKey != accessKeyStr.(string) { changedCreds = true + configEntry.AccessKey = accessKeyStr.(string) } - configEntry.AccessKey = accessKeyStr.(string) } else if req.Operation == logical.CreateOperation { // Use the default configEntry.AccessKey = data.Get("access_key").(string) @@ -151,8 +135,8 @@ func (b *backend) pathConfigClientCreateUpdate( if ok { if configEntry.SecretKey != secretKeyStr.(string) { changedCreds = true + configEntry.SecretKey = secretKeyStr.(string) } - configEntry.SecretKey = secretKeyStr.(string) } else if req.Operation == logical.CreateOperation { configEntry.SecretKey = data.Get("secret_key").(string) } @@ -170,13 +154,8 @@ func (b *backend) pathConfigClientCreateUpdate( // We have to be careful here to re-lock as we have a deferred unlock // queued up and unlocking an unlocked mutex leads to a panic b.configMutex.Unlock() - _, err = b.clientEC2(req.Storage, true) + b.flushCachedEC2Clients() b.configMutex.Lock() - if err != nil { - return logical.ErrorResponse(fmt.Sprintf( - "error creating client with updated credentials: %s", err), - ), nil - } } return nil, nil @@ -187,7 +166,6 @@ func (b *backend) pathConfigClientCreateUpdate( 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"` - Region string `json:"region" structs:"region" mapstructure:"region"` } const pathConfigClientHelpSyn = ` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 216bc4e11b..f30d44aeec 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -38,18 +38,18 @@ func pathLogin(b *backend) *framework.Path { } } -// validateInstanceID queries the status of the EC2 instance using AWS EC2 API and +// 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) validateInstanceID(s logical.Storage, instanceID string) error { +func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocument) error { // Create an EC2 client to pull the instance information - ec2Client, err := b.clientEC2(s, false) + ec2Client, err := b.clientEC2(s, identityDoc.Region, false) if err != nil { return err } // Get the status of the instance instanceStatus, err := ec2Client.DescribeInstanceStatus(&ec2.DescribeInstanceStatusInput{ - InstanceIds: []*string{aws.String(instanceID)}, + InstanceIds: []*string{aws.String(identityDoc.InstanceID)}, }) if err != nil { return err @@ -186,12 +186,9 @@ func (b *backend) pathLoginUpdate( } // Validate the instance ID. - //TODO: uncomment this block, until the API invoking problem is resolved. - /* - if err := b.validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { - return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil - } - */ + if err := b.validateInstance(req.Storage, identityDoc); err != nil { + return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil + } // Get the entry for the AMI used by the instance. imageEntry, err := awsImage(req.Storage, identityDoc.AmiID) @@ -311,8 +308,8 @@ func (b *backend) pathLoginUpdate( // fetchRoleTagValue creates an AWS EC2 client and queries the tags // attached to the instance identified by the given instanceID. -func (b *backend) fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) { - ec2Client, err := b.clientEC2(s, false) +func (b *backend) fetchRoleTagValue(s logical.Storage, region string, tagKey string) (string, error) { + ec2Client, err := b.clientEC2(s, region, false) if err != nil { return "", err } @@ -350,7 +347,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc // NOTE: If AWS adds the instance tags as meta-data in the instance identity // document, then it is better to look this information there instead of making // another API call. Currently, we don't have an option but make this call. - rTagValue, err := b.fetchRoleTagValue(s, imageEntry.RoleTag) + rTagValue, err := b.fetchRoleTagValue(s, identityDoc.Region, imageEntry.RoleTag) if err != nil { return nil, err } From 7c39fffe0d173b79b17be2f89efcf3f010394e19 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 14 Apr 2016 14:41:49 +0000 Subject: [PATCH 31/79] Removed redundant AWS public certificate. Docs update. --- .../credential/aws/path_config_certificate.go | 29 ----- website/source/docs/auth/aws.html.md | 123 ++++++++++++++---- 2 files changed, 99 insertions(+), 53 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index f618aecf84..2d6d6cfb36 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -41,26 +41,6 @@ vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw -----END CERTIFICATE----- ` -const govCloudAWSPublicCertificate = `-----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 { @@ -176,8 +156,6 @@ func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { certs = append(certs, decodedCert) } - // Append the two public certs provided in the AWS documentation. - // Append the generic certificate provided in the documentation. decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate) if err != nil { @@ -185,13 +163,6 @@ func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { } certs = append(certs, decodedCert) - // Append the govCloud certificate provided in the documentation. - decodedCert, err = decodePEMAndParseCertificate(govCloudAWSPublicCertificate) - if err != nil { - return nil, err - } - certs = append(certs, decodedCert) - return certs, nil } diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index ddd9fc12ef..ddeed21618 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -218,15 +218,11 @@ dictated by the safety buffer in order to actually remove the entry. 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 all regions except AWS GovCloud (US); -however, users of GovCloud may need to install a different public certificate, which can -be found at [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html), -via the `auth/aws/config/certificate` endpoint. - -If the instances that are using this backend require more than one certificate -due to being spread across normal AWS and GovCloud, this backend needs to be -mounted at as many paths as there are certificates. The clients should then use -an appropriate mount of the backend which can verify its PKCS#7 signature. +provided with the backend is applicable many regions. Users of instances whose +signatures cannott 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. ## Authentication @@ -245,7 +241,7 @@ IAM role-provided credentials if available. In addition, the `AWS_REGION` environment variable will be honored if available. ``` -$ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA region=us-east-1 +$ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA ``` #### Configure the policies on the AMI. @@ -272,7 +268,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/sys/auth/aws" -d ' #### 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", "region":"us-east-1"}' +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 AMI. @@ -358,13 +354,6 @@ The response will be in JSON. For example: AWS Secret key with permissions to query EC2 instance metadata. -
    -
  • - region - required - Region for API calls. Defaults to the value of the AWS_REGION env var. -
  • -
Returns
@@ -400,7 +389,6 @@ The response will be in JSON. For example: "warnings": null, "data": { "secret_key": "vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj", - "region": "us-east-1", "access_key": "VKIAJBRHKH6EVTTNXDHA" }, "lease_duration": 0, @@ -437,7 +425,7 @@ The response will be in JSON. For example: -### /auth/aws/config/certificate +### /auth/aws/config/certificate/ #### POST
Description
@@ -450,13 +438,20 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws/config/certificate`
+
`/auth/aws/config/certificate/`
Parameters
  • - aws_public_key + cert_name + required + Name of the certificate. +
  • +
+
    +
  • + aws_public_cert required AWS Public key required to verify PKCS7 signature of the EC2 instance metadata.
  • @@ -480,7 +475,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/config/certificate`
    +
    `/auth/aws/config/certificate/`
    Parameters
    @@ -506,6 +501,45 @@ The response will be in JSON. For example:
+#### 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/image/ #### POST @@ -535,6 +569,13 @@ The response will be in JSON. For example: AMI ID to be mapped. +
    +
  • + 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. +
  • +
  • role_tag @@ -1020,7 +1061,25 @@ The response will be in JSON. For example:
    Returns
    -
    `204` response code. +
    + +```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", + "ami_id": "ami-fce3c696" + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` +
    @@ -1045,6 +1104,22 @@ The response will be in JSON. For example:
    Returns
    `204` response code. + +```javascript +{ + "auth": null, + "warnings": null, + "data": { + "keys": [ + "i-aab47d37" + ] + }, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +} +``` +
    From b76a4df110b74f5253ae76282ad4abaff2b1b100 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 14 Apr 2016 16:42:18 +0000 Subject: [PATCH 32/79] Use fullsailor/pkcs7 package instead of its fork. Fix tests --- builtin/credential/aws/backend_test.go | 47 ++++++++------------------ builtin/credential/aws/path_login.go | 2 +- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 7135f4a545..a9e5316a37 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -23,7 +23,6 @@ func TestBackend_ConfigClient(t *testing.T) { data := map[string]interface{}{"access_key": "AKIAJBRHKV6EVTTNXDHA", "secret_key": "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", - "region": "us-east-1", } stepCreate := logicaltest.TestStep{ @@ -38,20 +37,8 @@ func TestBackend_ConfigClient(t *testing.T) { Data: data, } - data2 := map[string]interface{}{"access_key": "AKIAJBRHKV6EVTTNXDHA", - "secret_key": "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", - "region": "", - } - stepEmptyRegion := logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/client", - Data: data2, - ErrorOk: true, - } - data3 := map[string]interface{}{"access_key": "", "secret_key": "mCtSM8ZUEQ3mOFVZYPBQkf2sO6F/W7a5TVzrl3Oj", - "region": "us-east-1", } stepInvalidAccessKey := logicaltest.TestStep{ Operation: logical.UpdateOperation, @@ -62,7 +49,6 @@ func TestBackend_ConfigClient(t *testing.T) { data4 := map[string]interface{}{"access_key": "accesskey", "secret_key": "", - "region": "us-east-1", } stepInvalidSecretKey := logicaltest.TestStep{ Operation: logical.UpdateOperation, @@ -76,7 +62,6 @@ func TestBackend_ConfigClient(t *testing.T) { Backend: b, Steps: []logicaltest.TestStep{ stepCreate, - stepEmptyRegion, stepInvalidAccessKey, stepInvalidSecretKey, stepUpdate, @@ -129,8 +114,7 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal(err) } if clientConfig.AccessKey != data["access_key"] || - clientConfig.SecretKey != data["secret_key"] || - clientConfig.Region != data["region"] { + clientConfig.SecretKey != data["secret_key"] { t.Fatalf("bad: expected: %#v\ngot: %#v\n", data, clientConfig) } } @@ -147,17 +131,17 @@ func TestBackend_PathConfigCertificate(t *testing.T) { checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{ Operation: logical.CreateOperation, - Path: "config/certificate", + Path: "config/certificate/cert1", Storage: storage, }) if err != nil { t.Fatal(err) } if !checkFound { - t.Fatal("existence check not found for path 'config/certificate'") + t.Fatal("existence check not found for path 'config/certificate/cert1'") } if exists { - t.Fatal("existence check should have returned 'false' for 'config/certificate'") + t.Fatal("existence check should have returned 'false' for 'config/certificate/cert1'") } data := map[string]interface{}{ @@ -184,7 +168,7 @@ MlpCclZOR3JOOU4yZjZST2swazlLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K } _, err = b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, - Path: "config/certificate", + Path: "config/certificate/cert1", Storage: storage, Data: data, }) @@ -194,22 +178,22 @@ MlpCclZOR3JOOU4yZjZST2swazlLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K checkFound, exists, err = b.HandleExistenceCheck(&logical.Request{ Operation: logical.CreateOperation, - Path: "config/certificate", + Path: "config/certificate/cert1", Storage: storage, }) if err != nil { t.Fatal(err) } if !checkFound { - t.Fatal("existence check not found for path 'config/certificate'") + t.Fatal("existence check not found for path 'config/certificate/cert1'") } if !exists { - t.Fatal("existence check should have returned 'true' for 'config/certificate'") + t.Fatal("existence check should have returned 'true' for 'config/certificate/cert1'") } resp, err := b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, - Path: "config/certificate", + Path: "config/certificate/cert1", Storage: storage, }) expectedCert := `-----BEGIN CERTIFICATE----- @@ -354,7 +338,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { } resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123/tag", + Path: "image/abcd-123/roletag", Storage: storage, Data: data2, }) @@ -376,7 +360,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { } if rTag.Version != "v1" || !policyutil.EquivalentPolicies(rTag.Policies, []string{"p", "q", "r", "s"}) || - rTag.ImageID != "abcd-123" { + rTag.AmiID != "abcd-123" { t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag) } } @@ -419,7 +403,7 @@ func TestBackend_PathImageTag(t *testing.T) { resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123/tag", + Path: "image/abcd-123/roletag", Storage: storage, }) if err != nil { @@ -465,7 +449,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { } resp, err := b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123/tag", + Path: "image/abcd-123/roletag", Storage: storage, Data: data2, }) @@ -544,9 +528,8 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { } clientConfig := map[string]interface{}{ - "access_key": os.Getenv("AWS_AUTH_ACCESS_KEY"), - "secret_key": os.Getenv("AWS_AUTH_SECRET_KEY"), - "region": os.Getenv("AWS_AUTH_REGION"), + "access_key": os.Getenv("AWS_ACCESS_KEY"), + "secret_key": os.Getenv("AWS_SECRET_KEY"), } if clientConfig["access_key"] == "" || clientConfig["secret_key"] == "" { diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index f30d44aeec..7aca1926dd 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - "github.com/vishalnayak/pkcs7" + "github.com/fullsailor/pkcs7" ) func pathLogin(b *backend) *framework.Path { From 81e4235fc01954fade03600ddc6ee6ab29410a89 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 18 Apr 2016 21:06:26 -0400 Subject: [PATCH 33/79] Support periodic tidy callback and config endpoints. --- builtin/credential/aws/backend.go | 43 ++++++ .../credential/aws/path_blacklist_roletag.go | 3 +- .../aws/path_blacklist_roletag_tidy.go | 35 ++--- builtin/credential/aws/path_config_client.go | 2 +- .../aws/path_config_tidy_blacklist_roletag.go | 141 ++++++++++++++++++ .../path_config_tidy_whitelist_identity.go | 141 ++++++++++++++++++ .../aws/path_whitelist_identity_tidy.go | 34 +++-- builtin/logical/aws/backend.go | 4 +- builtin/logical/aws/rollback.go | 6 +- logical/framework/backend.go | 49 ++++-- vault/rollback.go | 28 ++-- 11 files changed, 424 insertions(+), 62 deletions(-) create mode 100644 builtin/credential/aws/path_config_tidy_blacklist_roletag.go create mode 100644 builtin/credential/aws/path_config_tidy_whitelist_identity.go diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 8638015264..44bd9d8b3c 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -43,6 +43,8 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { pathImageTag(&b), pathConfigClient(&b), pathConfigCertificate(&b), + pathConfigTidyBlacklistRoleTag(&b), + pathConfigTidyWhitelistIdentity(&b), pathListCertificates(&b), pathBlacklistRoleTag(&b), pathListBlacklistRoleTags(&b), @@ -53,6 +55,8 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { }), AuthRenew: b.pathLoginRenew, + + TidyFunc: b.tidyFunc, } b.EC2ClientsMap = make(map[string]*ec2.EC2) @@ -69,6 +73,45 @@ type backend struct { EC2ClientsMap map[string]*ec2.EC2 } +func (b *backend) tidyFunc(req *logical.Request) error { + b.configMutex.Lock() + defer b.configMutex.Unlock() + // safety_buffer defaults to 72h + safety_buffer := 259200 + tidyBlacklistConfigEntry, err := configTidyBlacklistRoleTag(req.Storage) + if err != nil { + return err + } + skipBlacklistTidy := false + if tidyBlacklistConfigEntry != nil { + if tidyBlacklistConfigEntry.DisablePeriodicTidy { + skipBlacklistTidy = true + } + safety_buffer = tidyBlacklistConfigEntry.SafetyBuffer + } + if !skipBlacklistTidy { + tidyBlacklistRoleTag(req.Storage, safety_buffer) + } + + // reset the safety_buffer to 72h + safety_buffer = 259200 + tidyWhitelistConfigEntry, err := configTidyWhitelistIdentity(req.Storage) + if err != nil { + return err + } + skipWhitelistTidy := false + if tidyWhitelistConfigEntry != nil { + if tidyWhitelistConfigEntry.DisablePeriodicTidy { + skipWhitelistTidy = true + } + safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer + } + if !skipWhitelistTidy { + tidyWhitelistIdentity(req.Storage, safety_buffer) + } + return nil +} + const backendHelp = ` AWS auth backend takes in a AWS EC2 instance identity document, its PKCS#7 signature and a client created nonce to authenticates the instance with Vault. diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 9100f2346b..4ebad43010 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -171,8 +171,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( currentTime := time.Now() - var epoch time.Time - if blEntry.CreationTime.Equal(epoch) { + 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. diff --git a/builtin/credential/aws/path_blacklist_roletag_tidy.go b/builtin/credential/aws/path_blacklist_roletag_tidy.go index 7de0f0808a..1a506840a8 100644 --- a/builtin/credential/aws/path_blacklist_roletag_tidy.go +++ b/builtin/credential/aws/path_blacklist_roletag_tidy.go @@ -29,46 +29,47 @@ expiration, before it is removed from the backend storage.`, } } -// pathBlacklistRoleTagTidyUpdate is used to clean-up the entries in the role tag blacklist. -func (b *backend) pathBlacklistRoleTagTidyUpdate( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - - // safety_buffer is an optional parameter. - safety_buffer := data.Get("safety_buffer").(int) +// tidyBlacklistRoleTag is used to clean-up the entries in the role tag blacklist. +func tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error { bufferDuration := time.Duration(safety_buffer) * time.Second - - tags, err := req.Storage.List("blacklist/roletag/") + tags, err := s.List("blacklist/roletag/") if err != nil { - return nil, err + return err } for _, tag := range tags { - tagEntry, err := req.Storage.Get("blacklist/roletag/" + tag) + tagEntry, err := s.Get("blacklist/roletag/" + tag) if err != nil { - return nil, fmt.Errorf("error fetching tag %s: %s", tag, err) + return fmt.Errorf("error fetching tag %s: %s", tag, err) } if tagEntry == nil { - return nil, fmt.Errorf("tag entry for tag %s is nil", tag) + return fmt.Errorf("tag entry for tag %s is nil", tag) } if tagEntry.Value == nil || len(tagEntry.Value) == 0 { - return nil, fmt.Errorf("found entry for tag %s but actual tag is empty", tag) + 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 nil, err + return err } if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { - if err := req.Storage.Delete("blacklist/roletag" + tag); err != nil { - return nil, fmt.Errorf("error deleting tag %s from storage: %s", tag, err) + if err := s.Delete("blacklist/roletag" + tag); err != nil { + return fmt.Errorf("error deleting tag %s from storage: %s", tag, err) } } } - return nil, nil + return nil +} + +// pathBlacklistRoleTagTidyUpdate is used to clean-up the entries in the role tag blacklist. +func (b *backend) pathBlacklistRoleTagTidyUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return nil, tidyBlacklistRoleTag(req.Storage, data.Get("safety_buffer").(int)) } const pathBlacklistRoleTagTidySyn = ` diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index fd0c600c15..331dd3d6d8 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -8,7 +8,7 @@ import ( func pathConfigClient(b *backend) *framework.Path { return &framework.Path{ - Pattern: "config/client", + Pattern: "config/client$", Fields: map[string]*framework.FieldSchema{ "access_key": &framework.FieldSchema{ Type: framework.TypeString, diff --git a/builtin/credential/aws/path_config_tidy_blacklist_roletag.go b/builtin/credential/aws/path_config_tidy_blacklist_roletag.go new file mode 100644 index 0000000000..4d319927e1 --- /dev/null +++ b/builtin/credential/aws/path_config_tidy_blacklist_roletag.go @@ -0,0 +1,141 @@ +package aws + +import ( + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfigTidyBlacklistRoleTag(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/tidy/blacklist/roletag$", + 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.`, + }, + "disable_periodic_tidy": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: "If set to 'true', disables the periodic tidying of the 'blacklist/roletag/' entries and 'whitelist/identity' entries.", + }, + }, + + ExistenceCheck: b.pathConfigTidyBlacklistRoleTagExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigTidyBlacklistRoleTagCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyBlacklistRoleTagCreateUpdate, + }, + + HelpSynopsis: pathConfigTidyBlacklistRoleTagHelpSyn, + HelpDescription: pathConfigTidyBlacklistRoleTagHelpDesc, + } +} + +func (b *backend) pathConfigTidyBlacklistRoleTagExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + entry, err := configTidyBlacklistRoleTag(req.Storage) + if err != nil { + return false, err + } + return entry != nil, nil +} + +func configTidyBlacklistRoleTag(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { + entry, err := s.Get("config/tidy/blacklist/roletag") + 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) pathConfigTidyBlacklistRoleTagCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + configEntry, err := configTidyBlacklistRoleTag(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("config/tidy/blacklist/roletag", configEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathConfigTidyBlacklistRoleTagRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + clientConfig, err := configTidyBlacklistRoleTag(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) pathConfigTidyBlacklistRoleTagDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + if err := req.Storage.Delete("config/tidy/blacklist/roletag"); err != nil { + return nil, err + } + + return nil, nil +} + +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 pathConfigTidyBlacklistRoleTagHelpSyn = ` +Configures the periodic tidying operation of the blacklisted role tag entries. +` +const pathConfigTidyBlacklistRoleTagHelpDesc = ` +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 purge them. +However, there is a safety buffer duration (defaults to 72h), which purges the entries, +only if they have been persisting this duration, past its expiration time. +` diff --git a/builtin/credential/aws/path_config_tidy_whitelist_identity.go b/builtin/credential/aws/path_config_tidy_whitelist_identity.go new file mode 100644 index 0000000000..1baf28885a --- /dev/null +++ b/builtin/credential/aws/path_config_tidy_whitelist_identity.go @@ -0,0 +1,141 @@ +package aws + +import ( + "github.com/fatih/structs" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfigTidyWhitelistIdentity(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/tidy/whitelist/identity$", + 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 'whitelist/identity/' entries and 'whitelist/identity' entries.", + }, + }, + + ExistenceCheck: b.pathConfigTidyWhitelistIdentityExistenceCheck, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.CreateOperation: b.pathConfigTidyWhitelistIdentityCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyWhitelistIdentityCreateUpdate, + }, + + HelpSynopsis: pathConfigTidyWhitelistIdentityHelpSyn, + HelpDescription: pathConfigTidyWhitelistIdentityHelpDesc, + } +} + +func (b *backend) pathConfigTidyWhitelistIdentityExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + entry, err := configTidyWhitelistIdentity(req.Storage) + if err != nil { + return false, err + } + return entry != nil, nil +} + +func configTidyWhitelistIdentity(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { + entry, err := s.Get("config/tidy/whitelist/identity") + 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) pathConfigTidyWhitelistIdentityCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + configEntry, err := configTidyWhitelistIdentity(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("config/tidy/whitelist/identity", configEntry) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathConfigTidyWhitelistIdentityRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + + clientConfig, err := configTidyWhitelistIdentity(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) pathConfigTidyWhitelistIdentityDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + + if err := req.Storage.Delete("config/tidy/whitelist/identity"); err != nil { + return nil, err + } + + return nil, nil +} + +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 pathConfigTidyWhitelistIdentityHelpSyn = ` +Configures the periodic tidying operation of the whitelisted identity entries. +` +const pathConfigTidyWhitelistIdentityHelpDesc = ` +By default, the expired entries in teb whitelist will be attempted to be removed +periodically. This operation will look for expired items in the list and purge them. +However, there is a safety buffer duration (defaults to 72h), which purges the entries, +only if they have been persisting this duration, past its expiration time. +` diff --git a/builtin/credential/aws/path_whitelist_identity_tidy.go b/builtin/credential/aws/path_whitelist_identity_tidy.go index bbefba46da..b80c495966 100644 --- a/builtin/credential/aws/path_whitelist_identity_tidy.go +++ b/builtin/credential/aws/path_whitelist_identity_tidy.go @@ -29,46 +29,48 @@ expiration, before it is removed from the backend storage.`, } } -// pathWhitelistIdentityTidyUpdate is used to delete entries in the whitelist that are expired. -func (b *backend) pathWhitelistIdentityTidyUpdate( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - - safety_buffer := data.Get("safety_buffer").(int) - +// tidyWhitelistIdentity is used to delete entries in the whitelist that are expired. +func tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error { bufferDuration := time.Duration(safety_buffer) * time.Second - identities, err := req.Storage.List("whitelist/identity/") + identities, err := s.List("whitelist/identity/") if err != nil { - return nil, err + return err } for _, instanceID := range identities { - identityEntry, err := req.Storage.Get("whitelist/identity/" + instanceID) + identityEntry, err := s.Get("whitelist/identity/" + instanceID) if err != nil { - return nil, fmt.Errorf("error fetching identity of instanceID %s: %s", instanceID, err) + return fmt.Errorf("error fetching identity of instanceID %s: %s", instanceID, err) } if identityEntry == nil { - return nil, fmt.Errorf("identity entry for instanceID %s is nil", instanceID) + return fmt.Errorf("identity entry for instanceID %s is nil", instanceID) } if identityEntry.Value == nil || len(identityEntry.Value) == 0 { - return nil, fmt.Errorf("found identity entry for instanceID %s but actual identity is empty", instanceID) + 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 nil, err + return err } if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { - if err := req.Storage.Delete("whitelist/identity" + instanceID); err != nil { - return nil, fmt.Errorf("error deleting identity of instanceID %s from storage: %s", instanceID, err) + 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, nil + return nil +} + +// pathWhitelistIdentityTidyUpdate is used to delete entries in the whitelist that are expired. +func (b *backend) pathWhitelistIdentityTidyUpdate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + return nil, tidyWhitelistIdentity(req.Storage, data.Get("safety_buffer").(int)) } const pathWhitelistIdentityTidySyn = ` 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/logical/framework/backend.go b/logical/framework/backend.go index e6749935aa..0df8624b9d 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 + // TidyFunc is the callback, which if set, will be invoked when the + // periodic timer of RollbackManager ticks. This can be used by + // backends to do any tidying tasks. + // + // TidyFunc is different from 'Clean' in the sense that, TidyFunc is + // invoked to, say to periodically delete expired/stale entries in backend's + // storage, while the backend is still being used. Whereas `Clean` is + // invoked just before the backend is unmounted. + TidyFunc tidyFunc + + // 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 } +// tidyFunc is the callback called when the RollbackManager's timer ticks. +// This can be utilized by the backends to do tidying tasks. +type tidyFunc 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() @@ -385,6 +399,19 @@ func (b *Backend) handleRevokeRenew( } } +// handleRollback invokes the TidyFunc 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 tidy operation. + if b.TidyFunc != nil { + if err := b.TidyFunc(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 @@ -393,9 +420,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 } @@ -410,7 +437,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 } @@ -434,8 +461,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/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 } From 4f46bbaa32865b783e2fcacf09552d899e1ed3b3 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 19 Apr 2016 14:21:27 -0400 Subject: [PATCH 34/79] Added cooldown period for periodic tidying operation --- builtin/credential/aws/backend.go | 124 +++++----- .../aws/path_config_tidy_blacklist_roletag.go | 4 +- .../path_config_tidy_whitelist_identity.go | 2 +- logical/framework/backend.go | 28 +-- website/source/docs/auth/aws.html.md | 212 +++++++++++++++++- 5 files changed, 296 insertions(+), 74 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 44bd9d8b3c..52312949a8 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -2,6 +2,7 @@ package aws import ( "sync" + "time" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/vault/helper/salt" @@ -25,42 +26,40 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { return nil, err } - var b backend - b.Salt = salt - b.Backend = &framework.Backend{ - Help: backendHelp, + b := &backend{ + 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: append([]*framework.Path{ - pathLogin(&b), - pathImage(&b), - pathListImages(&b), - pathImageTag(&b), - pathConfigClient(&b), - pathConfigCertificate(&b), - pathConfigTidyBlacklistRoleTag(&b), - pathConfigTidyWhitelistIdentity(&b), - pathListCertificates(&b), - pathBlacklistRoleTag(&b), - pathListBlacklistRoleTags(&b), - pathBlacklistRoleTagTidy(&b), - pathWhitelistIdentity(&b), - pathWhitelistIdentityTidy(&b), - pathListWhitelistIdentities(&b), + pathLogin(b), + pathImage(b), + pathListImages(b), + pathImageTag(b), + pathConfigClient(b), + pathConfigCertificate(b), + pathConfigTidyBlacklistRoleTag(b), + pathConfigTidyWhitelistIdentity(b), + pathListCertificates(b), + pathBlacklistRoleTag(b), + pathListBlacklistRoleTags(b), + pathBlacklistRoleTagTidy(b), + pathWhitelistIdentity(b), + pathWhitelistIdentityTidy(b), + pathListWhitelistIdentities(b), }), - - AuthRenew: b.pathLoginRenew, - - TidyFunc: b.tidyFunc, } - b.EC2ClientsMap = make(map[string]*ec2.EC2) - return b.Backend, nil } @@ -68,46 +67,57 @@ type backend struct { *framework.Backend Salt *salt.Salt - configMutex sync.RWMutex + configMutex sync.RWMutex + tidyCooldownPeriod time.Duration + nextTidyTime time.Time EC2ClientsMap map[string]*ec2.EC2 } -func (b *backend) tidyFunc(req *logical.Request) error { +// 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 are to cleanup the expired entries of both blacklist +// and whitelist. +func (b *backend) periodicFunc(req *logical.Request) error { b.configMutex.Lock() defer b.configMutex.Unlock() - // safety_buffer defaults to 72h - safety_buffer := 259200 - tidyBlacklistConfigEntry, err := configTidyBlacklistRoleTag(req.Storage) - if err != nil { - return err - } - skipBlacklistTidy := false - if tidyBlacklistConfigEntry != nil { - if tidyBlacklistConfigEntry.DisablePeriodicTidy { - skipBlacklistTidy = true + if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { + // safety_buffer defaults to 72h + safety_buffer := 259200 + tidyBlacklistConfigEntry, err := configTidyBlacklistRoleTag(req.Storage) + if err != nil { + return err + } + skipBlacklistTidy := false + if tidyBlacklistConfigEntry != nil { + if tidyBlacklistConfigEntry.DisablePeriodicTidy { + skipBlacklistTidy = true + } + safety_buffer = tidyBlacklistConfigEntry.SafetyBuffer + } + if !skipBlacklistTidy { + tidyBlacklistRoleTag(req.Storage, safety_buffer) } - safety_buffer = tidyBlacklistConfigEntry.SafetyBuffer - } - if !skipBlacklistTidy { - tidyBlacklistRoleTag(req.Storage, safety_buffer) - } - // reset the safety_buffer to 72h - safety_buffer = 259200 - tidyWhitelistConfigEntry, err := configTidyWhitelistIdentity(req.Storage) - if err != nil { - return err - } - skipWhitelistTidy := false - if tidyWhitelistConfigEntry != nil { - if tidyWhitelistConfigEntry.DisablePeriodicTidy { - skipWhitelistTidy = true + // reset the safety_buffer to 72h + safety_buffer = 259200 + tidyWhitelistConfigEntry, err := configTidyWhitelistIdentity(req.Storage) + if err != nil { + return err } - safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer - } - if !skipWhitelistTidy { - tidyWhitelistIdentity(req.Storage, safety_buffer) + skipWhitelistTidy := false + if tidyWhitelistConfigEntry != nil { + if tidyWhitelistConfigEntry.DisablePeriodicTidy { + skipWhitelistTidy = true + } + safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer + } + if !skipWhitelistTidy { + tidyWhitelistIdentity(req.Storage, safety_buffer) + } + + // Update the lastTidyTime + b.nextTidyTime = time.Now().Add(b.tidyCooldownPeriod) } return nil } diff --git a/builtin/credential/aws/path_config_tidy_blacklist_roletag.go b/builtin/credential/aws/path_config_tidy_blacklist_roletag.go index 4d319927e1..c3c493e6b5 100644 --- a/builtin/credential/aws/path_config_tidy_blacklist_roletag.go +++ b/builtin/credential/aws/path_config_tidy_blacklist_roletag.go @@ -19,7 +19,7 @@ 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 'blacklist/roletag/' entries and 'whitelist/identity' entries.", + Description: "If set to 'true', disables the periodic tidying of the 'blacklist/roletag/' entries and 'whitelist/identity/' entries.", }, }, @@ -28,6 +28,8 @@ expiration, before it is removed from the backend storage.`, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.CreateOperation: b.pathConfigTidyBlacklistRoleTagCreateUpdate, logical.UpdateOperation: b.pathConfigTidyBlacklistRoleTagCreateUpdate, + logical.ReadOperation: b.pathConfigTidyBlacklistRoleTagRead, + logical.DeleteOperation: b.pathConfigTidyBlacklistRoleTagDelete, }, HelpSynopsis: pathConfigTidyBlacklistRoleTagHelpSyn, diff --git a/builtin/credential/aws/path_config_tidy_whitelist_identity.go b/builtin/credential/aws/path_config_tidy_whitelist_identity.go index 1baf28885a..daf746e941 100644 --- a/builtin/credential/aws/path_config_tidy_whitelist_identity.go +++ b/builtin/credential/aws/path_config_tidy_whitelist_identity.go @@ -19,7 +19,7 @@ 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 'whitelist/identity/' entries and 'whitelist/identity' entries.", + Description: "If set to 'true', disables the periodic tidying of the 'whitelist/identity/' entries and 'whitelist/identity/' entries.", }, }, diff --git a/logical/framework/backend.go b/logical/framework/backend.go index 0df8624b9d..01804d32b7 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -42,15 +42,15 @@ type Backend struct { // and ease specifying callbacks for revocation, renewal, etc. Secrets []*Secret - // TidyFunc is the callback, which if set, will be invoked when the + // 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 any tidying tasks. + // backends to do anything it wishes to do periodically. // - // TidyFunc is different from 'Clean' in the sense that, TidyFunc is - // invoked to, say to periodically delete expired/stale entries in backend's - // storage, while the backend is still being used. Whereas `Clean` is - // invoked just before the backend is unmounted. - TidyFunc tidyFunc + // 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. @@ -76,9 +76,9 @@ type Backend struct { pathsRe []*regexp.Regexp } -// tidyFunc is the callback called when the RollbackManager's timer ticks. -// This can be utilized by the backends to do tidying tasks. -type tidyFunc func(*logical.Request) error +// 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) @@ -399,12 +399,12 @@ func (b *Backend) handleRevokeRenew( } } -// handleRollback invokes the TidyFunc set on the backend. It also does a WAL rollback operation. +// 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 tidy operation. - if b.TidyFunc != nil { - if err := b.TidyFunc(req); err != nil { + // Response is not expected from the periodic operation. + if b.PeriodicFunc != nil { + if err := b.PeriodicFunc(req); err != nil { return nil, err } } diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index ddeed21618..31aa01d1fa 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -214,6 +214,12 @@ 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. +Additionally, the backend performs has a periodic function that does the tidying +of both blacklist role tags and whitelist identities. This periodic tidying is +activated by default and will have a safety buffer of 72 hours. This can be +configured via `config/tidy/blacklist/roletag` and `config/tidy/whitelist/identity` +endpoints. + ### Varying Public Certificates The AWS public certificate which contains the public key used to verify the @@ -280,7 +286,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/image/ami #### Perform the login operation ``` -curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"ault-client-nonce"}' +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"vault-client-nonce"}' ``` @@ -540,6 +546,210 @@ The response will be in JSON. For example: +### /auth/aws/config/tidy/whitelist/identity +##### POST +
    +
    Description
    +
    + Configures the periodic tidying operation of the whitelisted identity entries. +
    + +
    Method
    +
    POST
    + +
    URL
    +
    `/auth/aws/config/tidy/whitelist/identity`
    + +
    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 'whitelist/identity/' + entries and 'whitelist/identity/' entries. +
    • +
    +
    + +
    Returns
    +
    `204` response code. +
    +
    + + +#### GET +
    +
    Description
    +
    + Returns the previously configured periodic whitelist tidying settings. +
    + +
    Method
    +
    GET
    + +
    URL
    +
    `/auth/aws/config/tidy/whitelist/identity`
    + +
    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/whitelist/identity`
    + +
    Parameters
    +
    + None. +
    + +
    Returns
    +
    `204` response code. +
    +
    + + + +### /auth/aws/config/tidy/blacklist/roletag +##### POST +
    +
    Description
    +
    + Configures the periodic tidying operation of the blacklisted role tag entries. +
    + +
    Method
    +
    POST
    + +
    URL
    +
    `/auth/aws/config/tidy/blacklist/roletag`
    + +
    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 'blacklist/roletag/' entries and 'whitelist/identity/' entries. +
    • +
    +
    + +
    Returns
    +
    `204` response code. +
    +
    + + +#### GET +
    +
    Description
    +
    + Returns the previously configured periodic blacklist tidying settings. +
    + +
    Method
    +
    GET
    + +
    URL
    +
    `/auth/aws/config/tidy/blacklist/roletag`
    + +
    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/blacklist/roletag`
    + +
    Parameters
    +
    + None. +
    + +
    Returns
    +
    `204` response code. +
    +
    + + ### /auth/aws/image/ #### POST From 23a2bc76a53b8f9d8aa467a0146b6cbe955d6c6d Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 19 Apr 2016 15:32:15 -0400 Subject: [PATCH 35/79] Added mutex locking for config/certificate endpoint --- builtin/credential/aws/path_config_certificate.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 2d6d6cfb36..7b441bbe1f 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -101,6 +101,8 @@ func (b *backend) pathConfigCertificateExistenceCheck(req *logical.Request, data // 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 @@ -169,6 +171,8 @@ func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { // 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 awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() entry, err := s.Get("config/certificate/" + certName) if err != nil { return nil, err @@ -271,6 +275,8 @@ func (b *backend) pathConfigCertificateCreateUpdate( return logical.ErrorResponse("invalid certificate; failed to decode and parse certificate"), nil } + b.configMutex.Lock() + defer b.configMutex.Unlock() // If none of the checks fail, save the provided certificate. entry, err := logical.StorageEntryJSON("config/certificate/"+certName, certEntry) if err != nil { From 3110f65834baee2c991312e0cfd6deb6c7c0895a Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 19 Apr 2016 17:07:06 -0400 Subject: [PATCH 36/79] Rework and refactoring --- .../credential/aws/path_config_certificate.go | 12 +-- builtin/credential/aws/path_image_tag.go | 89 +++++++++++-------- builtin/credential/aws/path_login.go | 8 +- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 7b441bbe1f..05e7b1d1d2 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -91,7 +91,7 @@ func (b *backend) pathConfigCertificateExistenceCheck(req *logical.Request, data if certName == "" { return false, fmt.Errorf("missing cert_name") } - entry, err := awsPublicCertificateEntry(req.Storage, certName) + entry, err := b.awsPublicCertificateEntry(req.Storage, certName) if err != nil { return false, err } @@ -132,7 +132,7 @@ func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) // awsPublicCertificates returns a slice of all the parsed AWS public // certificates, that were registered using `config/certificate/` endpoint. // This method will also append two default certificates to the slice. -func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { +func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { // Get the list `cert_name`s of all the registered certificates. registeredCerts, err := s.List("config/certificate/") @@ -144,7 +144,7 @@ func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { // Iterate through each certificate, parse and append it to a slice. for _, cert := range registeredCerts { - certEntry, err := awsPublicCertificateEntry(s, cert) + certEntry, err := b.awsPublicCertificateEntry(s, cert) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { // 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 awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) { +func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() entry, err := s.Get("config/certificate/" + certName) @@ -213,7 +213,7 @@ func (b *backend) pathConfigCertificateRead( return logical.ErrorResponse("missing cert_name"), nil } - certificateEntry, err := awsPublicCertificateEntry(req.Storage, certName) + certificateEntry, err := b.awsPublicCertificateEntry(req.Storage, certName) if err != nil { return nil, err } @@ -239,7 +239,7 @@ func (b *backend) pathConfigCertificateCreateUpdate( } // Check if there is already a certificate entry registered. - certEntry, err := awsPublicCertificateEntry(req.Storage, certName) + certEntry, err := b.awsPublicCertificateEntry(req.Storage, certName) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index 802493ccf9..1bd28e1de2 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -3,6 +3,7 @@ package aws import ( "crypto/hmac" "crypto/sha256" + "crypto/subtle" "encoding/base64" "fmt" "strconv" @@ -115,8 +116,9 @@ func (b *backend) pathImageTagUpdate( return logical.ErrorResponse("max_ttl cannot be negative"), nil } - // Attach version, nonce, policies and maxTTL to the role tag value. - rTagValue, err := prepareRoleTagPlainValue(&roleTag{Version: roleTagVersion, + // Create a role tag out of all the information provided. + rTagValue, err := createRoleTagValue(req.Storage, &roleTag{ + Version: roleTagVersion, AmiID: amiID, Nonce: nonce, Policies: policies, @@ -128,24 +130,8 @@ func (b *backend) pathImageTagUpdate( return nil, err } - // Get the key used for creating the HMAC - key, err := hmacKey(req.Storage) - if err != nil { - return nil, err - } - - // Create the HMAC of the value - hmacB64, err := createRoleTagHMACBase64(key, rTagValue) - if err != nil { - return nil, err - } - - // attach the HMAC to the value - rTagValue = fmt.Sprintf("%s:%s", rTagValue, hmacB64) - if len(rTagValue) > 255 { - return nil, fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") - } - + // 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. return &logical.Response{ Data: map[string]interface{}{ "tag_key": imageEntry.RoleTag, @@ -154,12 +140,48 @@ func (b *backend) pathImageTagUpdate( }, nil } +// createRoleTagValue prepares the plaintext version of the role tag, +// and appends a HMAC of the plaintext value to it, before returning. +func createRoleTagValue(s logical.Storage, rTag *roleTag) (string, error) { + // Attach version, nonce, policies and maxTTL to the role tag value. + rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) + if err != nil { + return "", err + } + + return appendHMAC(s, rTagPlainText) +} + +// 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(s logical.Storage, rTagPlainText string) (string, error) { + // Get the key used for creating the HMAC + key, err := hmacKey(s) + if err != nil { + return "", err + } + + // Create the HMAC of the value + hmacB64, err := createRoleTagHMACBase64(key, rTagPlainText) + if err != nil { + return "", err + } + + // attach the HMAC to the value + rTagValue := fmt.Sprintf("%s:%s", rTagPlainText, hmacB64) + if len(rTagValue) > 255 { + return "", fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") + } + + return rTagValue, nil +} + // verifyRoleTagValue rebuilds the role tag value without the HMAC, // computes the HMAC from it using the backend specific key and // compares it with the received HMAC. func verifyRoleTagValue(s logical.Storage, rTag *roleTag) (bool, error) { // Fetch the plaintext part of role tag - rTagPlainText, err := prepareRoleTagPlainValue(rTag) + rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) if err != nil { return false, err } @@ -175,41 +197,30 @@ func verifyRoleTagValue(s logical.Storage, rTag *roleTag) (bool, error) { if err != nil { return false, err } - return rTag.HMAC == hmacB64, nil + return subtle.ConstantTimeCompare([]byte(rTag.HMAC), []byte(hmacB64)) == 1, nil } -// prepareRoleTagPlainValue builds the role tag value without the HMAC in it. -func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { +// prepareRoleTagPlaintextValue builds the role tag value without the HMAC in it. +func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { if rTag.Version == "" { return "", fmt.Errorf("missing version") } - // attach version to the value - value := rTag.Version - if rTag.Nonce == "" { return "", fmt.Errorf("missing nonce") } - // attach nonce to the value - value = fmt.Sprintf("%s:%s", value, rTag.Nonce) - if rTag.AmiID == "" { return "", fmt.Errorf("missing ami_id") } - // attach ami_id to the value - value = fmt.Sprintf("%s:a=%s", value, rTag.AmiID) - // attach policies to value. rTag.Policies will never be empty. - value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) + // Attach Version, Nonce, AMI ID, Policies, DisallowReauthentication fields. + value := fmt.Sprintf("%s:%s:a=%s:p=%s:d=%s", rTag.Version, rTag.Nonce, rTag.AmiID, strings.Join(rTag.Policies, ","), strconv.FormatBool(rTag.DisallowReauthentication)) - // attach disallow_reauthentication field - value = fmt.Sprintf("%s:d=%s", value, strconv.FormatBool(rTag.DisallowReauthentication)) - - // attach instance_id if set + // Attach instance_id if set. if rTag.InstanceID != "" { value = fmt.Sprintf("%s:i=%s", value, rTag.InstanceID) } - // attach max_ttl if it is provided + // Attach max_ttl if it is provided. if rTag.MaxTTL > time.Duration(0) { value = fmt.Sprintf("%s:t=%s", value, rTag.MaxTTL) } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 7aca1926dd..6122b7c113 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -8,10 +8,10 @@ import ( "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" - "github.com/fullsailor/pkcs7" ) func pathLogin(b *backend) *framework.Path { @@ -116,7 +116,7 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist // 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 parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) { +func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) { pkcs7B64 = fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", pkcs7B64) // Decode the PEM encoded signature. @@ -132,7 +132,7 @@ func parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocumen } // Get the public certificate that is used to verify the signature. - publicCerts, err := awsPublicCertificates(s) + publicCerts, err := b.awsPublicCertificates(s) if err != nil { return nil, err } @@ -177,7 +177,7 @@ func (b *backend) pathLoginUpdate( } // Verify the signature of the identity document. - identityDoc, err := parseIdentityDocument(req.Storage, pkcs7B64) + identityDoc, err := b.parseIdentityDocument(req.Storage, pkcs7B64) if err != nil { return nil, err } From dd03c55d68e7d1f4c80bd3c4a16ae76d1caf662c Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 20 Apr 2016 06:14:38 -0400 Subject: [PATCH 37/79] HMAC Key per AMI ID and avoided secondary call to AWS to fetch the tags --- builtin/credential/aws/path_image.go | 27 +++- builtin/credential/aws/path_image_tag.go | 49 +++---- builtin/credential/aws/path_login.go | 136 +++++++------------- builtin/credential/aws/role_tag_hmac_key.go | 44 ------- 4 files changed, 91 insertions(+), 165 deletions(-) delete mode 100644 builtin/credential/aws/role_tag_hmac_key.go diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index 8110902d8d..fdfa054eab 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -5,6 +5,8 @@ import ( "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" @@ -137,14 +139,17 @@ func (b *backend) pathImageRead( return nil, nil } + // Prepare the map of all the entries in the imageEntry. + respData := structs.New(imageEntry).Map() + + // HMAC key belonging to the AMI should NOT be exported. + delete(respData, "hmac_key") + + // Display the max_ttl in seconds. + respData["max_ttl"] = imageEntry.MaxTTL / time.Second + return &logical.Response{ - Data: map[string]interface{}{ - "role_tag": imageEntry.RoleTag, - "policies": imageEntry.Policies, - "max_ttl": imageEntry.MaxTTL / time.Second, - "allow_instance_migration": imageEntry.AllowInstanceMigration, - "disallow_reauthentication": imageEntry.DisallowReauthentication, - }, + Data: respData, }, nil } @@ -213,13 +218,20 @@ func (b *backend) pathImageCreateUpdate( imageEntry.RoleTag = data.Get("role_tag").(string) } + imageEntry.HMACKey, err = uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("failed to generate uuid HMAC key: %v", err) + } + entry, err := logical.StorageEntryJSON("image/"+imageID, imageEntry) if err != nil { return nil, err } + if err := req.Storage.Put(entry); err != nil { return nil, err } + return nil, nil } @@ -230,6 +242,7 @@ type awsImageEntry struct { 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 pathImageSyn = ` diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index 1bd28e1de2..b139a2c533 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -85,7 +85,7 @@ func (b *backend) pathImageTagUpdate( return nil, err } if imageEntry == nil { - return logical.ErrorResponse("image entry not found"), nil + return logical.ErrorResponse(fmt.Sprintf("entry not found for AMI %s", amiID)), nil } // If RoleTag is empty, disallow creation of tag. @@ -93,6 +93,12 @@ func (b *backend) pathImageTagUpdate( return logical.ErrorResponse("tag creation is not enabled for this image"), nil } + // There should be a HMAC key present in the image entry + if imageEntry.HMACKey == "" { + // Not able to find the HMACKey is an internal error + return nil, fmt.Errorf("failed to find the HMAC key") + } + // Create a random nonce nonce, err := createRoleTagNonce() if err != nil { @@ -117,7 +123,7 @@ func (b *backend) pathImageTagUpdate( } // Create a role tag out of all the information provided. - rTagValue, err := createRoleTagValue(req.Storage, &roleTag{ + rTagValue, err := createRoleTagValue(&roleTag{ Version: roleTagVersion, AmiID: amiID, Nonce: nonce, @@ -125,7 +131,7 @@ func (b *backend) pathImageTagUpdate( MaxTTL: maxTTL, InstanceID: instanceID, DisallowReauthentication: disallowReauthentication, - }) + }, imageEntry) if err != nil { return nil, err } @@ -142,27 +148,21 @@ func (b *backend) pathImageTagUpdate( // createRoleTagValue prepares the plaintext version of the role tag, // and appends a HMAC of the plaintext value to it, before returning. -func createRoleTagValue(s logical.Storage, rTag *roleTag) (string, error) { +func createRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (string, error) { // Attach version, nonce, policies and maxTTL to the role tag value. rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) if err != nil { return "", err } - return appendHMAC(s, rTagPlainText) + return appendHMAC(rTagPlainText, imageEntry) } // 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(s logical.Storage, rTagPlainText string) (string, error) { - // Get the key used for creating the HMAC - key, err := hmacKey(s) - if err != nil { - return "", err - } - +func appendHMAC(rTagPlainText string, imageEntry *awsImageEntry) (string, error) { // Create the HMAC of the value - hmacB64, err := createRoleTagHMACBase64(key, rTagPlainText) + hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlainText) if err != nil { return "", err } @@ -179,21 +179,15 @@ func appendHMAC(s logical.Storage, rTagPlainText string) (string, error) { // verifyRoleTagValue rebuilds the role tag value without the HMAC, // computes the HMAC from it using the backend specific key and // compares it with the received HMAC. -func verifyRoleTagValue(s logical.Storage, rTag *roleTag) (bool, error) { +func verifyRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (bool, error) { // Fetch the plaintext part of role tag rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) if err != nil { return false, err } - // Get the key used for creating the HMAC - key, err := hmacKey(s) - if err != nil { - return false, err - } - // Compute the HMAC of the plaintext - hmacB64, err := createRoleTagHMACBase64(key, rTagPlainText) + hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlainText) if err != nil { return false, err } @@ -284,8 +278,16 @@ func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { return nil, fmt.Errorf("missing image ID") } + imageEntry, err := awsImage(s, rTag.AmiID) + if err != nil { + return nil, err + } + if imageEntry == nil { + return nil, fmt.Errorf("entry not found for AMI %s", rTag.AmiID) + } + // Create a HMAC of the plaintext value of role tag and compare it with the given value. - verified, err := verifyRoleTagValue(s, rTag) + verified, err := verifyRoleTagValue(rTag, imageEntry) if err != nil { return nil, err } @@ -297,6 +299,9 @@ func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { // Creates base64 encoded HMAC using a backend specific 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)) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 6122b7c113..1e2a1ed2dd 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -40,30 +40,51 @@ func pathLogin(b *backend) *framework.Path { // 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, identityDoc *identityDocument) error { +func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocument) (*ec2.DescribeInstancesOutput, error) { // Create an EC2 client to pull the instance information ec2Client, err := b.clientEC2(s, identityDoc.Region, false) if err != nil { - return err + return nil, err } - // Get the status of the instance - instanceStatus, err := ec2Client.DescribeInstanceStatus(&ec2.DescribeInstanceStatusInput{ - InstanceIds: []*string{aws.String(identityDoc.InstanceID)}, + status, err := ec2Client.DescribeInstances(&ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("instance-id"), + Values: []*string{ + aws.String(identityDoc.InstanceID), + }, + }, + }, }) if err != nil { - return err + return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", identityDoc.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 != identityDoc.InstanceID { + return nil, fmt.Errorf("expected instance ID does not match 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") + } // Validate the instance through InstanceState, InstanceStatus and SystemStatus - return validateInstanceStatus(instanceStatus) + 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, imageEntry *awsImageEntry) error { - // If reauthentication is disabled, doesn't matter what other metadata is provided, // authentication will not succeed. if storedIdentity.DisallowReauthentication { @@ -186,7 +207,8 @@ func (b *backend) pathLoginUpdate( } // Validate the instance ID. - if err := b.validateInstance(req.Storage, identityDoc); err != nil { + instanceDesc, err := b.validateInstance(req.Storage, identityDoc) + if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil } @@ -231,7 +253,7 @@ func (b *backend) pathLoginUpdate( // Role tag is enabled for the AMI. if imageEntry.RoleTag != "" { // Overwrite the policies with the ones returned from processing the role tag. - resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, imageEntry) + resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, imageEntry, instanceDesc) if err != nil { return nil, err } @@ -306,50 +328,21 @@ func (b *backend) pathLoginUpdate( } -// fetchRoleTagValue creates an AWS EC2 client and queries the tags -// attached to the instance identified by the given instanceID. -func (b *backend) fetchRoleTagValue(s logical.Storage, region string, tagKey string) (string, error) { - ec2Client, err := b.clientEC2(s, region, false) - if err != nil { - return "", err - } - - // Retrieve the instance tag with a "key" filter matching tagKey. - tagsOutput, err := ec2Client.DescribeTags(&ec2.DescribeTagsInput{ - Filters: []*ec2.Filter{ - &ec2.Filter{ - Name: aws.String("key"), - Values: []*string{ - aws.String(tagKey), - }, - }, - }, - }) - if err != nil { - return "", err - } - - if tagsOutput.Tags == nil || - len(tagsOutput.Tags) != 1 || - *tagsOutput.Tags[0].Key != tagKey || - *tagsOutput.Tags[0].ResourceType != "instance" { - return "", nil - } - - return *tagsOutput.Tags[0].Value, 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, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) { +func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) { - // Make a secondary call to the AWS instance to see if the desired tag is set. - // NOTE: If AWS adds the instance tags as meta-data in the instance identity - // document, then it is better to look this information there instead of making - // another API call. Currently, we don't have an option but make this call. - rTagValue, err := b.fetchRoleTagValue(s, identityDoc.Region, imageEntry.RoleTag) - if err != nil { - return nil, err + 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", imageEntry.RoleTag) + } + + rTagValue := "" + for _, tagItem := range tags { + if tagItem.Key != nil && *tagItem.Key == imageEntry.RoleTag { + rTagValue = *tagItem.Value + break + } } if rTagValue == "" { @@ -439,47 +432,6 @@ func (b *backend) pathLoginRenew( return framework.LeaseExtend(req.Auth.TTL, maxTTL, b.System())(req, data) } -// Validates the instance by checking the InstanceState, InstanceStatus and SystemStatus -func validateInstanceStatus(instanceStatus *ec2.DescribeInstanceStatusOutput) error { - - if instanceStatus.InstanceStatuses == nil { - return fmt.Errorf("instance statuses not found") - } - - if len(instanceStatus.InstanceStatuses) != 1 { - return fmt.Errorf("length of instance statuses is more than 1") - } - - if instanceStatus.InstanceStatuses[0].InstanceState == nil { - return fmt.Errorf("instance state not found") - } - - // Instance should be in 'running'(code 16) state. - if *instanceStatus.InstanceStatuses[0].InstanceState.Code != 16 { - return fmt.Errorf("instance state is not 'running'") - } - - if instanceStatus.InstanceStatuses[0].InstanceStatus == nil { - return fmt.Errorf("instance status not found") - } - - // InstanceStatus should be 'ok' - if *instanceStatus.InstanceStatuses[0].InstanceStatus.Status != "ok" { - return fmt.Errorf("instance status is not 'ok'") - } - - if instanceStatus.InstanceStatuses[0].SystemStatus == nil { - return fmt.Errorf("system status not found") - } - - // SystemStatus should be 'ok' - if *instanceStatus.InstanceStatuses[0].SystemStatus.Status != "ok" { - return fmt.Errorf("system status is not 'ok'") - } - - return nil -} - // 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"` diff --git a/builtin/credential/aws/role_tag_hmac_key.go b/builtin/credential/aws/role_tag_hmac_key.go deleted file mode 100644 index 7b608de7c2..0000000000 --- a/builtin/credential/aws/role_tag_hmac_key.go +++ /dev/null @@ -1,44 +0,0 @@ -package aws - -import ( - "fmt" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/logical" -) - -const ( - RoleTagHMACKeyLocation = "role_tag_hmac_key" -) - -// hmacKey returns the key to HMAC the RoleTag value. The key is valid per backend mount. -// If a key is not created for the mount, a new key will be created. -func hmacKey(s logical.Storage) (string, error) { - raw, err := s.Get(RoleTagHMACKeyLocation) - if err != nil { - return "", fmt.Errorf("failed to read key: %v", err) - } - - key := "" - if raw != nil { - key = string(raw.Value) - } - - if key == "" { - key, err = uuid.GenerateUUID() - if err != nil { - return "", fmt.Errorf("failed to generate uuid: %v", err) - } - if s != nil { - entry := &logical.StorageEntry{ - Key: RoleTagHMACKeyLocation, - Value: []byte(key), - } - if err := s.Put(entry); err != nil { - return "", fmt.Errorf("failed to persist key: %v", err) - } - } - } - - return key, nil -} From 06a174c2f05323dbec13d3c16ad42075d805220d Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Sun, 24 Apr 2016 20:50:59 -0400 Subject: [PATCH 38/79] tidy endpoint fixes --- builtin/credential/aws/backend.go | 8 ++--- .../credential/aws/path_blacklist_roletag.go | 4 +-- ...tity.go => path_config_tidy_identities.go} | 32 ++++++++--------- ...oletag.go => path_config_tidy_roletags.go} | 36 +++++++++---------- builtin/credential/aws/path_login.go | 2 +- ...entity_tidy.go => path_tidy_identities.go} | 18 +++++----- ..._roletag_tidy.go => path_tidy_roletags.go} | 18 +++++----- .../credential/aws/path_whitelist_identity.go | 2 +- website/source/docs/auth/aws.html.md | 30 ++++++++-------- 9 files changed, 75 insertions(+), 75 deletions(-) rename builtin/credential/aws/{path_config_tidy_whitelist_identity.go => path_config_tidy_identities.go} (71%) rename builtin/credential/aws/{path_config_tidy_blacklist_roletag.go => path_config_tidy_roletags.go} (71%) rename builtin/credential/aws/{path_whitelist_identity_tidy.go => path_tidy_identities.go} (82%) rename builtin/credential/aws/{path_blacklist_roletag_tidy.go => path_tidy_roletags.go} (82%) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 52312949a8..5d1b96b8b3 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -48,14 +48,14 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { pathImageTag(b), pathConfigClient(b), pathConfigCertificate(b), - pathConfigTidyBlacklistRoleTag(b), - pathConfigTidyWhitelistIdentity(b), + pathConfigTidyRoleTags(b), + pathConfigTidyIdentities(b), pathListCertificates(b), pathBlacklistRoleTag(b), pathListBlacklistRoleTags(b), - pathBlacklistRoleTagTidy(b), + pathTidyRoleTags(b), pathWhitelistIdentity(b), - pathWhitelistIdentityTidy(b), + pathTidyIdentities(b), pathListWhitelistIdentities(b), }), } diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 4ebad43010..ec66984203 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -65,7 +65,7 @@ func (b *backend) pathBlacklistRoleTagExistenceCheck(req *logical.Request, data // Fetch an entry from the role tag blacklist for a given tag. func blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { - entry, err := s.Get("blacklist/roletag/" + tag) + entry, err := s.Get("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag))) if err != nil { return nil, err } @@ -190,7 +190,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( // Expiration time is decided by the max_ttl value. blEntry.ExpirationTime = currentTime.Add(rTag.MaxTTL) - entry, err := logical.StorageEntryJSON("blacklist/roletag/"+tag, blEntry) + entry, err := logical.StorageEntryJSON("blacklist/roletag/"+base64.StdEncoding.EncodeToString([]byte(tag)), blEntry) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_tidy_whitelist_identity.go b/builtin/credential/aws/path_config_tidy_identities.go similarity index 71% rename from builtin/credential/aws/path_config_tidy_whitelist_identity.go rename to builtin/credential/aws/path_config_tidy_identities.go index daf746e941..a1469c8e5a 100644 --- a/builtin/credential/aws/path_config_tidy_whitelist_identity.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -6,9 +6,9 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathConfigTidyWhitelistIdentity(b *backend) *framework.Path { +func pathConfigTidyIdentities(b *backend) *framework.Path { return &framework.Path{ - Pattern: "config/tidy/whitelist/identity$", + Pattern: "config/tidy/identities$", Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -23,19 +23,19 @@ expiration, before it is removed from the backend storage.`, }, }, - ExistenceCheck: b.pathConfigTidyWhitelistIdentityExistenceCheck, + ExistenceCheck: b.pathConfigTidyIdentitiesExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathConfigTidyWhitelistIdentityCreateUpdate, - logical.UpdateOperation: b.pathConfigTidyWhitelistIdentityCreateUpdate, + logical.CreateOperation: b.pathConfigTidyIdentitiesCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyIdentitiesCreateUpdate, }, - HelpSynopsis: pathConfigTidyWhitelistIdentityHelpSyn, - HelpDescription: pathConfigTidyWhitelistIdentityHelpDesc, + HelpSynopsis: pathConfigTidyIdentitiesHelpSyn, + HelpDescription: pathConfigTidyIdentitiesHelpDesc, } } -func (b *backend) pathConfigTidyWhitelistIdentityExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { +func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() @@ -47,7 +47,7 @@ func (b *backend) pathConfigTidyWhitelistIdentityExistenceCheck(req *logical.Req } func configTidyWhitelistIdentity(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { - entry, err := s.Get("config/tidy/whitelist/identity") + entry, err := s.Get("config/tidy/identities") if err != nil { return nil, err } @@ -62,7 +62,7 @@ func configTidyWhitelistIdentity(s logical.Storage) (*tidyWhitelistIdentityConfi return &result, nil } -func (b *backend) pathConfigTidyWhitelistIdentityCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() configEntry, err := configTidyWhitelistIdentity(req.Storage) @@ -85,7 +85,7 @@ func (b *backend) pathConfigTidyWhitelistIdentityCreateUpdate(req *logical.Reque configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } - entry, err := logical.StorageEntryJSON("config/tidy/whitelist/identity", configEntry) + entry, err := logical.StorageEntryJSON("config/tidy/identities", configEntry) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (b *backend) pathConfigTidyWhitelistIdentityCreateUpdate(req *logical.Reque return nil, nil } -func (b *backend) pathConfigTidyWhitelistIdentityRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() @@ -114,11 +114,11 @@ func (b *backend) pathConfigTidyWhitelistIdentityRead(req *logical.Request, data }, nil } -func (b *backend) pathConfigTidyWhitelistIdentityDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyIdentitiesDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() - if err := req.Storage.Delete("config/tidy/whitelist/identity"); err != nil { + if err := req.Storage.Delete("config/tidy/identities"); err != nil { return nil, err } @@ -130,10 +130,10 @@ type tidyWhitelistIdentityConfig struct { DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"` } -const pathConfigTidyWhitelistIdentityHelpSyn = ` +const pathConfigTidyIdentitiesHelpSyn = ` Configures the periodic tidying operation of the whitelisted identity entries. ` -const pathConfigTidyWhitelistIdentityHelpDesc = ` +const pathConfigTidyIdentitiesHelpDesc = ` By default, the expired entries in teb whitelist will be attempted to be removed periodically. This operation will look for expired items in the list and purge them. However, there is a safety buffer duration (defaults to 72h), which purges the entries, diff --git a/builtin/credential/aws/path_config_tidy_blacklist_roletag.go b/builtin/credential/aws/path_config_tidy_roletags.go similarity index 71% rename from builtin/credential/aws/path_config_tidy_blacklist_roletag.go rename to builtin/credential/aws/path_config_tidy_roletags.go index c3c493e6b5..6022c0aaa3 100644 --- a/builtin/credential/aws/path_config_tidy_blacklist_roletag.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -6,9 +6,9 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathConfigTidyBlacklistRoleTag(b *backend) *framework.Path { +func pathConfigTidyRoleTags(b *backend) *framework.Path { return &framework.Path{ - Pattern: "config/tidy/blacklist/roletag$", + Pattern: "config/tidy/roletags$", Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -23,21 +23,21 @@ expiration, before it is removed from the backend storage.`, }, }, - ExistenceCheck: b.pathConfigTidyBlacklistRoleTagExistenceCheck, + ExistenceCheck: b.pathConfigTidyRoleTagsExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathConfigTidyBlacklistRoleTagCreateUpdate, - logical.UpdateOperation: b.pathConfigTidyBlacklistRoleTagCreateUpdate, - logical.ReadOperation: b.pathConfigTidyBlacklistRoleTagRead, - logical.DeleteOperation: b.pathConfigTidyBlacklistRoleTagDelete, + logical.CreateOperation: b.pathConfigTidyRoleTagsCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyRoleTagsCreateUpdate, + logical.ReadOperation: b.pathConfigTidyRoleTagsRead, + logical.DeleteOperation: b.pathConfigTidyRoleTagsDelete, }, - HelpSynopsis: pathConfigTidyBlacklistRoleTagHelpSyn, - HelpDescription: pathConfigTidyBlacklistRoleTagHelpDesc, + HelpSynopsis: pathConfigTidyRoleTagsHelpSyn, + HelpDescription: pathConfigTidyRoleTagsHelpDesc, } } -func (b *backend) pathConfigTidyBlacklistRoleTagExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { +func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() @@ -49,7 +49,7 @@ func (b *backend) pathConfigTidyBlacklistRoleTagExistenceCheck(req *logical.Requ } func configTidyBlacklistRoleTag(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { - entry, err := s.Get("config/tidy/blacklist/roletag") + entry, err := s.Get("config/tidy/roletags") if err != nil { return nil, err } @@ -64,7 +64,7 @@ func configTidyBlacklistRoleTag(s logical.Storage) (*tidyBlacklistRoleTagConfig, return &result, nil } -func (b *backend) pathConfigTidyBlacklistRoleTagCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() configEntry, err := configTidyBlacklistRoleTag(req.Storage) @@ -87,7 +87,7 @@ func (b *backend) pathConfigTidyBlacklistRoleTagCreateUpdate(req *logical.Reques configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } - entry, err := logical.StorageEntryJSON("config/tidy/blacklist/roletag", configEntry) + entry, err := logical.StorageEntryJSON("config/tidy/roletags", configEntry) if err != nil { return nil, err } @@ -99,7 +99,7 @@ func (b *backend) pathConfigTidyBlacklistRoleTagCreateUpdate(req *logical.Reques return nil, nil } -func (b *backend) pathConfigTidyBlacklistRoleTagRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() @@ -116,11 +116,11 @@ func (b *backend) pathConfigTidyBlacklistRoleTagRead(req *logical.Request, data }, nil } -func (b *backend) pathConfigTidyBlacklistRoleTagDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyRoleTagsDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() - if err := req.Storage.Delete("config/tidy/blacklist/roletag"); err != nil { + if err := req.Storage.Delete("config/tidy/roletags"); err != nil { return nil, err } @@ -132,10 +132,10 @@ type tidyBlacklistRoleTagConfig struct { DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"` } -const pathConfigTidyBlacklistRoleTagHelpSyn = ` +const pathConfigTidyRoleTagsHelpSyn = ` Configures the periodic tidying operation of the blacklisted role tag entries. ` -const pathConfigTidyBlacklistRoleTagHelpDesc = ` +const pathConfigTidyRoleTagsHelpDesc = ` 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 purge them. However, there is a safety buffer duration (defaults to 72h), which purges the entries, diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 1e2a1ed2dd..7dc815d1ce 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -461,6 +461,6 @@ provided. All future logins will succeed only if the client nonce matches the no whitelisted entry. The entries in the whitelist are not automatically deleted. Although, they will have an -expiration time set on the entry. There is a separate endpoint 'whitelist/identity/tidy', +expiration time set on the entry. There is a separate endpoint 'tidy/identities', that needs to be invoked to clean-up all the expired entries in the whitelist. ` diff --git a/builtin/credential/aws/path_whitelist_identity_tidy.go b/builtin/credential/aws/path_tidy_identities.go similarity index 82% rename from builtin/credential/aws/path_whitelist_identity_tidy.go rename to builtin/credential/aws/path_tidy_identities.go index b80c495966..2546beffac 100644 --- a/builtin/credential/aws/path_whitelist_identity_tidy.go +++ b/builtin/credential/aws/path_tidy_identities.go @@ -8,9 +8,9 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathWhitelistIdentityTidy(b *backend) *framework.Path { +func pathTidyIdentities(b *backend) *framework.Path { return &framework.Path{ - Pattern: "whitelist/identity/tidy$", + Pattern: "tidy/identities$", Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -21,11 +21,11 @@ expiration, before it is removed from the backend storage.`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathWhitelistIdentityTidyUpdate, + logical.UpdateOperation: b.pathTidyIdentitiesUpdate, }, - HelpSynopsis: pathWhitelistIdentityTidySyn, - HelpDescription: pathWhitelistIdentityTidyDesc, + HelpSynopsis: pathTidyIdentitiesSyn, + HelpDescription: pathTidyIdentitiesDesc, } } @@ -67,17 +67,17 @@ func tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error { return nil } -// pathWhitelistIdentityTidyUpdate is used to delete entries in the whitelist that are expired. -func (b *backend) pathWhitelistIdentityTidyUpdate( +// pathTidyIdentitiesUpdate is used to delete entries in the whitelist that are expired. +func (b *backend) pathTidyIdentitiesUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { return nil, tidyWhitelistIdentity(req.Storage, data.Get("safety_buffer").(int)) } -const pathWhitelistIdentityTidySyn = ` +const pathTidyIdentitiesSyn = ` Clean-up the whitelisted instance identity entries. ` -const pathWhitelistIdentityTidyDesc = ` +const pathTidyIdentitiesDesc = ` When an instance identity is whitelisted, the expiration time of the whitelist entry is set to the least amont 'max_ttl' of the registered AMI, 'max_ttl' of the role tag and 'max_ttl' of the backend mount. diff --git a/builtin/credential/aws/path_blacklist_roletag_tidy.go b/builtin/credential/aws/path_tidy_roletags.go similarity index 82% rename from builtin/credential/aws/path_blacklist_roletag_tidy.go rename to builtin/credential/aws/path_tidy_roletags.go index 1a506840a8..00279b9407 100644 --- a/builtin/credential/aws/path_blacklist_roletag_tidy.go +++ b/builtin/credential/aws/path_tidy_roletags.go @@ -8,9 +8,9 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathBlacklistRoleTagTidy(b *backend) *framework.Path { +func pathTidyRoleTags(b *backend) *framework.Path { return &framework.Path{ - Pattern: "blacklist/roletag/tidy$", + Pattern: "tidy/roletags$", Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -21,11 +21,11 @@ expiration, before it is removed from the backend storage.`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathBlacklistRoleTagTidyUpdate, + logical.UpdateOperation: b.pathTidyRoleTagsUpdate, }, - HelpSynopsis: pathBlacklistRoleTagTidySyn, - HelpDescription: pathBlacklistRoleTagTidyDesc, + HelpSynopsis: pathTidyRoleTagsSyn, + HelpDescription: pathTidyRoleTagsDesc, } } @@ -66,17 +66,17 @@ func tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error { return nil } -// pathBlacklistRoleTagTidyUpdate is used to clean-up the entries in the role tag blacklist. -func (b *backend) pathBlacklistRoleTagTidyUpdate( +// pathTidyRoleTagsUpdate is used to clean-up the entries in the role tag blacklist. +func (b *backend) pathTidyRoleTagsUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { return nil, tidyBlacklistRoleTag(req.Storage, data.Get("safety_buffer").(int)) } -const pathBlacklistRoleTagTidySyn = ` +const pathTidyRoleTagsSyn = ` Clean-up the blacklisted role tag entries. ` -const pathBlacklistRoleTagTidyDesc = ` +const pathTidyRoleTagsDesc = ` When a role tag is blacklisted, the expiration time of the blacklist entry is determined by the 'max_ttl' present in the role tag. If 'max_ttl' is not provided in the role tag, the backend mount's 'max_ttl' value will be used to determine diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 97979eae3a..b15dad507e 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -147,7 +147,7 @@ Each login from an EC2 instance creates/updates an entry in the identity whiteli Entries in this list can be viewed or deleted using this endpoint. The entries in the whitelist are not automatically deleted. Although, they will have an -expiration time set on the entry. There is a separate endpoint 'whitelist/identity/tidy', +expiration time set on the entry. There is a separate endpoint 'tidy/identities', that needs to be invoked to clean-up all the expired entries in the whitelist. ` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 31aa01d1fa..e50de88cf3 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -208,8 +208,8 @@ 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/whitelist/identity/tidy` and -`aws/auth/blacklist/roletag/tidy` are provided to clean up the entries present +The endpoints `aws/auth/tidy/identities` and +`aws/auth/tidy/roletags` 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. @@ -217,7 +217,7 @@ dictated by the safety buffer in order to actually remove the entry. Additionally, the backend performs has a periodic function that does the tidying of both blacklist role tags and whitelist identities. This periodic tidying is activated by default and will have a safety buffer of 72 hours. This can be -configured via `config/tidy/blacklist/roletag` and `config/tidy/whitelist/identity` +configured via `config/tidy/roletags` and `config/tidy/identities` endpoints. ### Varying Public Certificates @@ -546,7 +546,7 @@ The response will be in JSON. For example: -### /auth/aws/config/tidy/whitelist/identity +### /auth/aws/config/tidy/identities ##### POST
    Description
    @@ -558,7 +558,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/config/tidy/whitelist/identity`
    +
    `/auth/aws/config/tidy/identities`
    Parameters
    @@ -597,7 +597,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/config/tidy/whitelist/identity`
    +
    `/auth/aws/config/tidy/identities`
    Parameters
    @@ -635,7 +635,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws/config/tidy/whitelist/identity`
    +
    `/auth/aws/config/tidy/identities`
    Parameters
    @@ -649,7 +649,7 @@ The response will be in JSON. For example: -### /auth/aws/config/tidy/blacklist/roletag +### /auth/aws/config/tidy/roletags ##### POST
    Description
    @@ -661,7 +661,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/config/tidy/blacklist/roletag`
    +
    `/auth/aws/config/tidy/roletags`
    Parameters
    @@ -699,7 +699,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/config/tidy/blacklist/roletag`
    +
    `/auth/aws/config/tidy/roletags`
    Parameters
    @@ -737,7 +737,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws/config/tidy/blacklist/roletag`
    +
    `/auth/aws/config/tidy/roletags`
    Parameters
    @@ -1214,7 +1214,7 @@ The response will be in JSON. For example:
    -### /auth/aws/blacklist/roletag/tidy +### /auth/aws/tidy/roletags #### POST
    Description
    @@ -1226,7 +1226,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/blacklist/roletag/tidy`
    +
    `/auth/aws/tidy/roletags`
    Parameters
    @@ -1358,7 +1358,7 @@ The response will be in JSON. For example:
    -### /auth/aws/whitelist/identity/tidy +### /auth/aws/tidy/identities #### POST
    Description
    @@ -1370,7 +1370,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/whitelist/identity/tidy`
    +
    `/auth/aws/tidy/identities`
    Parameters
    From 2582e5d0ee4751fe2735fb9afafe77093e3a721d Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 25 Apr 2016 07:19:30 -0400 Subject: [PATCH 39/79] Added tests --- builtin/credential/aws/backend.go | 4 +- builtin/credential/aws/backend_test.go | 97 +++++++++++++++++++ .../credential/aws/path_blacklist_roletag.go | 2 +- .../aws/path_config_tidy_identities.go | 10 +- .../aws/path_config_tidy_roletags.go | 8 +- 5 files changed, 110 insertions(+), 11 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 5d1b96b8b3..b4cd1d860b 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -84,7 +84,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { // safety_buffer defaults to 72h safety_buffer := 259200 - tidyBlacklistConfigEntry, err := configTidyBlacklistRoleTag(req.Storage) + tidyBlacklistConfigEntry, err := configTidyRoleTags(req.Storage) if err != nil { return err } @@ -101,7 +101,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { // reset the safety_buffer to 72h safety_buffer = 259200 - tidyWhitelistConfigEntry, err := configTidyWhitelistIdentity(req.Storage) + tidyWhitelistConfigEntry, err := configTidyIdentities(req.Storage) if err != nil { return err } diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index a9e5316a37..111a51e499 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -11,6 +11,96 @@ import ( logicaltest "github.com/hashicorp/vault/logical/testing" ) +func TestBackend_ConfigTidyIdentities(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "safety_buffer": "60", + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/tidy/identities", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_ConfigTidyRoleTags(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "safety_buffer": "60", + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/tidy/roletags", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_TidyIdentities(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "tidy/identities", + 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 := Factory(config) + if err != nil { + t.Fatal(err) + } + + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "tidy/roletags", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } +} + func TestBackend_ConfigClient(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} @@ -421,6 +511,7 @@ func TestBackend_PathImageTag(t *testing.T) { } func TestBackend_PathBlacklistRoleTag(t *testing.T) { + // create the backend storage := &logical.InmemStorage{} config := logical.TestBackendConfig() config.StorageView = storage @@ -429,6 +520,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatal(err) } + // create an image entry data := map[string]interface{}{ "ami_id": "abcd-123", "policies": "p,q,r,s", @@ -444,6 +536,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatal(err) } + // create a role tag against an image registered before data2 := map[string]interface{}{ "policies": "p,q,r,s", } @@ -467,6 +560,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { 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: "blacklist/roletag/" + tag, @@ -479,6 +573,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatalf("failed to blacklist the roletag: %s\n", tag) } + // read the blacklist entry resp, err = b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, Path: "blacklist/roletag/" + tag, @@ -494,6 +589,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { 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: "blacklist/roletag/" + tag, @@ -503,6 +599,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatal(err) } + // try to read the deleted entry tagEntry, err := blacklistRoleTagEntry(storage, tag) if err != nil { t.Fatal(err) diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index ec66984203..8fa60ba972 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -89,7 +89,7 @@ func (b *backend) pathBlacklistRoleTagDelete( return logical.ErrorResponse("missing role_tag"), nil } - err := req.Storage.Delete("blacklist/roletag/" + tag) + err := req.Storage.Delete("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag))) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index a1469c8e5a..f53493a69e 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -28,6 +28,8 @@ expiration, before it is removed from the backend storage.`, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.CreateOperation: b.pathConfigTidyIdentitiesCreateUpdate, logical.UpdateOperation: b.pathConfigTidyIdentitiesCreateUpdate, + logical.ReadOperation: b.pathConfigTidyIdentitiesRead, + logical.DeleteOperation: b.pathConfigTidyIdentitiesDelete, }, HelpSynopsis: pathConfigTidyIdentitiesHelpSyn, @@ -39,14 +41,14 @@ func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, d b.configMutex.RLock() defer b.configMutex.RUnlock() - entry, err := configTidyWhitelistIdentity(req.Storage) + entry, err := configTidyIdentities(req.Storage) if err != nil { return false, err } return entry != nil, nil } -func configTidyWhitelistIdentity(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { +func configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { entry, err := s.Get("config/tidy/identities") if err != nil { return nil, err @@ -65,7 +67,7 @@ func configTidyWhitelistIdentity(s logical.Storage) (*tidyWhitelistIdentityConfi func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() - configEntry, err := configTidyWhitelistIdentity(req.Storage) + configEntry, err := configTidyIdentities(req.Storage) if err != nil { return nil, err } @@ -101,7 +103,7 @@ func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *frame b.configMutex.RLock() defer b.configMutex.RUnlock() - clientConfig, err := configTidyWhitelistIdentity(req.Storage) + clientConfig, err := configTidyIdentities(req.Storage) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index 6022c0aaa3..eecb443acc 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -41,14 +41,14 @@ func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, dat b.configMutex.RLock() defer b.configMutex.RUnlock() - entry, err := configTidyBlacklistRoleTag(req.Storage) + entry, err := configTidyRoleTags(req.Storage) if err != nil { return false, err } return entry != nil, nil } -func configTidyBlacklistRoleTag(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { +func configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { entry, err := s.Get("config/tidy/roletags") if err != nil { return nil, err @@ -67,7 +67,7 @@ func configTidyBlacklistRoleTag(s logical.Storage) (*tidyBlacklistRoleTagConfig, func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() - configEntry, err := configTidyBlacklistRoleTag(req.Storage) + configEntry, err := configTidyRoleTags(req.Storage) if err != nil { return nil, err } @@ -103,7 +103,7 @@ func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framewo b.configMutex.RLock() defer b.configMutex.RUnlock() - clientConfig, err := configTidyBlacklistRoleTag(req.Storage) + clientConfig, err := configTidyRoleTags(req.Storage) if err != nil { return nil, err } From b05042ff2d9319d9bc0a9c973cef55478c8f172a Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 25 Apr 2016 10:40:05 -0400 Subject: [PATCH 40/79] Added tests --- builtin/credential/aws/backend_test.go | 89 ++++++++++++++++++++++++ builtin/credential/aws/path_image_tag.go | 4 ++ 2 files changed, 93 insertions(+) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 111a51e499..9214aaa29b 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1,6 +1,7 @@ package aws import ( + "encoding/base64" "fmt" "os" "strings" @@ -11,6 +12,85 @@ import ( logicaltest "github.com/hashicorp/vault/logical/testing" ) +func TestBackend_HMAC(t *testing.T) { + nonce, err := createRoleTagNonce() + if err != nil { + t.Fatal(err) + } + rTag := &roleTag{ + Version: "v1", + Nonce: nonce, + AmiID: "abcd-123", + } + + rTag.Version = "" + val, err := prepareRoleTagPlaintextValue(rTag) + if err == nil { + t.Fatalf("expected error for missing version") + } + rTag.Version = "v1" + + rTag.Nonce = "" + val, err = prepareRoleTagPlaintextValue(rTag) + if err == nil { + t.Fatalf("expected error for missing nonce") + } + rTag.Nonce = nonce + + rTag.AmiID = "" + val, err = prepareRoleTagPlaintextValue(rTag) + if err == nil { + t.Fatalf("expected error for missing ami_id") + } + rTag.AmiID = "abcd-123" + + val, err = prepareRoleTagPlaintextValue(rTag) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(val, "a=") || + !strings.Contains(val, "p=") || + !strings.Contains(val, "d=") || + !strings.HasPrefix(val, "v1") { + t.Fatalf("incorrect information in role tag plaintext value") + } + + rTag.InstanceID = "instance-123" + val, err = prepareRoleTagPlaintextValue(rTag) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(val, "i=") { + t.Fatalf("missing instance ID in role tag plaintext value") + } + + rTag.MaxTTL = 200 + val, err = prepareRoleTagPlaintextValue(rTag) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(val, "t=") { + t.Fatalf("missing instance ID in role tag plaintext value") + } +} + +func TestBackend_CreateRoleTagNonce(t *testing.T) { + nonce, err := createRoleTagNonce() + if err != nil { + t.Fatal(err) + } + if nonce == "" { + t.Fatalf("failed to create role tag nonce") + } + 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) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} @@ -391,6 +471,7 @@ func TestBackend_PathImage(t *testing.T) { } func TestBackend_parseRoleTagValue(t *testing.T) { + // create a backend config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -399,6 +480,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { t.Fatal(err) } + // create an entry for an AMI data := map[string]interface{}{ "policies": "p,q,r,s", "max_ttl": "120s", @@ -414,6 +496,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { t.Fatal(err) } + // verify that the entry is created resp, err := b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, Path: "image/abcd-123", @@ -423,6 +506,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { t.Fatalf("expected an image entry for abcd-123") } + // create a role tag data2 := map[string]interface{}{ "policies": "p,q,r,s", } @@ -441,6 +525,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { } tagValue := resp.Data["tag_value"].(string) + // parse the value and check if the verifiable values match rTag, err := parseRoleTagValue(storage, tagValue) if err != nil { t.Fatalf("err: %s", err) @@ -616,6 +701,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { return } + // create the backend storage := &logical.InmemStorage{} config := logical.TestBackendConfig() config.StorageView = storage @@ -624,6 +710,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatal(err) } + // get the API credentials from env vars clientConfig := map[string]interface{}{ "access_key": os.Getenv("AWS_ACCESS_KEY"), "secret_key": os.Getenv("AWS_SECRET_KEY"), @@ -633,6 +720,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatalf("credentials not configured") } + // store the credentials _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, Storage: storage, @@ -643,6 +731,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatal(err) } + // write an entry for an ami data := map[string]interface{}{ "policies": "root", "max_ttl": "120s", diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index b139a2c533..acf2168578 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -206,6 +206,10 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { return "", fmt.Errorf("missing ami_id") } + if rTag.Policies == nil || len(rTag.Policies) == 0 { + rTag.Policies = []string{"default"} + } + // Attach Version, Nonce, AMI ID, Policies, DisallowReauthentication fields. value := fmt.Sprintf("%s:%s:a=%s:p=%s:d=%s", rTag.Version, rTag.Nonce, rTag.AmiID, strings.Join(rTag.Policies, ","), strconv.FormatBool(rTag.DisallowReauthentication)) From 3ee386ecb444c6b478bac7525faeee995aba319d Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 25 Apr 2016 11:44:58 -0400 Subject: [PATCH 41/79] Added tests --- builtin/credential/aws/backend_test.go | 50 +++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 9214aaa29b..2c95f6cab0 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -12,7 +12,55 @@ import ( logicaltest "github.com/hashicorp/vault/logical/testing" ) -func TestBackend_HMAC(t *testing.T) { +func TestBackend_createRoleTagValue(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "policies": "p,q,r,s", + } + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/abcd-123", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + imageEntry, err := awsImage(storage, "abcd-123") + if err != nil { + t.Fatal(err) + } + + nonce, err := createRoleTagNonce() + if err != nil { + t.Fatal(err) + } + rTag := &roleTag{ + Version: "v1", + AmiID: "abcd-123", + Nonce: nonce, + Policies: []string{"p", "q", "r"}, + MaxTTL: 200, + } + val, err := createRoleTagValue(rTag, imageEntry) + if err != nil { + t.Fatal(err) + } + if val == "" { + t.Fatalf("failed to create role tag") + } +} + +func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { nonce, err := createRoleTagNonce() if err != nil { t.Fatal(err) From 9f06bb2cea367ffde451b621e1778f4546112e20 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 25 Apr 2016 12:13:12 -0400 Subject: [PATCH 42/79] Added tests --- builtin/credential/aws/backend_test.go | 103 ++++++++++++++++++++++- builtin/credential/aws/path_image_tag.go | 27 +++++- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 2c95f6cab0..600fafdf2f 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -12,7 +12,8 @@ import ( logicaltest "github.com/hashicorp/vault/logical/testing" ) -func TestBackend_createRoleTagValue(t *testing.T) { +func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { + // create a backend config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -22,6 +23,7 @@ func TestBackend_createRoleTagValue(t *testing.T) { t.Fatal(err) } + // create an entry for ami data := map[string]interface{}{ "policies": "p,q,r,s", } @@ -35,32 +37,99 @@ func TestBackend_createRoleTagValue(t *testing.T) { t.Fatal(err) } + // read the created image entry imageEntry, err := awsImage(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) } - rTag := &roleTag{ + rTag1 := &roleTag{ Version: "v1", AmiID: "abcd-123", Nonce: nonce, Policies: []string{"p", "q", "r"}, MaxTTL: 200, } - val, err := createRoleTagValue(rTag, imageEntry) + + // create a role tag against the image entry + val, err := createRoleTagValue(rTag1, imageEntry) if err != nil { t.Fatal(err) } if val == "" { t.Fatalf("failed to create role tag") } + + // parse the created role tag + rTag2, err := parseRoleTagValue(storage, val) + if err != nil { + t.Fatal(err) + } + + // check the values in parsed role tag + if rTag2.Version != "v1" || + rTag2.Nonce != nonce || + rTag2.AmiID != "abcd-123" || + rTag2.MaxTTL != 200 || + !policyutil.EquivalentPolicies(rTag2.Policies, []string{"p", "q", "r"}) || + len(rTag2.HMAC) == 0 { + t.Fatalf("parsed role tag is invalid") + } + + // verify the tag contents using image specific HMAC key + verified, err := verifyRoleTagValue(rTag2, imageEntry) + if err != nil { + t.Fatal(err) + } + if !verified { + t.Fatalf("failed to verify the role tag") + } + + // register a different ami + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/ami-6789", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatal(err) + } + + // entry for the newly created ami entry + imageEntry2, err := awsImage(storage, "ami-6789") + if err != nil { + t.Fatal(err) + } + + // try to verify the tag created with previous image's HMAC key + // with the newly registered entry's HMAC key + verified, err = verifyRoleTagValue(rTag2, imageEntry2) + if err != nil { + t.Fatal(err) + } + if verified { + t.Fatalf("verification of role tag should have failed: invalid AMI ID") + } + + // modify any value in role tag and try to verify it + rTag2.Version = "v2" + verified, err = verifyRoleTagValue(rTag2, imageEntry) + 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) @@ -72,6 +141,8 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { } 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") @@ -79,6 +150,8 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { 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") @@ -86,16 +159,21 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { rTag.Nonce = nonce rTag.AmiID = "" + // try to create plaintext part of role tag + // without specifying ami_id val, err = prepareRoleTagPlaintextValue(rTag) if err == nil { t.Fatalf("expected error for missing ami_id") } rTag.AmiID = "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, "a=") || !strings.Contains(val, "p=") || !strings.Contains(val, "d=") || @@ -104,25 +182,30 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { } 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 = 200 + // 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 instance ID 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) @@ -130,6 +213,8 @@ func TestBackend_CreateRoleTagNonce(t *testing.T) { 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) @@ -140,6 +225,7 @@ func TestBackend_CreateRoleTagNonce(t *testing.T) { } func TestBackend_ConfigTidyIdentities(t *testing.T) { + // create a backend config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -149,10 +235,10 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { t.Fatal(err) } + // configure the tidying behavior of whitelist identities data := map[string]interface{}{ "safety_buffer": "60", } - _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, Path: "config/tidy/identities", @@ -187,6 +273,15 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { if err != nil { t.Fatal(err) } + + resp, err := b.HandleRequest(&logical.Request{ + Operation: logical.ReadOperation, + Path: "config/tidy/roletags", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } } func TestBackend_TidyIdentities(t *testing.T) { diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index acf2168578..563f54c7bc 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -149,6 +149,14 @@ func (b *backend) pathImageTagUpdate( // 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, imageEntry *awsImageEntry) (string, error) { + if rTag == nil { + return "", fmt.Errorf("nil role tag") + } + + if imageEntry == nil { + return "", fmt.Errorf("nil image entry") + } + // Attach version, nonce, policies and maxTTL to the role tag value. rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) if err != nil { @@ -161,6 +169,10 @@ func createRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (string, error // 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, imageEntry *awsImageEntry) (string, error) { + if imageEntry == nil { + return "", fmt.Errorf("nil image entry") + } + // Create the HMAC of the value hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlainText) if err != nil { @@ -180,6 +192,14 @@ func appendHMAC(rTagPlainText string, imageEntry *awsImageEntry) (string, error) // computes the HMAC from it using the backend specific key and // compares it with the received HMAC. func verifyRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (bool, error) { + if rTag == nil { + return false, fmt.Errorf("nil role tag") + } + + if imageEntry == nil { + return false, fmt.Errorf("nil image entry") + } + // Fetch the plaintext part of role tag rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) if err != nil { @@ -196,6 +216,9 @@ func verifyRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (bool, error) // 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") } @@ -335,7 +358,9 @@ type roleTag struct { } func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { - return rTag1.Version == rTag2.Version && + return rTag1 != nil && + rTag2 != nil && + rTag1.Version == rTag2.Version && rTag1.Nonce == rTag2.Nonce && policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && rTag1.MaxTTL == rTag2.MaxTTL && From 7e821db4f0ee1415a93b4003972856e16ee14065 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 26 Apr 2016 23:40:11 -0400 Subject: [PATCH 43/79] Added tests --- builtin/credential/aws/backend_test.go | 309 +++++++++++++----- .../credential/aws/path_config_certificate.go | 2 + 2 files changed, 225 insertions(+), 86 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 600fafdf2f..4861344725 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -235,19 +235,44 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { t.Fatal(err) } - // configure the tidying behavior of whitelist identities - data := map[string]interface{}{ - "safety_buffer": "60", - } - _, err = b.HandleRequest(&logical.Request{ + // test update operation + tidyRequest := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/tidy/identities", Storage: storage, - Data: data, - }) + } + 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/identities 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/identities") + } } func TestBackend_ConfigTidyRoleTags(t *testing.T) { @@ -260,28 +285,44 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { t.Fatal(err) } - data := map[string]interface{}{ - "safety_buffer": "60", - } - - _, err = b.HandleRequest(&logical.Request{ + // test update operation + tidyRequest := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/tidy/roletags", Storage: storage, - Data: data, - }) + } + data := map[string]interface{}{ + "safety_buffer": "60", + "disable_periodic_tidy": true, + } + tidyRequest.Data = data + _, err = b.HandleRequest(tidyRequest) if err != nil { t.Fatal(err) } - resp, err := b.HandleRequest(&logical.Request{ - Operation: logical.ReadOperation, - Path: "config/tidy/roletags", - Storage: storage, - }) + // 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/roletags 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/roletags") + } } func TestBackend_TidyIdentities(t *testing.T) { @@ -294,6 +335,7 @@ func TestBackend_TidyIdentities(t *testing.T) { t.Fatal(err) } + // test update operation _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, Path: "tidy/identities", @@ -314,6 +356,7 @@ func TestBackend_TidyRoleTags(t *testing.T) { t.Fatal(err) } + // test update operation _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, Path: "tidy/roletags", @@ -381,6 +424,7 @@ func TestBackend_ConfigClient(t *testing.T) { }, }) + // test existence check returning false checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{ Operation: logical.CreateOperation, Path: "config/client", @@ -396,6 +440,7 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal("existence check should have returned 'false' for 'config/client'") } + // create an entry configClientCreateRequest := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/client", @@ -407,6 +452,7 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal(err) } + //test existence check returning true checkFound, exists, err = b.HandleExistenceCheck(&logical.Request{ Operation: logical.CreateOperation, Path: "config/client", @@ -422,6 +468,7 @@ func TestBackend_ConfigClient(t *testing.T) { t.Fatal("existence check should have returned 'true' for 'config/client'") } + // test the "config/client" read helper clientConfig, err := clientConfigEntry(storage) if err != nil { t.Fatal(err) @@ -432,7 +479,7 @@ func TestBackend_ConfigClient(t *testing.T) { } } -func TestBackend_PathConfigCertificate(t *testing.T) { +func TestBackend_pathConfigCertificate(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -442,11 +489,12 @@ func TestBackend_PathConfigCertificate(t *testing.T) { t.Fatal(err) } - checkFound, exists, err := b.HandleExistenceCheck(&logical.Request{ + certReq := &logical.Request{ Operation: logical.CreateOperation, - Path: "config/certificate/cert1", Storage: storage, - }) + Path: "config/certificate/cert1", + } + checkFound, exists, err := b.HandleExistenceCheck(certReq) if err != nil { t.Fatal(err) } @@ -479,21 +527,17 @@ R0J5cUdTTTQ0QkFNREx3QXdMQUlVV1hCbGs0MHhUd1N3CjdIWDMyTXhYWXJ1c2U5QUNGQk5HbWRY MlpCclZOR3JOOU4yZjZST2swazlLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K `, } - _, err = b.HandleRequest(&logical.Request{ - Operation: logical.CreateOperation, - Path: "config/certificate/cert1", - Storage: storage, - Data: data, - }) + + certReq.Data = data + // test create operation + _, err = b.HandleRequest(certReq) if err != nil { t.Fatal(err) } - checkFound, exists, err = b.HandleExistenceCheck(&logical.Request{ - Operation: logical.CreateOperation, - Path: "config/certificate/cert1", - Storage: storage, - }) + certReq.Data = nil + // test existence check + checkFound, exists, err = b.HandleExistenceCheck(certReq) if err != nil { t.Fatal(err) } @@ -504,11 +548,9 @@ MlpCclZOR3JOOU4yZjZST2swazlLCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K t.Fatal("existence check should have returned 'true' for 'config/certificate/cert1'") } - resp, err := b.HandleRequest(&logical.Request{ - Operation: logical.ReadOperation, - Path: "config/certificate/cert1", - Storage: storage, - }) + certReq.Operation = logical.ReadOperation + // test read operation + resp, err := b.HandleRequest(certReq) expectedCert := `-----BEGIN CERTIFICATE----- MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD @@ -531,9 +573,60 @@ vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw 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_PathImage(t *testing.T) { +func TestBackend_pathImage(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -570,6 +663,7 @@ func TestBackend_PathImage(t *testing.T) { } data["allow_instance_migration"] = true + data["disallow_reauthentication"] = true _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, Path: "image/ami-abcd123", @@ -587,8 +681,35 @@ func TestBackend_PathImage(t *testing.T) { if err != nil { t.Fatal(err) } - if !resp.Data["allow_instance_migration"].(bool) { - t.Fatal("bad: allow_instance_migration: expected:true got:false\n") + 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 image entries + _, err = b.HandleRequest(&logical.Request{ + Operation: logical.UpdateOperation, + Path: "image/ami-abcd456", + Data: data, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.ListOperation, + Path: "images", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp == nil || resp.Data == nil || resp.IsError() { + t.Fatalf("failed to list the image entries") + } + keys := resp.Data["keys"].([]string) + if len(keys) != 2 { + t.Fatalf("bad: keys: %#v\n", keys) } _, err = b.HandleRequest(&logical.Request{ @@ -611,6 +732,7 @@ func TestBackend_PathImage(t *testing.T) { if resp != nil { t.Fatalf("bad: response: expected:nil actual:%#v\n", resp) } + } func TestBackend_parseRoleTagValue(t *testing.T) { @@ -838,12 +960,27 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { } // 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") + } + // create the backend storage := &logical.InmemStorage{} config := logical.TestBackendConfig() @@ -853,35 +990,44 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatal(err) } - // get the API credentials from env vars - clientConfig := map[string]interface{}{ - "access_key": os.Getenv("AWS_ACCESS_KEY"), - "secret_key": os.Getenv("AWS_SECRET_KEY"), - } - if clientConfig["access_key"] == "" || - clientConfig["secret_key"] == "" { - t.Fatalf("credentials not configured") + 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) + } } - // store the credentials - _, err = b.HandleRequest(&logical.Request{ - Operation: logical.UpdateOperation, - Storage: storage, - Path: "config/client", - Data: clientConfig, - }) - if err != nil { - t.Fatal(err) - } - - // write an entry for an ami + // create an entry for the AMI. This is required for login to work. data := map[string]interface{}{ "policies": "root", "max_ttl": "120s", } + _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/ami-fce3c696", + Path: "image/" + amiID, Storage: storage, Data: data, }) @@ -889,23 +1035,12 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatal(err) } - loginInput := map[string]interface{}{"pkcs7": `MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewog -ICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMu -NjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAi -MjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJv -ZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJ -ZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVu -ZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4 -ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVn -aW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkw -FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6 -b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMx -CwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEW -BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U -0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA`, + loginInput := map[string]interface{}{ + "pkcs7": pkcs7, "nonce": "vault-client-nonce", } + // perform the login operation. loginRequest := &logical.Request{ Operation: logical.UpdateOperation, Path: "login", @@ -916,20 +1051,20 @@ BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U if err != nil { t.Fatal(err) } - if resp == nil || resp.Auth == nil { - t.Fatalf("login attempt failed") + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Fatalf("first login attempt failed") } - // try to login again and see if it succeeds + // 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 { - t.Fatalf("login attempt failed") + if resp == nil || resp.Auth == nil || resp.IsError() { + t.Fatalf("second login attempt failed") } - //instanceID := resp.Auth.Metadata.(map[string]string)["instance_id"] + // 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") @@ -945,6 +1080,7 @@ BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U 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: "whitelist/identity/" + instanceID, @@ -954,10 +1090,11 @@ BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U if err != nil { t.Fatal(err) } - if resp == nil || resp.Data == nil || resp.Data["ami_id"] != "ami-fce3c696" { + if resp == nil || resp.Data == nil || resp.Data["ami_id"] != amiID { t.Fatalf("failed to read whitelist identity") } + // Delete the whitelist identity entry. wlRequest.Operation = logical.DeleteOperation resp, err = b.HandleRequest(wlRequest) if err != nil { @@ -967,12 +1104,12 @@ BBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U t.Fatalf("failed to delete whitelist identity") } - // try to login again and see if it succeeds + // Allow a fresh login. resp, err = b.HandleRequest(loginRequest) if err != nil { t.Fatal(err) } - if resp == nil || resp.Auth == nil { + if resp == nil || resp.Auth == nil || resp.IsError() { t.Fatalf("login attempt failed") } } diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 05e7b1d1d2..e6184ef765 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -192,6 +192,8 @@ func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) // 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 From 70ea33ccfc8cd03d71e616cf35112e53d630b029 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 11:32:05 -0400 Subject: [PATCH 44/79] Added ami_id to token metadata --- builtin/credential/aws/backend_test.go | 2 ++ builtin/credential/aws/path_login.go | 1 + 2 files changed, 3 insertions(+) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 4861344725..c07675e6e3 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -23,6 +23,8 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { t.Fatal(err) } + backend := b.(*backend) + // create an entry for ami data := map[string]interface{}{ "policies": "p,q,r,s", diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 7dc815d1ce..858a9d795e 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -311,6 +311,7 @@ func (b *backend) pathLoginUpdate( Metadata: map[string]string{ "instance_id": identityDoc.InstanceID, "role_tag_max_ttl": rTagMaxTTL.String(), + "ami_id": identityDoc.AmiID, }, LeaseOptions: logical.LeaseOptions{ Renewable: true, From 36193850fd093d49fc9780741adc3614c5794b40 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 20:01:39 -0400 Subject: [PATCH 45/79] Remove recreate parameter from clientEC2 --- builtin/credential/aws/backend.go | 49 +++++++++++++++++++------- builtin/credential/aws/backend_test.go | 2 -- builtin/credential/aws/client.go | 29 ++++++++++----- builtin/credential/aws/path_login.go | 2 +- 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index b4cd1d860b..e38401ef61 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -27,6 +27,8 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { } 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), @@ -67,20 +69,30 @@ type backend struct { *framework.Backend Salt *salt.Salt - configMutex sync.RWMutex - tidyCooldownPeriod time.Duration - nextTidyTime time.Time + // Lock to make changes to any of the backend's configuration endpoints. + configMutex sync.RWMutex + // Duration after which the periodic function of the backend needs to be + // executed. + tidyCooldownPeriod time.Duration + + // Var that holds the time at which the periodic func should initiatite + // the tidy operations. + 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. EC2ClientsMap map[string]*ec2.EC2 } // 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 are to cleanup the expired entries of both blacklist -// and whitelist. +// and whitelist. Tidying is done not once in a minute, but once in an hour. +// 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 { - b.configMutex.Lock() - defer b.configMutex.Unlock() if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { // safety_buffer defaults to 72h safety_buffer := 259200 @@ -89,12 +101,16 @@ func (b *backend) periodicFunc(req *logical.Request) error { 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 { tidyBlacklistRoleTag(req.Storage, safety_buffer) } @@ -106,28 +122,37 @@ func (b *backend) periodicFunc(req *logical.Request) error { 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 { tidyWhitelistIdentity(req.Storage, safety_buffer) } - // Update the lastTidyTime + // Update the nextTidyTime b.nextTidyTime = time.Now().Add(b.tidyCooldownPeriod) } return nil } const backendHelp = ` -AWS auth backend takes in a AWS EC2 instance identity document, its PKCS#7 signature -and a client created nonce to authenticates the instance with Vault. +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 association of AMIs to Vault's policies -through 'image/' endpoint. For instances that share an AMI, an instance tag can -be created through 'image//tag'. This tag should be attached to the EC2 instance -before the instance attempts to login to Vault. +through 'image/' endpoint. All the instances that are using this AMI will +get the policies configured on the AMI. + +If there is need to further restrict the policies set on the AMI, 'role_tag' option +can be enabled on the AMI and a tag can be generated using 'image//roletag' +endpoint. This tag represents the subset of capabilities set on the AMI. When the +'role_tag' option is enabled on the AMI, the login operation requires that a respective +role tag is attached to the EC2 instance that is performing the login. ` diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index c07675e6e3..4861344725 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -23,8 +23,6 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { t.Fatal(err) } - backend := b.(*backend) - // create an entry for ami data := map[string]interface{}{ "policies": "p,q,r,s", diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 92c4d29f80..1c3de00cf3 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -32,6 +32,7 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config if config != nil { switch { case config.AccessKey != "" && config.SecretKey != "": + // Add the static credential provider providers = append(providers, &credentials.StaticProvider{ Value: credentials.Value{ AccessKeyID: config.AccessKey, @@ -45,8 +46,10 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config } } + // Add the environment credential provider providers = append(providers, &credentials.EnvProvider{}) + // Add the instance metadata role provider // Create the credentials required to access the API. providers = append(providers, &ec2rolecreds.EC2RoleProvider{ Client: ec2metadata.New(session.New(&aws.Config{ @@ -70,34 +73,44 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config } // 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. func (b *backend) flushCachedEC2Clients() { b.configMutex.Lock() defer b.configMutex.Unlock() + // 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, recreate bool) (*ec2.EC2, error) { - if !recreate { - b.configMutex.RLock() - if b.EC2ClientsMap[region] != nil { - defer b.configMutex.RUnlock() - return b.EC2ClientsMap[region], nil - } - b.configMutex.RUnlock() +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 + } + + // Fetch the configured credentials 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_login.go b/builtin/credential/aws/path_login.go index 858a9d795e..3372f848ae 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -42,7 +42,7 @@ func pathLogin(b *backend) *framework.Path { // checks if the instance is running and is healthy. func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocument) (*ec2.DescribeInstancesOutput, error) { // Create an EC2 client to pull the instance information - ec2Client, err := b.clientEC2(s, identityDoc.Region, false) + ec2Client, err := b.clientEC2(s, identityDoc.Region) if err != nil { return nil, err } From aae439b9470f0030cdf876c45c6b2e428bbec926 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 20:13:56 -0400 Subject: [PATCH 46/79] Remove unnecessary lock switching around flushCachedEC2Clients --- builtin/credential/aws/client.go | 6 +++--- builtin/credential/aws/path_config_client.go | 6 ------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 1c3de00cf3..65ed6958af 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -75,10 +75,10 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config // 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. +// +// Lock should be actuired using b.configMutex.Lock() before calling this method and +// unlocked using b.configMutex.Unlock() after returning. func (b *backend) flushCachedEC2Clients() { - b.configMutex.Lock() - defer b.configMutex.Unlock() - // deleting items in map during iteration is safe. for region, _ := range b.EC2ClientsMap { delete(b.EC2ClientsMap, region) diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 331dd3d6d8..e43c0686d3 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -95,8 +95,6 @@ func (b *backend) pathConfigClientDelete( return nil, err } - b.configMutex.Unlock() - // Remove all the cached EC2 client objects in the backend. b.flushCachedEC2Clients() @@ -151,11 +149,7 @@ func (b *backend) pathConfigClientCreateUpdate( } if changedCreds { - // We have to be careful here to re-lock as we have a deferred unlock - // queued up and unlocking an unlocked mutex leads to a panic - b.configMutex.Unlock() b.flushCachedEC2Clients() - b.configMutex.Lock() } return nil, nil From 329361f9515ead754e55fd2891051860a1a47665 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 21:29:32 -0400 Subject: [PATCH 47/79] Removed existence check on blacklist/roletags, docs fixes --- builtin/credential/aws/backend_test.go | 6 +- .../credential/aws/path_blacklist_roletag.go | 57 ++++++++++--------- builtin/credential/aws/path_image_tag.go | 5 +- builtin/credential/aws/path_login.go | 2 +- .../credential/aws/path_whitelist_identity.go | 30 +++++----- website/source/docs/auth/aws.html.md | 4 +- 6 files changed, 51 insertions(+), 53 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 4861344725..1661e990d6 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -66,7 +66,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { } // parse the created role tag - rTag2, err := parseRoleTagValue(storage, val) + rTag2, err := parseAndVerifyRoleTagValue(storage, val) if err != nil { t.Fatal(err) } @@ -735,7 +735,7 @@ func TestBackend_pathImage(t *testing.T) { } -func TestBackend_parseRoleTagValue(t *testing.T) { +func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { // create a backend config := logical.TestBackendConfig() storage := &logical.InmemStorage{} @@ -791,7 +791,7 @@ func TestBackend_parseRoleTagValue(t *testing.T) { tagValue := resp.Data["tag_value"].(string) // parse the value and check if the verifiable values match - rTag, err := parseRoleTagValue(storage, tagValue) + rTag, err := parseAndVerifyRoleTagValue(storage, tagValue) if err != nil { t.Fatalf("err: %s", err) } diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 8fa60ba972..d4b7653883 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "time" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -14,12 +15,10 @@ func pathBlacklistRoleTag(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "role_tag": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Role tag that needs be blacklisted. The tag can be supplied as-is, or can be base64 encoded.", + Description: "Role tag that needs be blacklisted. The tag can be supplied as-is. In order to avoid any encoding problems, it can be base64 encoded.", }, }, - ExistenceCheck: b.pathBlacklistRoleTagExistenceCheck, - Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathBlacklistRoleTagUpdate, logical.ReadOperation: b.pathBlacklistRoleTagRead, @@ -55,15 +54,8 @@ func (b *backend) pathBlacklistRoleTagsList( return logical.ListResponse(tags), nil } -// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. -// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. -// -// A role should be allowed to be blacklisted even if it was prevously blacklisted. -func (b *backend) pathBlacklistRoleTagExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - return true, 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 blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { entry, err := s.Get("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag))) if err != nil { @@ -83,7 +75,6 @@ func blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntr // Deletes an entry from the role tag blacklist for a given tag. func (b *backend) pathBlacklistRoleTagDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - tag := data.Get("role_tag").(string) if tag == "" { return logical.ErrorResponse("missing role_tag"), nil @@ -115,10 +106,7 @@ func (b *backend) pathBlacklistRoleTagRead( } return &logical.Response{ - Data: map[string]interface{}{ - "creation_time": entry.CreationTime, - "expiration_time": entry.ExpirationTime, - }, + Data: structs.New(entry).Map(), }, nil } @@ -146,13 +134,16 @@ func (b *backend) pathBlacklistRoleTagUpdate( tag = string(tagBytes) } - // Parse the role tag from string form to a struct form. - rTag, err := parseRoleTagValue(req.Storage, tag) + // Parse and the role tag from string form to a struct form and verify it. + rTag, err := 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 AMI used by the instance. + // Get the entry for the AMI mentioned in the role tag. imageEntry, err := awsImage(req.Storage, rTag.AmiID) if err != nil { return nil, err @@ -161,6 +152,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( return logical.ErrorResponse("image entry not found"), nil } + // Check if the role tag is already blacklisted. If yes, update it. blEntry, err := blacklistRoleTagEntry(req.Storage, tag) if err != nil { return nil, err @@ -171,6 +163,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( currentTime := time.Now() + // Check if this is creation of entry. if blEntry.CreationTime.IsZero() { // Set the creation time for the blacklist entry. // This should not be updated after setting it once. @@ -178,16 +171,20 @@ func (b *backend) pathBlacklistRoleTagUpdate( blEntry.CreationTime = currentTime } + // Decide the expiration time based on the max_ttl values. + // If max_ttl is not set for the role tag, fall back on the mount's max_ttl. if rTag.MaxTTL == time.Duration(0) { rTag.MaxTTL = b.System().MaxLeaseTTL() } + // The max_ttl value on the role tag is scoped by the value set on the AMI entry. if imageEntry.MaxTTL > time.Duration(0) && rTag.MaxTTL > imageEntry.MaxTTL { rTag.MaxTTL = imageEntry.MaxTTL } - // Expiration time is decided by the max_ttl value. + // Expiration time is decided by least of the max_ttl values set on: + // role tag, ami entry, backend's mount. blEntry.ExpirationTime = currentTime.Add(rTag.MaxTTL) entry, err := logical.StorageEntryJSON("blacklist/roletag/"+base64.StdEncoding.EncodeToString([]byte(tag)), blEntry) @@ -195,7 +192,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( return nil, err } - // Store it. + // Store the blacklist entry. if err := req.Storage.Put(entry); err != nil { return nil, err } @@ -215,11 +212,15 @@ Blacklist a previously created role tag. const pathBlacklistRoleTagDesc = ` Blacklist a role tag so that it cannot be used by an EC2 instance to perform logins in the future. This can be used if the role tag is suspected or believed to be possessed -by an unauthorized entity. +by an unintended party. -The entries in the blacklist are not automatically deleted. Although, they will have an -expiration time set on the entry. There is a separate endpoint 'blacklist/roletag/tidy', -that needs to be invoked to clean-up all the expired entries in the blacklist. +By default, a cron task will periodically look for expired entries in the blacklist +and delete 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 pathListBlacklistRoleTagsHelpSyn = ` @@ -227,7 +228,7 @@ List the blacklisted role tags. ` const pathListBlacklistRoleTagsHelpDesc = ` -List all the entries present in the blacklist. This will show both the valid entries and -the expired entries in the blacklist. Use 'blacklist/roletag/tidy' endpoint to clean-up -the blacklist of role tags. +List 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. ` diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index 563f54c7bc..d4a5cc3c5b 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -249,8 +249,9 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { return value, nil } -// Parses the tag from string form into a struct form. -func parseRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { +// Parses the tag from string form into a struct form. This method +// also verifies the correctness of the parsed role tag. +func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { tagItems := strings.Split(tag, ":") // Tag must contain version, nonce, policies and HMAC if len(tagItems) < 4 { diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 3372f848ae..a939104613 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -351,7 +351,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc } // Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC. - rTag, err := parseRoleTagValue(s, rTagValue) + rTag, err := parseAndVerifyRoleTagValue(s, rTagValue) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index b15dad507e..5066456972 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -3,6 +3,7 @@ package aws import ( "time" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -29,7 +30,7 @@ func pathWhitelistIdentity(b *backend) *framework.Path { func pathListWhitelistIdentities(b *backend) *framework.Path { return &framework.Path{ - Pattern: "whitelist/identity/?", + Pattern: "whitelist/identities/?", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ListOperation: b.pathWhitelistIdentitiesList, @@ -91,8 +92,7 @@ func (b *backend) pathWhitelistIdentityDelete( return logical.ErrorResponse("missing instance_id"), nil } - err := req.Storage.Delete("whitelist/identity/" + instanceID) - if err != nil { + if err := req.Storage.Delete("whitelist/identity/" + instanceID); err != nil { return nil, err } @@ -116,25 +116,19 @@ func (b *backend) pathWhitelistIdentityRead( } return &logical.Response{ - Data: map[string]interface{}{ - "ami_id": entry.AmiID, - "creation_time": entry.CreationTime.String(), - "expiration_time": entry.ExpirationTime.String(), - "client_nonce": entry.ClientNonce, - "pending_time": entry.PendingTime, - }, + Data: structs.New(entry).Map(), }, nil } // Struct to represent each item in the identity whitelist. type whitelistIdentity struct { AmiID string `json:"ami_id" structs:"ami_id" mapstructure:"ami_id"` - DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"` - PendingTime string `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` ClientNonce string `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` - LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_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 pathWhitelistIdentitySyn = ` @@ -146,9 +140,10 @@ Each login from an EC2 instance creates/updates an entry in the identity whiteli Entries in this list can be viewed or deleted using this endpoint. -The entries in the whitelist are not automatically deleted. Although, they will have an -expiration time set on the entry. There is a separate endpoint 'tidy/identities', -that needs to be invoked to clean-up all the expired entries in the whitelist. +By default, a cron task will periodically look for expired entries in the whitelist +and delete 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 pathListWhitelistIdentitiesHelpSyn = ` @@ -158,5 +153,6 @@ List the items present in the identity whitelist. const pathListWhitelistIdentitiesHelpDesc = ` 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. +expired and un-expired entries. Use 'tidy/identities' endpoint to clean-up +the whitelist of identities. ` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index e50de88cf3..53825c4935 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -1161,7 +1161,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/blacklist/roletag?list=true`
    +
    `/auth/aws/blacklist/roletags?list=true`
    Parameters
    @@ -1305,7 +1305,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/whitelist/identity?list=true`
    +
    `/auth/aws/whitelist/identities?list=true`
    Parameters
    From 0b561d668bab64e27d2fb6c2bd5303305e90579b Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 22:03:11 -0400 Subject: [PATCH 48/79] Fix the list response of role tags --- builtin/credential/aws/path_blacklist_roletag.go | 11 +++++++++++ builtin/credential/aws/path_tidy_identities.go | 6 +++--- builtin/credential/aws/path_tidy_roletags.go | 7 +++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index d4b7653883..63a91c6165 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -51,6 +51,17 @@ func (b *backend) pathBlacklistRoleTagsList( if err != nil { return nil, err } + + // Tags are base64 encoded and then indexed 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 { + tags[i] = string(key) + } + } return logical.ListResponse(tags), nil } diff --git a/builtin/credential/aws/path_tidy_identities.go b/builtin/credential/aws/path_tidy_identities.go index 2546beffac..1e2101aa2e 100644 --- a/builtin/credential/aws/path_tidy_identities.go +++ b/builtin/credential/aws/path_tidy_identities.go @@ -79,10 +79,10 @@ Clean-up the whitelisted instance identity entries. const pathTidyIdentitiesDesc = ` When an instance identity is whitelisted, the expiration time of the whitelist -entry is set to the least amont 'max_ttl' of the registered AMI, 'max_ttl' of the -role tag and 'max_ttl' of the backend mount. +entry is set based on the least 'max_ttl' value set on: AMI entry, the role tag +and the backend's mount. -When this endpoint is invoked all the entries that are expired will be deleted. +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_roletags.go b/builtin/credential/aws/path_tidy_roletags.go index 00279b9407..3bb98bf7fb 100644 --- a/builtin/credential/aws/path_tidy_roletags.go +++ b/builtin/credential/aws/path_tidy_roletags.go @@ -78,11 +78,10 @@ Clean-up the blacklisted role tag entries. const pathTidyRoleTagsDesc = ` When a role tag is blacklisted, the expiration time of the blacklist entry is -determined by the 'max_ttl' present in the role tag. If 'max_ttl' is not provided -in the role tag, the backend mount's 'max_ttl' value will be used to determine -the expiration time of the blacklist entry. +set based on the least 'max_ttl' value set on: AMI entry, the role tag and the +backend's mount. -When this endpoint is invoked all the entries that are expired will be deleted. +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. From c7bfdd7ed0b0f734170e0b4fbf8d68fe20dc095b Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 22:25:15 -0400 Subject: [PATCH 49/79] Fix locking around config/client --- .../credential/aws/path_config_certificate.go | 44 +++++++++---------- builtin/credential/aws/path_config_client.go | 18 ++++---- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index e6184ef765..446803b77b 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" + "github.com/fatih/structs" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -131,17 +132,23 @@ func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) // awsPublicCertificates returns a slice of all the parsed AWS public // certificates, that were registered using `config/certificate/` endpoint. -// This method will also append two default certificates to the slice. +// This method will also append default certificate to the slice. func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { - // Get the list `cert_name`s of all the registered certificates. + 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 } - var certs []*x509.Certificate - // Iterate through each certificate, parse and append it to a slice. for _, cert := range registeredCerts { certEntry, err := b.awsPublicCertificateEntry(s, cert) @@ -158,13 +165,6 @@ func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, certs = append(certs, decodedCert) } - // Append the generic certificate provided in the documentation. - decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate) - if err != nil { - return nil, err - } - certs = append(certs, decodedCert) - return certs, nil } @@ -178,7 +178,6 @@ func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) return nil, err } if entry == nil { - // Existence check depends on this being nil when the storage entry is not present. return nil, nil } @@ -209,7 +208,6 @@ func (b *backend) pathConfigCertificateDelete(req *logical.Request, data *framew // 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 @@ -224,9 +222,7 @@ func (b *backend) pathConfigCertificateRead( } return &logical.Response{ - Data: map[string]interface{}{ - "aws_public_cert": certificateEntry.AWSPublicCert, - }, + Data: structs.New(certificateEntry).Map(), }, nil } @@ -234,7 +230,6 @@ func (b *backend) pathConfigCertificateRead( // 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 @@ -303,12 +298,17 @@ Configure the AWS Public Key that is used to verify the PKCS#7 signature of the ` const pathConfigCertificateDesc = ` -AWS Public Key used to verify the PKCS#7 signature of the identity document -varies by region. It can be found in AWS's 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), +AWS Public Key which is used to verify the PKCS#7 signature of the identity document, +varies by region. The public key 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 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 registered certificates +added using this endpoint. ` const pathListCertificatesHelpSyn = ` Lists all the AWS public certificates that are registered with Vault. diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index e43c0686d3..f6574d004c 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -25,9 +25,9 @@ func pathConfigClient(b *backend) *framework.Path { Callbacks: map[logical.Operation]framework.OperationFunc{ logical.CreateOperation: b.pathConfigClientCreateUpdate, + logical.UpdateOperation: b.pathConfigClientCreateUpdate, logical.DeleteOperation: b.pathConfigClientDelete, logical.ReadOperation: b.pathConfigClientRead, - logical.UpdateOperation: b.pathConfigClientCreateUpdate, }, HelpSynopsis: pathConfigClientHelpSyn, @@ -39,9 +39,6 @@ func pathConfigClient(b *backend) *framework.Path { // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) pathConfigClientExistenceCheck( req *logical.Request, data *framework.FieldData) (bool, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - entry, err := clientConfigEntry(req.Storage) if err != nil { return false, err @@ -51,6 +48,9 @@ func (b *backend) pathConfigClientExistenceCheck( // Fetch the client configuration required to access the AWS API. func clientConfigEntry(s logical.Storage) (*clientConfig, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + entry, err := s.Get("config/client") if err != nil { return nil, err @@ -68,9 +68,6 @@ func clientConfigEntry(s logical.Storage) (*clientConfig, error) { func (b *backend) pathConfigClientRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - clientConfig, err := clientConfigEntry(req.Storage) if err != nil { return nil, err @@ -88,6 +85,7 @@ func (b *backend) pathConfigClientRead( func (b *backend) pathConfigClientDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() + defer b.configMutex.Unlock() err := req.Storage.Delete("config/client") if err != nil { @@ -105,9 +103,6 @@ func (b *backend) pathConfigClientDelete( // 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 := clientConfigEntry(req.Storage) if err != nil { return nil, err @@ -139,6 +134,9 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry.SecretKey = data.Get("secret_key").(string) } + b.configMutex.Lock() + defer b.configMutex.Unlock() + entry, err := logical.StorageEntryJSON("config/client", configEntry) if err != nil { return nil, err From ba62ef6a86c6abbfeba667d4e39c963c10ae0784 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 27 Apr 2016 22:32:43 -0400 Subject: [PATCH 50/79] Refactor locks around config tidy endpoints --- .../aws/path_config_tidy_identities.go | 23 +++++++++---------- .../aws/path_config_tidy_roletags.go | 21 ++++++++--------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index f53493a69e..f382fc592f 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -19,7 +19,7 @@ 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 'whitelist/identity/' entries and 'whitelist/identity/' entries.", + Description: "If set to 'true', disables the periodic tidying of the 'whitelist/identity/' entries.", }, }, @@ -38,9 +38,6 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - entry, err := configTidyIdentities(req.Storage) if err != nil { return false, err @@ -49,6 +46,8 @@ func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, d } func configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() entry, err := s.Get("config/tidy/identities") if err != nil { return nil, err @@ -65,8 +64,6 @@ func configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, erro } func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.Lock() - defer b.configMutex.Unlock() configEntry, err := configTidyIdentities(req.Storage) if err != nil { return nil, err @@ -74,12 +71,14 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat 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) @@ -87,6 +86,9 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } + b.configMutex.Lock() + defer b.configMutex.Unlock() + entry, err := logical.StorageEntryJSON("config/tidy/identities", configEntry) if err != nil { return nil, err @@ -100,17 +102,14 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat } func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - clientConfig, err := configTidyIdentities(req.Storage) if err != nil { return nil, err } - if clientConfig == nil { return nil, nil } + return &logical.Response{ Data: structs.New(clientConfig).Map(), }, nil @@ -136,8 +135,8 @@ const pathConfigTidyIdentitiesHelpSyn = ` Configures the periodic tidying operation of the whitelisted identity entries. ` const pathConfigTidyIdentitiesHelpDesc = ` -By default, the expired entries in teb whitelist will be attempted to be removed +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 purge them. -However, there is a safety buffer duration (defaults to 72h), which purges the entries, +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_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index eecb443acc..b708cc78c0 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -19,7 +19,7 @@ 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 'blacklist/roletag/' entries and 'whitelist/identity/' entries.", + Description: "If set to 'true', disables the periodic tidying of the 'blacklist/roletag/' entries.", }, }, @@ -38,9 +38,6 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - entry, err := configTidyRoleTags(req.Storage) if err != nil { return false, err @@ -49,6 +46,9 @@ func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, dat } func configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + entry, err := s.Get("config/tidy/roletags") if err != nil { return nil, err @@ -61,12 +61,11 @@ func configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) if err := entry.DecodeJSON(&result); err != nil { return nil, err } + return &result, nil } func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.Lock() - defer b.configMutex.Unlock() configEntry, err := configTidyRoleTags(req.Storage) if err != nil { return nil, err @@ -87,6 +86,9 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } + b.configMutex.Lock() + defer b.configMutex.Unlock() + entry, err := logical.StorageEntryJSON("config/tidy/roletags", configEntry) if err != nil { return nil, err @@ -100,17 +102,14 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data } func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - clientConfig, err := configTidyRoleTags(req.Storage) if err != nil { return nil, err } - if clientConfig == nil { return nil, nil } + return &logical.Response{ Data: structs.New(clientConfig).Map(), }, nil @@ -138,6 +137,6 @@ Configures the periodic tidying operation of the blacklisted role tag entries. const pathConfigTidyRoleTagsHelpDesc = ` 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 purge them. -However, there is a safety buffer duration (defaults to 72h), which purges the entries, +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. ` From 05c10dad94255a143c9470f0a778c3c00cfbe6f5 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 28 Apr 2016 00:35:49 -0400 Subject: [PATCH 51/79] minor updates --- builtin/credential/aws/backend.go | 4 +- builtin/credential/aws/client.go | 2 +- .../credential/aws/path_blacklist_roletag.go | 8 +- .../credential/aws/path_config_certificate.go | 8 +- builtin/credential/aws/path_config_client.go | 11 +- .../aws/path_config_tidy_identities.go | 14 +-- .../aws/path_config_tidy_roletags.go | 14 +-- builtin/credential/aws/path_image.go | 22 ++-- builtin/credential/aws/path_image_tag.go | 100 ++++++++++-------- builtin/credential/aws/path_login.go | 68 ++++++++---- .../credential/aws/path_whitelist_identity.go | 8 +- 11 files changed, 141 insertions(+), 118 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index e38401ef61..3d416f04bd 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -96,7 +96,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { // safety_buffer defaults to 72h safety_buffer := 259200 - tidyBlacklistConfigEntry, err := configTidyRoleTags(req.Storage) + tidyBlacklistConfigEntry, err := b.configTidyRoleTags(req.Storage) if err != nil { return err } @@ -117,7 +117,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { // reset the safety_buffer to 72h safety_buffer = 259200 - tidyWhitelistConfigEntry, err := configTidyIdentities(req.Storage) + tidyWhitelistConfigEntry, err := b.configTidyIdentities(req.Storage) if err != nil { return err } diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 65ed6958af..92e82f3065 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -22,7 +22,7 @@ import ( // * Instance metadata role func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config, error) { // Read the configured secret key and access key - config, err := clientConfigEntry(s) + config, err := b.clientConfigEntry(s) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 63a91c6165..a7ca106452 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -91,11 +91,7 @@ func (b *backend) pathBlacklistRoleTagDelete( return logical.ErrorResponse("missing role_tag"), nil } - err := req.Storage.Delete("blacklist/roletag/" + base64.StdEncoding.EncodeToString([]byte(tag))) - if err != nil { - return nil, err - } - return nil, 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. @@ -225,7 +221,7 @@ Blacklist a role tag so that it cannot be used by an EC2 instance to perform log in the future. 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 +By default, a cron task will periodically looks for expired entries in the blacklist and delete 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. diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 446803b77b..8f3eb9f80a 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -193,15 +193,13 @@ func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) 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 } - err := req.Storage.Delete("config/certificate/" + certName) - if err != nil { - return nil, err - } - return nil, nil + + return nil, req.Storage.Delete("config/certificate/" + certName) } // pathConfigCertificateRead is used to view the configured AWS Public Key that is diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index f6574d004c..3e282848a5 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -39,7 +39,7 @@ func pathConfigClient(b *backend) *framework.Path { // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) pathConfigClientExistenceCheck( req *logical.Request, data *framework.FieldData) (bool, error) { - entry, err := clientConfigEntry(req.Storage) + entry, err := b.clientConfigEntry(req.Storage) if err != nil { return false, err } @@ -47,7 +47,7 @@ func (b *backend) pathConfigClientExistenceCheck( } // Fetch the client configuration required to access the AWS API. -func clientConfigEntry(s logical.Storage) (*clientConfig, error) { +func (b *backend) clientConfigEntry(s logical.Storage) (*clientConfig, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() @@ -68,7 +68,7 @@ func clientConfigEntry(s logical.Storage) (*clientConfig, error) { func (b *backend) pathConfigClientRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - clientConfig, err := clientConfigEntry(req.Storage) + clientConfig, err := b.clientConfigEntry(req.Storage) if err != nil { return nil, err } @@ -87,8 +87,7 @@ func (b *backend) pathConfigClientDelete( b.configMutex.Lock() defer b.configMutex.Unlock() - err := req.Storage.Delete("config/client") - if err != nil { + if err := req.Storage.Delete("config/client"); err != nil { b.configMutex.Unlock() return nil, err } @@ -103,7 +102,7 @@ func (b *backend) pathConfigClientDelete( // that can be used to interact with AWS EC2 API. func (b *backend) pathConfigClientCreateUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - configEntry, err := clientConfigEntry(req.Storage) + configEntry, err := b.clientConfigEntry(req.Storage) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index f382fc592f..19d7e64c01 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -38,14 +38,14 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - entry, err := configTidyIdentities(req.Storage) + entry, err := b.configTidyIdentities(req.Storage) if err != nil { return false, err } return entry != nil, nil } -func configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { +func (b *backend) configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() entry, err := s.Get("config/tidy/identities") @@ -64,7 +64,7 @@ func configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, erro } func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - configEntry, err := configTidyIdentities(req.Storage) + configEntry, err := b.configTidyIdentities(req.Storage) if err != nil { return nil, err } @@ -102,7 +102,7 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat } func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - clientConfig, err := configTidyIdentities(req.Storage) + clientConfig, err := b.configTidyIdentities(req.Storage) if err != nil { return nil, err } @@ -119,11 +119,7 @@ func (b *backend) pathConfigTidyIdentitiesDelete(req *logical.Request, data *fra b.configMutex.Lock() defer b.configMutex.Unlock() - if err := req.Storage.Delete("config/tidy/identities"); err != nil { - return nil, err - } - - return nil, nil + return nil, req.Storage.Delete("config/tidy/identities") } type tidyWhitelistIdentityConfig struct { diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index b708cc78c0..1321500380 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -38,14 +38,14 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - entry, err := configTidyRoleTags(req.Storage) + entry, err := b.configTidyRoleTags(req.Storage) if err != nil { return false, err } return entry != nil, nil } -func configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { +func (b *backend) configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() @@ -66,7 +66,7 @@ func configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) } func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - configEntry, err := configTidyRoleTags(req.Storage) + configEntry, err := b.configTidyRoleTags(req.Storage) if err != nil { return nil, err } @@ -102,7 +102,7 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data } func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - clientConfig, err := configTidyRoleTags(req.Storage) + clientConfig, err := b.configTidyRoleTags(req.Storage) if err != nil { return nil, err } @@ -119,11 +119,7 @@ func (b *backend) pathConfigTidyRoleTagsDelete(req *logical.Request, data *frame b.configMutex.Lock() defer b.configMutex.Unlock() - if err := req.Storage.Delete("config/tidy/roletags"); err != nil { - return nil, err - } - - return nil, nil + return nil, req.Storage.Delete("config/tidy/roletags") } type tidyBlacklistRoleTagConfig struct { diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go index fdfa054eab..3198d929a5 100644 --- a/builtin/credential/aws/path_image.go +++ b/builtin/credential/aws/path_image.go @@ -24,7 +24,7 @@ func pathImage(b *backend) *framework.Path { "role_tag": &framework.FieldSchema{ Type: framework.TypeString, Default: "", - Description: "If set, enables the RoleTag for this AMI. The value set for this field should be the 'key' of the tag on the EC2 instance using the RoleTag. Defaults to empty string.", + Description: "If set, enables the RoleTag for this AMI. 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 'image//roletag' endpoint. Defaults to empty string.", }, "max_ttl": &framework.FieldSchema{ @@ -111,11 +111,7 @@ func awsImage(s logical.Storage, amiID string) (*awsImageEntry, error) { // pathImageDelete is used to delete the information registered for a given AMI ID. func (b *backend) pathImageDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - err := req.Storage.Delete("image/" + strings.ToLower(data.Get("ami_id").(string))) - if err != nil { - return nil, err - } - return nil, nil + return nil, req.Storage.Delete("image/" + strings.ToLower(data.Get("ami_id").(string))) } // pathImageList is used to list all the AMI IDs registered with Vault. @@ -255,15 +251,17 @@ be registered with Vault. After the authentication of the instance, the authorization for the instance to access Vault's resources is determined by the policies that are associated to the AMI through this endpoint. -In case the AMI is shared by many instances, then a role tag can be created -through the endpoint 'image//tag'. This tag needs to be applied on the -instance before it attempts to login to Vault. The policies on the tag should -be a subset of policies that are associated to the AMI in this endpoint. In -order to enable login using tags, RoleTag needs to be enabled in this endpoint. +When the instances share an AMI and when only a subset of policies on the AMI +are supposed to be applicable for any instance, then 'role_tag' option on the AMI +can be enabled to create a role via the endpoint 'image//tag'. +This tag then needs to be applied on the instance before it attempts to login +to Vault. The policies on the tag should be a subset of policies that are +associated to the AMI in this endpoint. In order to enable login using tags, +RoleTag needs to be enabled in this endpoint. 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 a upper -limit of the 'max_ttl' value that is applicable to the backend. +limit of the 'max_ttl' value that is applicable to the backend's mount. ` const pathListImagesHelpSyn = ` diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index d4a5cc3c5b..1ab85ea525 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -77,6 +77,7 @@ func (b *backend) pathImageTagUpdate( // Remove all other policies if 'root' is present. policies := policyutil.ParsePolicies(data.Get("policies").(string)) + // This is an optional field. disallowReauthentication := data.Get("disallow_reauthentication").(bool) // Fetch the image entry corresponding to the AMI ID @@ -95,11 +96,11 @@ func (b *backend) pathImageTagUpdate( // There should be a HMAC key present in the image entry if imageEntry.HMACKey == "" { - // Not able to find the HMACKey is an internal error + // Not being able to find the HMACKey is an internal error return nil, fmt.Errorf("failed to find the HMAC key") } - // Create a random nonce + // Create a random nonce. nonce, err := createRoleTagNonce() if err != nil { return nil, err @@ -158,29 +159,36 @@ func createRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (string, error } // Attach version, nonce, policies and maxTTL to the role tag value. - rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) + rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag) if err != nil { return "", err } - return appendHMAC(rTagPlainText, imageEntry) + // Attach HMAC to tag's plaintext and return. + return appendHMAC(rTagPlaintext, imageEntry) } // 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, imageEntry *awsImageEntry) (string, error) { +func appendHMAC(rTagPlaintext string, imageEntry *awsImageEntry) (string, error) { + if rTagPlaintext == "" { + return "", fmt.Errorf("empty role tag plaintext string") + } + if imageEntry == nil { return "", fmt.Errorf("nil image entry") } // Create the HMAC of the value - hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlainText) + hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlaintext) if err != nil { return "", err } // attach the HMAC to the value - rTagValue := fmt.Sprintf("%s:%s", rTagPlainText, hmacB64) + rTagValue := fmt.Sprintf("%s:%s", rTagPlaintext, hmacB64) + + // This limit of 255 is enforced on the EC2 instance. Hence complying to it here. if len(rTagValue) > 255 { return "", fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") } @@ -201,16 +209,17 @@ func verifyRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (bool, error) } // Fetch the plaintext part of role tag - rTagPlainText, err := prepareRoleTagPlaintextValue(rTag) + rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag) if err != nil { return false, err } // Compute the HMAC of the plaintext - hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlainText) + hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlaintext) if err != nil { return false, err } + return subtle.ConstantTimeCompare([]byte(rTag.HMAC), []byte(hmacB64)) == 1, nil } @@ -229,6 +238,7 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { return "", fmt.Errorf("missing ami_id") } + // This avoids an empty policy, ":p=:" in the role tag. if rTag.Policies == nil || len(rTag.Policies) == 0 { rTag.Policies = []string{"default"} } @@ -253,6 +263,7 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { // also verifies the correctness of the parsed role tag. func 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") @@ -263,45 +274,45 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) // Cache the HMAC value. The last item in the collection. rTag.HMAC = tagItems[len(tagItems)-1] - // Delete the HMAC from the list. + // Remove the HMAC from the list. tagItems = tagItems[:len(tagItems)-1] - // Version is the first element. + // Version will be the first element. rTag.Version = tagItems[0] if rTag.Version != roleTagVersion { return nil, fmt.Errorf("invalid role tag version") } - // Nonce is the second element. + // Nonce will be the second element. rTag.Nonce = tagItems[1] - if len(tagItems) > 2 { - // 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, "a="): - rTag.AmiID = strings.TrimPrefix(tagItem, "a=") - 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, "t="): - rTag.MaxTTL, err = time.ParseDuration(strings.TrimPrefix(tagItem, "t=")) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("unrecognized item in tag") + // 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, "a="): + rTag.AmiID = strings.TrimPrefix(tagItem, "a=") + 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, "t="): + rTag.MaxTTL, err = time.ParseDuration(strings.TrimPrefix(tagItem, "t=")) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unrecognized item %s in tag", tagItem) } } + if rTag.AmiID == "" { return nil, fmt.Errorf("missing image ID") } @@ -320,8 +331,9 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) return nil, err } if !verified { - return nil, fmt.Errorf("role tag signature mismatch") + return nil, fmt.Errorf("role tag signature verification failed") } + return rTag, nil } @@ -339,11 +351,11 @@ func createRoleTagHMACBase64(key, value string) (string, error) { // Creates a base64 encoded random nonce. func createRoleTagNonce() (string, error) { - uuidBytes, err := uuid.GenerateRandomBytes(8) - if err != nil { + if uuidBytes, err := uuid.GenerateRandomBytes(8); err != nil { return "", err + } else { + return base64.StdEncoding.EncodeToString(uuidBytes), nil } - return base64.StdEncoding.EncodeToString(uuidBytes), nil } // Struct roleTag represents a role tag in a struc form. @@ -376,14 +388,14 @@ Create a tag for an EC2 instance. ` const pathImageTagDesc = ` -When an AMI is used by more than one EC2 instance, policies to be associated -during login are determined by a particular tag on the instance. This tag -can be created using this endpoint. +When an AMI is used by more than one EC2 instance and there is a need +to apply only a subset of AMI's policies on the instance, create a +role tag using this endpoint and apply it on the instance. A RoleTag setting needs to be enabled in 'image/' endpoint, to be able to create a tag. Also, the policies to be associated with the tag should be a subset of the policies associated with the regisred AMI. This endpoint will return both the 'key' and the 'value' to be set for the -instance tag. +EC2 instance tag. ` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index a939104613..c1d19d8d90 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -68,7 +68,7 @@ func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocum return nil, fmt.Errorf("no instance details found in reservations") } if *status.Reservations[0].Instances[0].InstanceId != identityDoc.InstanceID { - return nil, fmt.Errorf("expected instance ID does not match the instance ID in the instance description") + 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") @@ -77,7 +77,6 @@ func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocum *status.Reservations[0].Instances[0].State.Name != "running" { return nil, fmt.Errorf("instance is not in 'running' state") } - // Validate the instance through InstanceState, InstanceStatus and SystemStatus return status, nil } @@ -107,7 +106,8 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist // 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 is necessary, or the client must durably store the nonce. + // ID from the whitelist is necessary, or the client must durably store + // the nonce. // // If the `allow_instance_migration` property of the registered AMI is // enabled, then the client nonce mismatch is ignored, as long as the @@ -126,8 +126,9 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist } } - // ensure that the 'pendingTime' on the given identity document is not before than the - // 'pendingTime' that was used for previous login. + // Ensure that the 'pendingTime' on the given identity document is not before than 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") } @@ -138,6 +139,7 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist // 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. @@ -153,6 +155,8 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id } // Get the public certificate that is used to verify the signature. + // This returns a slice of certificates containing the default certificate + // and all the registered certificates using 'config/certificate/' endpoint publicCerts, err := b.awsPublicCertificates(s) if err != nil { return nil, err @@ -186,8 +190,9 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id } // pathLoginUpdate is used to create a Vault token by the EC2 instances -// by providing its instance identity document, pkcs7 signature of the document, -// and a client created nonce. +// 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 AMI. func (b *backend) pathLoginUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -206,7 +211,9 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil } - // Validate the instance ID. + // 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) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil @@ -233,7 +240,8 @@ func (b *backend) pathLoginUpdate( 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. + // with which previous login was made. If 'allow_instance_migration' is + // enabled on the registered AMI, client nonce requirement is relaxed. if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry); err != nil { return logical.ErrorResponse(err.Error()), nil } @@ -257,6 +265,10 @@ func (b *backend) pathLoginUpdate( if err != nil { return nil, err } + if resp == nil { + return logical.ErrorResponse("failed to fetch and verify the role tag"), nil + } + policies = resp.Policies rTagMaxTTL = resp.MaxTTL @@ -267,6 +279,7 @@ func (b *backend) pathLoginUpdate( disallowReauthentication = resp.DisallowReauthentication } + // Scope the maxTTL to the value set on the role tag. if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < maxTTL { maxTTL = resp.MaxTTL } @@ -332,7 +345,18 @@ func (b *backend) pathLoginUpdate( // handleRoleTagLogin is used to fetch the role tag of the instance and verifies it to be correct. // Then the policies for the login request will be set off of the role tag, if certain creteria satisfies. func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) { + if identityDoc == nil { + return nil, fmt.Errorf("nil identityDoc") + } + if imageEntry == nil { + return nil, fmt.Errorf("nil imageEntry") + } + if instanceDesc == nil { + return nil, fmt.Errorf("nil instanceDesc") + } + // Input validation 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", imageEntry.RoleTag) @@ -346,6 +370,8 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc } } + // If 'role_tag' is enabled on the AMI, 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", imageEntry.RoleTag) } @@ -398,13 +424,16 @@ func (b *backend) pathLoginRenew( // For now, rTagMaxTTL is cached in internal data during login and used in renewal for // setting the MaxTTL for the stored login identity entry. - // If `instance_id` can be used to fetch the role tag again (through an API), it would be good. - // For accessing the max_ttl, storing the entire identity document is too heavy. + // + // Ideally, the instance ID should be used to query the role tag again. + // For now, we only make an API call during login and not during renewal. + // If there is a need to do make an API call, this should be changed. rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) if err != nil { return nil, err } + // Ensure that image entry is not deleted. imageEntry, err := awsImage(req.Storage, storedIdentity.AmiID) if err != nil { return nil, err @@ -413,6 +442,7 @@ func (b *backend) pathLoginRenew( return logical.ErrorResponse("image entry not found"), nil } + // Re-evaluate the maxTTL bounds. maxTTL := b.System().MaxLeaseTTL() if imageEntry.MaxTTL > time.Duration(0) && imageEntry.MaxTTL < maxTTL { maxTTL = imageEntry.MaxTTL @@ -421,7 +451,7 @@ func (b *backend) pathLoginRenew( maxTTL = rTagMaxTTL } - // Only LastUpdatedTime and ExpirationTime change, none else. + // Only LastUpdatedTime and ExpirationTime change and all other fields remain the same. currentTime := time.Now() storedIdentity.LastUpdatedTime = currentTime storedIdentity.ExpirationTime = currentTime.Add(maxTTL) @@ -453,15 +483,17 @@ Authenticates an EC2 instance with Vault. ` const pathLoginDesc = ` -An EC2 instance is authenticated using the instance identity document, the identity document's -PKCS#7 signature and a client created nonce. This nonce should be unique and should be used by -the instance for all future logins. +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 'allow_instance_migration' option on the +registered AMI 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. -The entries in the whitelist are not automatically deleted. Although, they will have an -expiration time set on the entry. There is a separate endpoint 'tidy/identities', -that needs to be invoked to clean-up all the expired entries in the whitelist. +By default, a cron task will periodically looks for expired entries in the whitelist +and delete 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_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 5066456972..09816bcd79 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -92,11 +92,7 @@ func (b *backend) pathWhitelistIdentityDelete( return logical.ErrorResponse("missing instance_id"), nil } - if err := req.Storage.Delete("whitelist/identity/" + instanceID); err != nil { - return nil, err - } - - return nil, nil + return nil, req.Storage.Delete("whitelist/identity/" + instanceID) } // pathWhitelistIdentityRead is used to view an entry in the identity whitelist given an instance ID. @@ -140,7 +136,7 @@ Each login from an EC2 instance creates/updates an entry in the identity whiteli 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 +By default, a cron task will periodically looks for expired entries in the whitelist and delete 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. From ba88b210e633b387118610eba986491bd96138d8 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 28 Apr 2016 01:01:33 -0400 Subject: [PATCH 52/79] Fix the deadlock issue --- builtin/credential/aws/backend_test.go | 10 ---------- builtin/credential/aws/path_config_client.go | 7 ++++--- builtin/credential/aws/path_config_tidy_identities.go | 8 ++++++-- builtin/credential/aws/path_config_tidy_roletags.go | 9 ++++++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 1661e990d6..0924d291b8 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -467,16 +467,6 @@ func TestBackend_ConfigClient(t *testing.T) { if !exists { t.Fatal("existence check should have returned 'true' for 'config/client'") } - - // test the "config/client" read helper - clientConfig, err := clientConfigEntry(storage) - if err != nil { - t.Fatal(err) - } - if clientConfig.AccessKey != data["access_key"] || - clientConfig.SecretKey != data["secret_key"] { - t.Fatalf("bad: expected: %#v\ngot: %#v\n", data, clientConfig) - } } func TestBackend_pathConfigCertificate(t *testing.T) { diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 3e282848a5..20b2aabd86 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -39,6 +39,8 @@ func pathConfigClient(b *backend) *framework.Path { // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) pathConfigClientExistenceCheck( req *logical.Request, data *framework.FieldData) (bool, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() entry, err := b.clientConfigEntry(req.Storage) if err != nil { return false, err @@ -48,9 +50,6 @@ func (b *backend) pathConfigClientExistenceCheck( // 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() - entry, err := s.Get("config/client") if err != nil { return nil, err @@ -68,6 +67,8 @@ func (b *backend) clientConfigEntry(s logical.Storage) (*clientConfig, error) { func (b *backend) pathConfigClientRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() clientConfig, err := b.clientConfigEntry(req.Storage) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index 19d7e64c01..2b8bc193a3 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -38,6 +38,9 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + entry, err := b.configTidyIdentities(req.Storage) if err != nil { return false, err @@ -46,8 +49,6 @@ func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, d } func (b *backend) configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() entry, err := s.Get("config/tidy/identities") if err != nil { return nil, err @@ -102,6 +103,9 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat } func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + clientConfig, err := b.configTidyIdentities(req.Storage) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index 1321500380..c3b3ec01f6 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -38,6 +38,9 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + entry, err := b.configTidyRoleTags(req.Storage) if err != nil { return false, err @@ -46,9 +49,6 @@ func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, dat } func (b *backend) configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - entry, err := s.Get("config/tidy/roletags") if err != nil { return nil, err @@ -102,6 +102,9 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data } func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.RLock() + defer b.configMutex.RUnlock() + clientConfig, err := b.configTidyRoleTags(req.Storage) if err != nil { return nil, err From 1a3c0a1f3e0671839701608c85a1b58e1f665446 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 28 Apr 2016 10:19:29 -0400 Subject: [PATCH 53/79] Change all time references to UTC --- builtin/credential/aws/backend.go | 4 ++-- builtin/credential/aws/path_blacklist_roletag.go | 2 +- builtin/credential/aws/path_login.go | 4 ++-- builtin/credential/aws/path_tidy_identities.go | 2 +- builtin/credential/aws/path_tidy_roletags.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 3d416f04bd..9ab39200be 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -93,7 +93,7 @@ type backend struct { // 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 { - if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { + if b.nextTidyTime.IsZero() || !time.Now().UTC().Before(b.nextTidyTime) { // safety_buffer defaults to 72h safety_buffer := 259200 tidyBlacklistConfigEntry, err := b.configTidyRoleTags(req.Storage) @@ -137,7 +137,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { } // Update the nextTidyTime - b.nextTidyTime = time.Now().Add(b.tidyCooldownPeriod) + b.nextTidyTime = time.Now().UTC().Add(b.tidyCooldownPeriod) } return nil } diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index a7ca106452..8c358d0730 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -168,7 +168,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( blEntry = &roleTagBlacklistEntry{} } - currentTime := time.Now() + currentTime := time.Now().UTC() // Check if this is creation of entry. if blEntry.CreationTime.IsZero() { diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index c1d19d8d90..232ad4e632 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -286,7 +286,7 @@ func (b *backend) pathLoginUpdate( } // Save the login attempt in the identity whitelist. - currentTime := time.Now() + currentTime := time.Now().UTC() if storedIdentity == nil { // AmiID, ClientNonce and CreationTime of the identity entry, // once set, should never change. @@ -452,7 +452,7 @@ func (b *backend) pathLoginRenew( } // Only LastUpdatedTime and ExpirationTime change and all other fields remain the same. - currentTime := time.Now() + currentTime := time.Now().UTC() storedIdentity.LastUpdatedTime = currentTime storedIdentity.ExpirationTime = currentTime.Add(maxTTL) diff --git a/builtin/credential/aws/path_tidy_identities.go b/builtin/credential/aws/path_tidy_identities.go index 1e2101aa2e..355162373f 100644 --- a/builtin/credential/aws/path_tidy_identities.go +++ b/builtin/credential/aws/path_tidy_identities.go @@ -57,7 +57,7 @@ func tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error { return err } - if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { + 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) } diff --git a/builtin/credential/aws/path_tidy_roletags.go b/builtin/credential/aws/path_tidy_roletags.go index 3bb98bf7fb..1fa3688a3f 100644 --- a/builtin/credential/aws/path_tidy_roletags.go +++ b/builtin/credential/aws/path_tidy_roletags.go @@ -56,7 +56,7 @@ func tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error { return err } - if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { + 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) } From a2c024ff96a52d20c4619abd4870477118c73847 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 28 Apr 2016 11:25:47 -0400 Subject: [PATCH 54/79] Updated docs --- website/source/docs/auth/aws.html.md | 75 +++++++++++++++++----------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 53825c4935..b4b854125d 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -9,7 +9,7 @@ description: |- # 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 +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 @@ -50,10 +50,13 @@ AMI 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 image, but cannot be used to grant additional privileges. +If the "role tag" is enabled on the AMI and the EC2 instance performing login +does not have an expected tag on it, or if the tag on the instance is deleted, +authentication fails. The role tags can be generated at will by an operator with appropriate API -access. They are HMAC-signed by a key stored within the backend, allowing the -backend to verify the authenticity of a found role tag and ensure that it has +access. They are HMAC-signed by a per-AMI 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. @@ -63,10 +66,10 @@ 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 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 +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 @@ -95,7 +98,9 @@ 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). +shut down and allow ASG to start a new one). By default, reauthentication +is enabled in this backend, and can be turned off using 'disallow_reauthentication' +parameter on the registered AMI. 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 @@ -115,19 +120,21 @@ role it plays, the `role_tag` option can be used to provide a tag to set on instances with the given AMI. 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 +instance. The tag holds information that represents a *subset* of privileges that are set on the AMI and are used to further restrict the set of the AMI's privileges for that particular instance. A `role_tag` can be created using `auth/aws/image//roletag` endpoint and is immutable. The information present in the tag is SHA256 hashed and HMAC -protected. The 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 +protected. The per-AMI 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 the `role_tag` option is set on an AMI, failure to provide any role tag at -all results in a login failure. If the role tag has no policy component, the -client will inherit the allowed policies set on the AMI. If the role tag has a +When 'role_tag' option is set on an AMI, 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 removing the tag. If the role tag has no policy component, +the client will inherit the allowed policies set on the AMI. If the role tag has a 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 @@ -178,7 +185,9 @@ 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). +is simply ignored). Note that reauthentication is enabled by default. If only +a single login is desired, `disable_reauthentication` should be set explicitly +on the registered AMI. The `disallow_reauthentication` option is set per-AMI, and can also be specified in a role tag. Since role tags can only restrict behavior, if the @@ -199,14 +208,12 @@ further login requests. ### Expiration Times and Tidying of `blacklist` and `whitelist` Entries -The entries in both identity `whitelist` and role tag `blacklist` are not -deleted automatically, as the amount of time they are required to be valid are -likely to vary with organizational policy. The entries in both of these lists -contain an expiration time which is dynamically determined by three factors: -`max_ttl` set on the AMI, `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 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 AMI, +`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/identities` and `aws/auth/tidy/roletags` are provided to clean up the entries present @@ -214,22 +221,32 @@ 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. -Additionally, the backend performs has a periodic function that does the tidying -of both blacklist role tags and whitelist identities. This periodic tidying is -activated by default and will have a safety buffer of 72 hours. This can be -configured via `config/tidy/roletags` and `config/tidy/identities` +Automatic deletion of expired entired 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/roletags` and `config/tidy/identities` 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 many regions. Users of instances whose -signatures cannott be verified by the default public certificate, can register a +provided with the backend is applicable for many regions. Users of instances whose +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 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 From 0b44a62e8ff50fd4a902fd79d0e1881f9621a9f4 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 28 Apr 2016 11:43:48 -0400 Subject: [PATCH 55/79] Added allow_instance_migration to the role tag --- builtin/credential/aws/path_image_tag.go | 21 +++++++++++++++++++-- website/source/docs/auth/aws.html.md | 15 ++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/builtin/credential/aws/path_image_tag.go b/builtin/credential/aws/path_image_tag.go index 1ab85ea525..dbe88d9112 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_image_tag.go @@ -44,6 +44,12 @@ This is an optional field, but if set, the created tag can only be used by the i Description: "The maximum allowed lease duration.", }, + "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, @@ -80,6 +86,9 @@ func (b *backend) pathImageTagUpdate( // This is an optional field. disallowReauthentication := data.Get("disallow_reauthentication").(bool) + // This is an optional field. + allowInstanceMigration := data.Get("allow_instance_migration").(bool) + // Fetch the image entry corresponding to the AMI ID imageEntry, err := awsImage(req.Storage, amiID) if err != nil { @@ -132,6 +141,7 @@ func (b *backend) pathImageTagUpdate( MaxTTL: maxTTL, InstanceID: instanceID, DisallowReauthentication: disallowReauthentication, + AllowInstanceMigration: allowInstanceMigration, }, imageEntry) if err != nil { return nil, err @@ -244,7 +254,7 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { } // Attach Version, Nonce, AMI ID, Policies, DisallowReauthentication fields. - value := fmt.Sprintf("%s:%s:a=%s:p=%s:d=%s", rTag.Version, rTag.Nonce, rTag.AmiID, strings.Join(rTag.Policies, ","), strconv.FormatBool(rTag.DisallowReauthentication)) + value := fmt.Sprintf("%s:%s:a=%s:p=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.AmiID, strings.Join(rTag.Policies, ","), strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration)) // Attach instance_id if set. if rTag.InstanceID != "" { @@ -303,6 +313,11 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) 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(strings.TrimPrefix(tagItem, "t=")) if err != nil { @@ -368,6 +383,7 @@ type roleTag struct { AmiID string `json:"ami_id" structs:"ami_id" mapstructure:"ami_id"` 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 { @@ -380,7 +396,8 @@ func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { rTag1.AmiID == rTag2.AmiID && rTag1.HMAC == rTag2.HMAC && rTag1.InstanceID == rTag2.InstanceID && - rTag1.DisallowReauthentication == rTag2.DisallowReauthentication + rTag1.DisallowReauthentication == rTag2.DisallowReauthentication && + rTag1.AllowInstanceMigration == rTag2.AllowInstanceMigration } const pathImageTagSyn = ` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index b4b854125d..7bff93b81f 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -174,6 +174,12 @@ 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-AMI, 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 AMI, a value of `true` in the role tag takes +effect; however, if the option is set to `true` on the AMI, 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 @@ -187,7 +193,7 @@ 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, `disable_reauthentication` should be set explicitly -on the registered AMI. +on the registered AMI or on the role tag. The `disallow_reauthentication` option is set per-AMI, and can also be specified in a role tag. Since role tags can only restrict behavior, if the @@ -1003,6 +1009,13 @@ The response will be in JSON. For example: If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/whitelist/identity 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
From fed55cff941f8761f6a9035f7cfcbe816b24de99 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 28 Apr 2016 16:34:35 -0400 Subject: [PATCH 56/79] Ensure that the instance is running during renewal --- builtin/credential/aws/path_login.go | 46 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 232ad4e632..55eb120064 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -40,9 +40,9 @@ func pathLogin(b *backend) *framework.Path { // 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, identityDoc *identityDocument) (*ec2.DescribeInstancesOutput, error) { +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, identityDoc.Region) + ec2Client, err := b.clientEC2(s, region) if err != nil { return nil, err } @@ -52,13 +52,13 @@ func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocum &ec2.Filter{ Name: aws.String("instance-id"), Values: []*string{ - aws.String(identityDoc.InstanceID), + aws.String(instanceID), }, }, }, }) if err != nil { - return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", identityDoc.InstanceID, err) + 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") @@ -67,7 +67,7 @@ func (b *backend) validateInstance(s logical.Storage, identityDoc *identityDocum if len(status.Reservations[0].Instances) == 0 { return nil, fmt.Errorf("no instance details found in reservations") } - if *status.Reservations[0].Instances[0].InstanceId != identityDoc.InstanceID { + 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 { @@ -214,7 +214,7 @@ func (b *backend) pathLoginUpdate( // Validate the instance ID by making a call to AWS EC2 DescribeInstances API // and fetching the instance description. Validation succeeds only if the // instance is in 'running' state. - instanceDesc, err := b.validateInstance(req.Storage, identityDoc) + 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 } @@ -323,6 +323,7 @@ func (b *backend) pathLoginUpdate( Policies: policies, Metadata: map[string]string{ "instance_id": identityDoc.InstanceID, + "region": identityDoc.Region, "role_tag_max_ttl": rTagMaxTTL.String(), "ami_id": identityDoc.AmiID, }, @@ -416,19 +417,23 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc // pathLoginRenew is used to renew an authenticated token. func (b *backend) pathLoginRenew( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - - storedIdentity, err := whitelistIdentityEntry(req.Storage, req.Auth.Metadata["instance_id"]) - if err != nil { - return nil, err + instanceID := req.Auth.Metadata["instance_id"] + if instanceID == "" { + return nil, fmt.Errorf("unable to fetch instance ID from metadata during renewal") } - // For now, rTagMaxTTL is cached in internal data during login and used in renewal for - // setting the MaxTTL for the stored login identity entry. - // - // Ideally, the instance ID should be used to query the role tag again. - // For now, we only make an API call during login and not during renewal. - // If there is a need to do make an API call, this should be changed. - rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) + 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 } @@ -442,6 +447,13 @@ func (b *backend) pathLoginRenew( return logical.ErrorResponse("image entry not found"), nil } + // For now, rTagMaxTTL is cached in internal data during login and used in renewal for + // setting the MaxTTL for the stored login identity entry. + rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) + if err != nil { + return nil, err + } + // Re-evaluate the maxTTL bounds. maxTTL := b.System().MaxLeaseTTL() if imageEntry.MaxTTL > time.Duration(0) && imageEntry.MaxTTL < maxTTL { From 6a68ff45a900352dde2dd5b7417c217c6fe6bfca Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 29 Apr 2016 14:33:08 +0000 Subject: [PATCH 57/79] Add vendored pkcs7 lib --- vendor/github.com/fullsailor/pkcs7/.gitignore | 24 + vendor/github.com/fullsailor/pkcs7/LICENSE | 22 + vendor/github.com/fullsailor/pkcs7/README.md | 7 + vendor/github.com/fullsailor/pkcs7/ber.go | 228 +++++ .../github.com/fullsailor/pkcs7/ber_test.go | 61 ++ vendor/github.com/fullsailor/pkcs7/pkcs7.go | 776 ++++++++++++++++++ .../github.com/fullsailor/pkcs7/pkcs7_test.go | 410 +++++++++ 7 files changed, 1528 insertions(+) create mode 100644 vendor/github.com/fullsailor/pkcs7/.gitignore create mode 100644 vendor/github.com/fullsailor/pkcs7/LICENSE create mode 100644 vendor/github.com/fullsailor/pkcs7/README.md create mode 100644 vendor/github.com/fullsailor/pkcs7/ber.go create mode 100644 vendor/github.com/fullsailor/pkcs7/ber_test.go create mode 100644 vendor/github.com/fullsailor/pkcs7/pkcs7.go create mode 100644 vendor/github.com/fullsailor/pkcs7/pkcs7_test.go 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-----` From 57e6361b24b34755d8d38323dcbb67f6738c8912 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Sat, 30 Apr 2016 03:20:21 -0400 Subject: [PATCH 58/79] Remove unnecessary append call --- builtin/credential/aws/backend.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 9ab39200be..c14e742e9b 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -43,7 +43,7 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { "login", }, }, - Paths: append([]*framework.Path{ + Paths: []*framework.Path{ pathLogin(b), pathImage(b), pathListImages(b), @@ -59,7 +59,7 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { pathWhitelistIdentity(b), pathTidyIdentities(b), pathListWhitelistIdentities(b), - }), + }, } return b.Backend, nil From 7945e4668ae0d734957516788e872d3b5c849f9f Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 2 May 2016 17:21:52 -0400 Subject: [PATCH 59/79] Allow custom endpoint URLs to be supplied to make EC2 API calls --- builtin/credential/aws/backend_test.go | 31 ++++++++++++++++++++ builtin/credential/aws/client.go | 10 +++++-- builtin/credential/aws/path_config_client.go | 16 ++++++++++ website/source/docs/auth/aws.html.md | 8 +++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 0924d291b8..93feb62e27 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -467,6 +467,37 @@ func TestBackend_ConfigClient(t *testing.T) { 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) { diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 92e82f3065..cdebab534d 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -65,11 +65,17 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config } // Create a config that can be used to make the API calls. - return &aws.Config{ + cfg := &aws.Config{ Credentials: creds, Region: aws.String(region), HTTPClient: cleanhttp.DefaultClient(), - }, nil + } + + // Override the default endpoint with the configured endpoint. + if config.Endpoint != "" { + cfg.Endpoint = aws.String(config.Endpoint) + } + return cfg, nil } // flushCachedEC2Clients deletes all the cached ec2 client objects from the backend. diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 20b2aabd86..4cdf00c668 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -19,6 +19,11 @@ func pathConfigClient(b *backend) *framework.Path { Type: framework.TypeString, Description: "AWS Secret key with permissions to query EC2 instance metadata.", }, + + "endpoint": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The endpoint to be used to make API calls to AWS EC2.", + }, }, ExistenceCheck: b.pathConfigClientExistenceCheck, @@ -134,6 +139,16 @@ func (b *backend) pathConfigClientCreateUpdate( 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) + } + b.configMutex.Lock() defer b.configMutex.Unlock() @@ -158,6 +173,7 @@ func (b *backend) pathConfigClientCreateUpdate( 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 = ` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 7bff93b81f..0d6568ee62 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -383,6 +383,13 @@ The response will be in JSON. For example: AWS Secret key with permissions to query EC2 instance metadata. +
    +
  • + endpoint + optional + URL to override the default generated endpoint for making AWS EC2 API calls. +
  • +
Returns
@@ -419,6 +426,7 @@ The response will be in JSON. For example: "data": { "secret_key": "vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj", "access_key": "VKIAJBRHKH6EVTTNXDHA" + "endpoint" "", }, "lease_duration": 0, "renewable": false, From ef83605f580c1da651d1c07d46036cf77ffbd3d2 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Tue, 3 May 2016 12:14:07 -0400 Subject: [PATCH 60/79] Change image/ to a more flexible /role endpoint --- builtin/credential/aws/backend.go | 73 +++-- builtin/credential/aws/backend_test.go | 211 +++++++----- builtin/credential/aws/client.go | 23 +- .../credential/aws/path_blacklist_roletag.go | 32 +- .../credential/aws/path_config_certificate.go | 6 +- builtin/credential/aws/path_config_client.go | 9 + builtin/credential/aws/path_image.go | 273 ---------------- builtin/credential/aws/path_login.go | 142 +++++---- builtin/credential/aws/path_role.go | 301 ++++++++++++++++++ .../{path_image_tag.go => path_role_tag.go} | 158 ++++----- .../credential/aws/path_tidy_identities.go | 5 +- builtin/credential/aws/path_tidy_roletags.go | 7 +- .../credential/aws/path_whitelist_identity.go | 3 +- website/source/docs/auth/aws.html.md | 206 ++++++------ 14 files changed, 794 insertions(+), 655 deletions(-) delete mode 100644 builtin/credential/aws/path_image.go create mode 100644 builtin/credential/aws/path_role.go rename builtin/credential/aws/{path_image_tag.go => path_role_tag.go} (70%) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index c14e742e9b..3501d099f6 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -18,6 +18,29 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { 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 + + // 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, @@ -45,9 +68,9 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { }, Paths: []*framework.Path{ pathLogin(b), - pathImage(b), - pathListImages(b), - pathImageTag(b), + pathRole(b), + pathListRoles(b), + pathRoleTag(b), pathConfigClient(b), pathConfigCertificate(b), pathConfigTidyRoleTags(b), @@ -65,34 +88,18 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { return b.Backend, nil } -type backend struct { - *framework.Backend - Salt *salt.Salt - - // Lock to make changes to any of the backend's configuration endpoints. - configMutex sync.RWMutex - - // Duration after which the periodic function of the backend needs to be - // executed. - tidyCooldownPeriod time.Duration - - // Var that holds the time at which the periodic func should initiatite - // the tidy operations. - 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. - EC2ClientsMap map[string]*ec2.EC2 -} - // 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 are to cleanup the expired entries of both blacklist -// and whitelist. Tidying is done not once in a minute, but once in an hour. +// 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 72h safety_buffer := 259200 @@ -136,7 +143,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { tidyWhitelistIdentity(req.Storage, safety_buffer) } - // Update the nextTidyTime + // Update the time at which to run the tidy functions again. b.nextTidyTime = time.Now().UTC().Add(b.tidyCooldownPeriod) } return nil @@ -146,13 +153,13 @@ 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 association of AMIs to Vault's policies -through 'image/' endpoint. All the instances that are using this AMI will -get the policies configured on the AMI. +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 policies set on the AMI, 'role_tag' option -can be enabled on the AMI and a tag can be generated using 'image//roletag' -endpoint. This tag represents the subset of capabilities set on the AMI. When the -'role_tag' option is enabled on the AMI, the login operation requires that a respective +If there is need to further restrict the policies set on 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 that is performing the login. ` diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 93feb62e27..0b39f52e85 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -23,22 +23,26 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { t.Fatal(err) } - // create an entry for ami + // create a role entry data := map[string]interface{}{ - "policies": "p,q,r,s", + "policies": "p,q,r,s", + "bound_ami_id": "abcd-123", } - _, err = b.HandleRequest(&logical.Request{ + resp, err := b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123", + 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 image entry - imageEntry, err := awsImage(storage, "abcd-123") + // read the created role entry + roleEntry, err := awsRole(storage, "abcd-123") if err != nil { t.Fatal(err) } @@ -50,14 +54,14 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { } rTag1 := &roleTag{ Version: "v1", - AmiID: "abcd-123", + RoleName: "abcd-123", Nonce: nonce, Policies: []string{"p", "q", "r"}, MaxTTL: 200, } - // create a role tag against the image entry - val, err := createRoleTagValue(rTag1, imageEntry) + // create a role tag against the role entry + val, err := createRoleTagValue(rTag1, roleEntry) if err != nil { t.Fatal(err) } @@ -74,15 +78,15 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { // check the values in parsed role tag if rTag2.Version != "v1" || rTag2.Nonce != nonce || - rTag2.AmiID != "abcd-123" || + rTag2.RoleName != "abcd-123" || rTag2.MaxTTL != 200 || !policyutil.EquivalentPolicies(rTag2.Policies, []string{"p", "q", "r"}) || len(rTag2.HMAC) == 0 { t.Fatalf("parsed role tag is invalid") } - // verify the tag contents using image specific HMAC key - verified, err := verifyRoleTagValue(rTag2, imageEntry) + // verify the tag contents using role specific HMAC key + verified, err := verifyRoleTagValue(rTag2, roleEntry) if err != nil { t.Fatal(err) } @@ -90,36 +94,39 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { t.Fatalf("failed to verify the role tag") } - // register a different ami - _, err = b.HandleRequest(&logical.Request{ + // register a different role + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/ami-6789", + 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) } - // entry for the newly created ami entry - imageEntry2, err := awsImage(storage, "ami-6789") + // get the entry of the newly created role entry + roleEntry2, err := awsRole(storage, "ami-6789") if err != nil { t.Fatal(err) } - // try to verify the tag created with previous image's HMAC key + // try to verify the tag created with previous role's HMAC key // with the newly registered entry's HMAC key - verified, err = verifyRoleTagValue(rTag2, imageEntry2) + verified, err = verifyRoleTagValue(rTag2, roleEntry2) if err != nil { t.Fatal(err) } if verified { - t.Fatalf("verification of role tag should have failed: invalid AMI ID") + 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, imageEntry) + verified, err = verifyRoleTagValue(rTag2, roleEntry) if err != nil { t.Fatal(err) } @@ -135,9 +142,9 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { t.Fatal(err) } rTag := &roleTag{ - Version: "v1", - Nonce: nonce, - AmiID: "abcd-123", + Version: "v1", + Nonce: nonce, + RoleName: "abcd-123", } rTag.Version = "" @@ -158,14 +165,14 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { } rTag.Nonce = nonce - rTag.AmiID = "" + rTag.RoleName = "" // try to create plaintext part of role tag - // without specifying ami_id + // without specifying role_name val, err = prepareRoleTagPlaintextValue(rTag) if err == nil { - t.Fatalf("expected error for missing ami_id") + t.Fatalf("expected error for missing role_name") } - rTag.AmiID = "abcd-123" + rTag.RoleName = "abcd-123" // create the plaintext part of the tag val, err = prepareRoleTagPlaintextValue(rTag) @@ -174,7 +181,7 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { } // verify if it contains known fields - if !strings.Contains(val, "a=") || + if !strings.Contains(val, "r=") || !strings.Contains(val, "p=") || !strings.Contains(val, "d=") || !strings.HasPrefix(val, "v1") { @@ -647,7 +654,7 @@ vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw } } -func TestBackend_pathImage(t *testing.T) { +func TestBackend_pathRole(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -658,45 +665,55 @@ func TestBackend_pathImage(t *testing.T) { } data := map[string]interface{}{ - "policies": "p,q,r,s", - "max_ttl": "2h", + "policies": "p,q,r,s", + "max_ttl": "2h", + "bound_ami_id": "ami-abcd123", } - _, err = b.HandleRequest(&logical.Request{ + resp, err := b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, - Path: "image/ami-abcd123", + 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{ + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, - Path: "image/ami-abcd123", + 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 - _, err = b.HandleRequest(&logical.Request{ + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/ami-abcd123", + 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: "image/ami-abcd123", + Path: "role/ami-abcd123", Storage: storage, }) if err != nil { @@ -706,27 +723,30 @@ func TestBackend_pathImage(t *testing.T) { t.Fatal("bad: expected:true got:false\n") } - // add another entry, to test listing of image entries - _, err = b.HandleRequest(&logical.Request{ + // add another entry, to test listing of role entries + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/ami-abcd456", + 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: "images", + Path: "roles", Storage: storage, }) if err != nil { t.Fatal(err) } if resp == nil || resp.Data == nil || resp.IsError() { - t.Fatalf("failed to list the image entries") + t.Fatalf("failed to list the role entries") } keys := resp.Data["keys"].([]string) if len(keys) != 2 { @@ -735,7 +755,7 @@ func TestBackend_pathImage(t *testing.T) { _, err = b.HandleRequest(&logical.Request{ Operation: logical.DeleteOperation, - Path: "image/ami-abcd123", + Path: "role/ami-abcd123", Storage: storage, }) if err != nil { @@ -744,7 +764,7 @@ func TestBackend_pathImage(t *testing.T) { resp, err = b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, - Path: "image/ami-abcd123", + Path: "role/ami-abcd123", Storage: storage, }) if err != nil { @@ -766,30 +786,34 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { t.Fatal(err) } - // create an entry for an AMI + // create a role data := map[string]interface{}{ - "policies": "p,q,r,s", - "max_ttl": "120s", - "role_tag": "VaultRole", + "policies": "p,q,r,s", + "max_ttl": "120s", + "role_tag": "VaultRole", + "bound_ami_id": "abcd-123", } - _, err = b.HandleRequest(&logical.Request{ + resp, err := b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, - Path: "image/abcd-123", + 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{ + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, - Path: "image/abcd-123", + Path: "role/abcd-123", Storage: storage, }) if resp == nil { - t.Fatalf("expected an image entry for abcd-123") + t.Fatalf("expected an role entry for abcd-123") } // create a role tag @@ -798,7 +822,7 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { } resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123/roletag", + Path: "role/abcd-123/tag", Storage: storage, Data: data2, }) @@ -821,12 +845,12 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { } if rTag.Version != "v1" || !policyutil.EquivalentPolicies(rTag.Policies, []string{"p", "q", "r", "s"}) || - rTag.AmiID != "abcd-123" { + rTag.RoleName != "abcd-123" { t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag) } } -func TestBackend_PathImageTag(t *testing.T) { +func TestBackend_PathRoleTag(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage @@ -836,45 +860,49 @@ func TestBackend_PathImageTag(t *testing.T) { } data := map[string]interface{}{ - "policies": "p,q,r,s", - "max_ttl": "120s", - "role_tag": "VaultRole", + "policies": "p,q,r,s", + "max_ttl": "120s", + "role_tag": "VaultRole", + "bound_ami_id": "abcd-123", } - _, err = b.HandleRequest(&logical.Request{ + resp, err := b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, - Path: "image/abcd-123", + 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{ + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, - Path: "image/abcd-123", + Path: "role/abcd-123", Storage: storage, }) if err != nil { t.Fatal(err) } if resp == nil { - t.Fatalf("failed to find an entry for ami_id: abcd-123") + t.Fatalf("failed to find a role entry for abcd-123") } resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123/roletag", + 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 ami_id: abcd-123") + t.Fatalf("failed to create a tag on role: abcd-123") } if resp.IsError() { - t.Fatalf("failed to create a tag on ami_id: abcd-123: %s\n", resp.Data["error"]) + 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) @@ -891,29 +919,32 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatal(err) } - // create an image entry + // create an role entry data := map[string]interface{}{ - "ami_id": "abcd-123", - "policies": "p,q,r,s", - "role_tag": "VaultRole", + "policies": "p,q,r,s", + "role_tag": "VaultRole", + "bound_ami_id": "abcd-123", } - _, err = b.HandleRequest(&logical.Request{ + resp, err := b.HandleRequest(&logical.Request{ Operation: logical.CreateOperation, - Path: "image/abcd-123", + 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 image registered before + // create a role tag against an role registered before data2 := map[string]interface{}{ "policies": "p,q,r,s", } - resp, err := b.HandleRequest(&logical.Request{ + resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/abcd-123/roletag", + Path: "role/abcd-123/tag", Storage: storage, Data: data2, }) @@ -921,10 +952,10 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { t.Fatal(err) } if resp == nil || resp.Data == nil { - t.Fatalf("failed to create a tag on ami_id: abcd-123") + t.Fatalf("failed to create a tag on role: abcd-123") } if resp.IsError() { - t.Fatalf("failed to create a tag on ami_id: abcd-123: %s\n", resp.Data["error"]) + t.Fatalf("failed to create a tag on role: abcd-123: %s\n", resp.Data["error"]) } tag := resp.Data["tag_value"].(string) if tag == "" { @@ -1002,6 +1033,8 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { t.Fatalf("env var TEST_AWS_EC2_AMI_ID not set") } + roleName := amiID + // create the backend storage := &logical.InmemStorage{} config := logical.TestBackendConfig() @@ -1040,18 +1073,22 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { } } - // create an entry for the AMI. This is required for login to work. + // create an entry for the role. This is required for login to work. data := map[string]interface{}{ - "policies": "root", - "max_ttl": "120s", + "policies": "root", + "max_ttl": "120s", + "bound_ami_id": amiID, } - _, err = b.HandleRequest(&logical.Request{ + resp, err := b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "image/" + amiID, + Path: "role/" + roleName, Storage: storage, Data: data, }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to create role") + } if err != nil { t.Fatal(err) } @@ -1068,7 +1105,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { Storage: storage, Data: loginInput, } - resp, err := b.HandleRequest(loginRequest) + resp, err = b.HandleRequest(loginRequest) if err != nil { t.Fatal(err) } @@ -1111,7 +1148,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { if err != nil { t.Fatal(err) } - if resp == nil || resp.Data == nil || resp.Data["ami_id"] != amiID { + if resp == nil || resp.Data == nil || resp.Data["role_name"] != roleName { t.Fatalf("failed to read whitelist identity") } diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index cdebab534d..c4d23f8885 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -29,7 +29,13 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config var providers []credentials.Provider + endpoint := aws.String("") if config != nil { + // Override the default endpoint with the configured endpoint. + if config.Endpoint != "" { + endpoint = aws.String(config.Endpoint) + } + switch { case config.AccessKey != "" && config.SecretKey != "": // Add the static credential provider @@ -65,25 +71,20 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config } // Create a config that can be used to make the API calls. - cfg := &aws.Config{ + return &aws.Config{ Credentials: creds, Region: aws.String(region), HTTPClient: cleanhttp.DefaultClient(), - } - - // Override the default endpoint with the configured endpoint. - if config.Endpoint != "" { - cfg.Endpoint = aws.String(config.Endpoint) - } - return cfg, nil + 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. // -// Lock should be actuired using b.configMutex.Lock() before calling this method and -// unlocked using b.configMutex.Unlock() after returning. +// 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 { @@ -110,7 +111,7 @@ func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error) return b.EC2ClientsMap[region], nil } - // Fetch the configured credentials + // Create a AWS config object using a chain of providers. awsConfig, err := b.getClientConfig(s, region) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 8c358d0730..13dcda8aa9 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -14,8 +14,9 @@ func pathBlacklistRoleTag(b *backend) *framework.Path { Pattern: "blacklist/roletag/(?P.*)", Fields: map[string]*framework.FieldSchema{ "role_tag": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Role tag that needs be blacklisted. The tag can be supplied as-is. In order to avoid any encoding problems, it can be base64 encoded.", + 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.`, }, }, @@ -52,13 +53,14 @@ func (b *backend) pathBlacklistRoleTagsList( return nil, err } - // Tags are base64 encoded and then indexed to avoid problems + // 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) } } @@ -150,13 +152,13 @@ func (b *backend) pathBlacklistRoleTagUpdate( return logical.ErrorResponse("failed to verify the role tag and parse it"), nil } - // Get the entry for the AMI mentioned in the role tag. - imageEntry, err := awsImage(req.Storage, rTag.AmiID) + // Get the entry for the role mentioned in the role tag. + roleEntry, err := awsRole(req.Storage, rTag.RoleName) if err != nil { return nil, err } - if imageEntry == nil { - return logical.ErrorResponse("image entry not found"), nil + if roleEntry == nil { + return logical.ErrorResponse("role entry not found"), nil } // Check if the role tag is already blacklisted. If yes, update it. @@ -170,7 +172,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( currentTime := time.Now().UTC() - // Check if this is creation of entry. + // 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. @@ -185,13 +187,13 @@ func (b *backend) pathBlacklistRoleTagUpdate( rTag.MaxTTL = b.System().MaxLeaseTTL() } - // The max_ttl value on the role tag is scoped by the value set on the AMI entry. - if imageEntry.MaxTTL > time.Duration(0) && rTag.MaxTTL > imageEntry.MaxTTL { - rTag.MaxTTL = imageEntry.MaxTTL + // The max_ttl value on the role tag is scoped by the value set on the role entry. + if roleEntry.MaxTTL > time.Duration(0) && rTag.MaxTTL > roleEntry.MaxTTL { + rTag.MaxTTL = roleEntry.MaxTTL } // Expiration time is decided by least of the max_ttl values set on: - // role tag, ami entry, backend's mount. + // role tag, role entry, backend's mount. blEntry.ExpirationTime = currentTime.Add(rTag.MaxTTL) entry, err := logical.StorageEntryJSON("blacklist/roletag/"+base64.StdEncoding.EncodeToString([]byte(tag)), blEntry) @@ -217,12 +219,12 @@ Blacklist a previously created role tag. ` const pathBlacklistRoleTagDesc = ` -Blacklist a role tag so that it cannot be used by an EC2 instance to perform logins +Blacklist a role tag so that it cannot be used by any EC2 instance to perform logins in the future. 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 looks for expired entries in the blacklist -and delete them. The duration to periodically run this is one hour by default. +and delete 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. @@ -237,5 +239,5 @@ List the blacklisted role tags. const pathListBlacklistRoleTagsHelpDesc = ` List 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. +to clean-up the blacklist of role tags based on expiration time. ` diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 8f3eb9f80a..78e7be5e7d 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -104,6 +104,7 @@ 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 @@ -132,10 +133,10 @@ func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) // 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 to the slice. +// This method will also append default certificate in the backend, to the slice. func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) { - var certs []*x509.Certificate + // Append the generic certificate provided in the AWS EC2 instance metadata documentation. decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate) if err != nil { @@ -173,6 +174,7 @@ func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() + entry, err := s.Get("config/certificate/" + certName) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 4cdf00c668..5ea5af0000 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -12,16 +12,19 @@ func pathConfigClient(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "access_key": &framework.FieldSchema{ Type: framework.TypeString, + Default: "", Description: "AWS Access key with permissions to query EC2 instance metadata.", }, "secret_key": &framework.FieldSchema{ Type: framework.TypeString, + Default: "", Description: "AWS Secret key with permissions to query EC2 instance metadata.", }, "endpoint": &framework.FieldSchema{ Type: framework.TypeString, + Default: "", Description: "The endpoint to be used to make API calls to AWS EC2.", }, }, @@ -46,6 +49,7 @@ func (b *backend) pathConfigClientExistenceCheck( req *logical.Request, data *framework.FieldData) (bool, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() + entry, err := b.clientConfigEntry(req.Storage) if err != nil { return false, err @@ -152,6 +156,11 @@ func (b *backend) pathConfigClientCreateUpdate( b.configMutex.Lock() defer b.configMutex.Unlock() + // 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 diff --git a/builtin/credential/aws/path_image.go b/builtin/credential/aws/path_image.go deleted file mode 100644 index 3198d929a5..0000000000 --- a/builtin/credential/aws/path_image.go +++ /dev/null @@ -1,273 +0,0 @@ -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 pathImage(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "image/" + framework.GenericNameRegex("ami_id"), - Fields: map[string]*framework.FieldSchema{ - "ami_id": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "AMI ID to be mapped.", - }, - - "role_tag": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "", - Description: "If set, enables the RoleTag for this AMI. 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 'image//roletag' endpoint. Defaults to empty string.", - }, - - "max_ttl": &framework.FieldSchema{ - Type: framework.TypeDurationSecond, - Default: 0, - Description: "The maximum allowed lease duration.", - }, - - "policies": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "default", - Description: "Policies to be associated with the AMI.", - }, - - "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/whitelist/identity/' endpoint.", - }, - }, - - ExistenceCheck: b.pathImageExistenceCheck, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathImageCreateUpdate, - logical.UpdateOperation: b.pathImageCreateUpdate, - logical.ReadOperation: b.pathImageRead, - logical.DeleteOperation: b.pathImageDelete, - }, - - HelpSynopsis: pathImageSyn, - HelpDescription: pathImageDesc, - } -} - -// pathListImages creates a path that enables listing of all the AMIs that are -// registered with Vault. -func pathListImages(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "images/?", - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.pathImageList, - }, - - HelpSynopsis: pathListImagesHelpSyn, - HelpDescription: pathListImagesHelpDesc, - } -} - -// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. -// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. -func (b *backend) pathImageExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - entry, err := awsImage(req.Storage, strings.ToLower(data.Get("ami_id").(string))) - if err != nil { - return false, err - } - return entry != nil, nil -} - -// awsImage is used to get the information registered for the given AMI ID. -func awsImage(s logical.Storage, amiID string) (*awsImageEntry, error) { - entry, err := s.Get("image/" + strings.ToLower(amiID)) - if err != nil { - return nil, err - } - if entry == nil { - return nil, nil - } - - var result awsImageEntry - if err := entry.DecodeJSON(&result); err != nil { - return nil, err - } - return &result, nil -} - -// pathImageDelete is used to delete the information registered for a given AMI ID. -func (b *backend) pathImageDelete( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - return nil, req.Storage.Delete("image/" + strings.ToLower(data.Get("ami_id").(string))) -} - -// pathImageList is used to list all the AMI IDs registered with Vault. -func (b *backend) pathImageList( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - images, err := req.Storage.List("image/") - if err != nil { - return nil, err - } - return logical.ListResponse(images), nil -} - -// pathImageRead is used to view the information registered for a given AMI ID. -func (b *backend) pathImageRead( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - imageEntry, err := awsImage(req.Storage, strings.ToLower(data.Get("ami_id").(string))) - if err != nil { - return nil, err - } - if imageEntry == nil { - return nil, nil - } - - // Prepare the map of all the entries in the imageEntry. - respData := structs.New(imageEntry).Map() - - // HMAC key belonging to the AMI should NOT be exported. - delete(respData, "hmac_key") - - // Display the max_ttl in seconds. - respData["max_ttl"] = imageEntry.MaxTTL / time.Second - - return &logical.Response{ - Data: respData, - }, nil -} - -// pathImageCreateUpdate is used to associate Vault policies to a given AMI ID. -func (b *backend) pathImageCreateUpdate( - req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - - imageID := strings.ToLower(data.Get("ami_id").(string)) - if imageID == "" { - return logical.ErrorResponse("missing ami_id"), nil - } - - imageEntry, err := awsImage(req.Storage, imageID) - if err != nil { - return nil, err - } - if imageEntry == nil { - imageEntry = &awsImageEntry{} - } - - policiesStr, ok := data.GetOk("policies") - if ok { - imageEntry.Policies = policyutil.ParsePolicies(policiesStr.(string)) - } else if req.Operation == logical.CreateOperation { - imageEntry.Policies = []string{"default"} - } - - disallowReauthenticationBool, ok := data.GetOk("disallow_reauthentication") - if ok { - imageEntry.DisallowReauthentication = disallowReauthenticationBool.(bool) - } else if req.Operation == logical.CreateOperation { - imageEntry.DisallowReauthentication = data.Get("disallow_reauthentication").(bool) - } - - allowInstanceMigrationBool, ok := data.GetOk("allow_instance_migration") - if ok { - imageEntry.AllowInstanceMigration = allowInstanceMigrationBool.(bool) - } else if req.Operation == logical.CreateOperation { - imageEntry.AllowInstanceMigration = data.Get("allow_instance_migration").(bool) - } - - maxTTLInt, ok := data.GetOk("max_ttl") - if ok { - maxTTL := time.Duration(maxTTLInt.(int)) * time.Second - systemMaxTTL := b.System().MaxLeaseTTL() - if maxTTL > systemMaxTTL { - return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", maxTTL/time.Second, systemMaxTTL/time.Second)), nil - } - - if maxTTL < time.Duration(0) { - return logical.ErrorResponse("max_ttl cannot be negative"), nil - } - - imageEntry.MaxTTL = maxTTL - } else if req.Operation == logical.CreateOperation { - imageEntry.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second - } - - roleTagStr, ok := data.GetOk("role_tag") - if ok { - imageEntry.RoleTag = roleTagStr.(string) - if len(imageEntry.RoleTag) > 127 { - return logical.ErrorResponse("role tag 'key' is exceeding the limit of 127 characters"), nil - } - } else if req.Operation == logical.CreateOperation { - imageEntry.RoleTag = data.Get("role_tag").(string) - } - - imageEntry.HMACKey, err = uuid.GenerateUUID() - if err != nil { - return nil, fmt.Errorf("failed to generate uuid HMAC key: %v", err) - } - - entry, err := logical.StorageEntryJSON("image/"+imageID, imageEntry) - if err != nil { - return nil, err - } - - if err := req.Storage.Put(entry); err != nil { - return nil, err - } - - return nil, nil -} - -// Struct to hold the information associated with an AMI ID in Vault. -type awsImageEntry struct { - 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 pathImageSyn = ` -Associate an AMI to Vault's policies. -` - -const pathImageDesc = ` -A precondition for login is that the AMI used by the EC2 instance, needs to -be registered with Vault. After the authentication of the instance, the -authorization for the instance to access Vault's resources is determined -by the policies that are associated to the AMI through this endpoint. - -When the instances share an AMI and when only a subset of policies on the AMI -are supposed to be applicable for any instance, then 'role_tag' option on the AMI -can be enabled to create a role via the endpoint 'image//tag'. -This tag then needs to be applied on the instance before it attempts to login -to Vault. The policies on the tag should be a subset of policies that are -associated to the AMI in this endpoint. In order to enable login using tags, -RoleTag needs to be enabled in this endpoint. - -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 a upper -limit of the 'max_ttl' value that is applicable to the backend's mount. -` - -const pathListImagesHelpSyn = ` -Lists all the AMIs that are registered with Vault. -` - -const pathListImagesHelpDesc = ` -AMIs will be listed by their respective AMI ID. -` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 55eb120064..d13d2eedaf 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -18,6 +18,13 @@ func pathLogin(b *backend) *framework.Path { return &framework.Path{ Pattern: "login$", Fields: map[string]*framework.FieldSchema{ + "role_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Name of the pre-registered role in this backend against which the login +is being attempted. If this is not supplied, the name of the AMI ID in +the instance identity document will be assumed to be the name of the role.`, + }, + "pkcs7": &framework.FieldSchema{ Type: framework.TypeString, Description: "PKCS7 signature of the identity document.", @@ -83,7 +90,7 @@ func (b *backend) validateInstance(s logical.Storage, instanceID, region string) // 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, imageEntry *awsImageEntry) error { +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 { @@ -109,7 +116,7 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist // ID from the whitelist is necessary, or the client must durably store // the nonce. // - // If the `allow_instance_migration` property of the registered AMI is + // 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 @@ -118,15 +125,15 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist // This is a weak criterion and hence the `allow_instance_migration` option // should be used with caution. if clientNonce != storedIdentity.ClientNonce { - if !imageEntry.AllowInstanceMigration { + if !roleEntry.AllowInstanceMigration { return fmt.Errorf("client nonce mismatch") } - if imageEntry.AllowInstanceMigration && !givenPendingTime.After(storedPendingTime) { + 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 than the + // 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) { @@ -154,9 +161,9 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id return nil, fmt.Errorf("failed to parse the BER encoded PKCS#7 signature: %s\n", err) } - // Get the public certificate that is used to verify the signature. + // 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 using 'config/certificate/' endpoint + // and all the registered certificates via 'config/certificate/' endpoint publicCerts, err := b.awsPublicCertificates(s) if err != nil { return nil, err @@ -165,7 +172,7 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id return nil, fmt.Errorf("certificates to verify the signature are not found") } - // Before calling Verify() on the PKCS#7 struct, set the certificate to be used + // 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 @@ -192,12 +199,11 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id // 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 AMI. +// 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 } @@ -211,6 +217,13 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil } + roleName := data.Get("role_name").(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. @@ -219,13 +232,13 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil } - // Get the entry for the AMI used by the instance. - imageEntry, err := awsImage(req.Storage, identityDoc.AmiID) + // Get the entry for the role used by the instance. + roleEntry, err := awsRole(req.Storage, roleName) if err != nil { return nil, err } - if imageEntry == nil { - return logical.ErrorResponse("image entry not found"), nil + if roleEntry == nil { + return logical.ErrorResponse("role entry not found"), nil } // Get the entry from the identity whitelist, if there is one. @@ -241,27 +254,28 @@ func (b *backend) pathLoginUpdate( // 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 AMI, client nonce requirement is relaxed. - if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry); err != nil { + // 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 image entry, - // before checking for overriding by the RoleTag + // Load the current values for max TTL and policies from the role entry, + // before checking for overriding max TTL in the role tag. maxTTL := b.System().MaxLeaseTTL() - if imageEntry.MaxTTL > time.Duration(0) && imageEntry.MaxTTL < maxTTL { - maxTTL = imageEntry.MaxTTL + if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < maxTTL { + maxTTL = roleEntry.MaxTTL } - policies := imageEntry.Policies + policies := roleEntry.Policies rTagMaxTTL := time.Duration(0) - disallowReauthentication := imageEntry.DisallowReauthentication + disallowReauthentication := roleEntry.DisallowReauthentication + + if roleEntry.RoleTag != "" { + // Role tag is enabled on the role. - // Role tag is enabled for the AMI. - if imageEntry.RoleTag != "" { // Overwrite the policies with the ones returned from processing the role tag. - resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, imageEntry, instanceDesc) + resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, roleName, roleEntry, instanceDesc) if err != nil { return nil, err } @@ -269,16 +283,23 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("failed to fetch and verify the role tag"), nil } - policies = resp.Policies - rTagMaxTTL = resp.MaxTTL + // 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 imageEntry had disallowReauthentication set to 'true', do not reset it + // 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 maxTTL to the value set on the role tag. if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < maxTTL { maxTTL = resp.MaxTTL @@ -288,10 +309,10 @@ func (b *backend) pathLoginUpdate( // Save the login attempt in the identity whitelist. currentTime := time.Now().UTC() if storedIdentity == nil { - // AmiID, ClientNonce and CreationTime of the identity entry, + // RoleName, ClientNonce and CreationTime of the identity entry, // once set, should never change. storedIdentity = &whitelistIdentity{ - AmiID: identityDoc.AmiID, + RoleName: roleName, ClientNonce: clientNonce, CreationTime: currentTime, } @@ -325,6 +346,7 @@ func (b *backend) pathLoginUpdate( "instance_id": identityDoc.InstanceID, "region": identityDoc.Region, "role_tag_max_ttl": rTagMaxTTL.String(), + "role_name": roleName, "ami_id": identityDoc.AmiID, }, LeaseOptions: logical.LeaseOptions{ @@ -334,7 +356,7 @@ func (b *backend) pathLoginUpdate( }, } - // Enforce our image/role tag maximum TTL + // Cap the TTL value. if maxTTL < resp.Auth.TTL { resp.Auth.TTL = maxTTL } @@ -345,36 +367,38 @@ func (b *backend) pathLoginUpdate( // handleRoleTagLogin is used to fetch the role tag of the instance and verifies it to be correct. // Then the policies for the login request will be set off of the role tag, if certain creteria satisfies. -func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) { +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 imageEntry == nil { - return nil, fmt.Errorf("nil imageEntry") + if roleEntry == nil { + return nil, fmt.Errorf("nil roleEntry") } if instanceDesc == nil { return nil, fmt.Errorf("nil instanceDesc") } - // Input validation is not performed here considering that it would have been done - // in validateInstance method. + // 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", imageEntry.RoleTag) + 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 == imageEntry.RoleTag { + if tagItem.Key != nil && *tagItem.Key == roleEntry.RoleTag { rTagValue = *tagItem.Value break } } - // If 'role_tag' is enabled on the AMI, and if a corresponding tag is not found + // 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", imageEntry.RoleTag) + 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. @@ -383,9 +407,10 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return nil, err } - // Check if the role tag belongs to the AMI ID of the instance. - if rTag.AmiID != identityDoc.AmiID { - return nil, fmt.Errorf("role tag does not belong to the instance's AMI ID.") + // Check if the role name with which this login is being made is same + // as the role name embedded in the tag. + if rTag.RoleName != roleName { + return nil, fmt.Errorf("role_name on the tag is not matching the role_name supplied") } // If instance_id was set on the role tag, check if the same instance is attempting to login. @@ -402,9 +427,9 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc return nil, fmt.Errorf("role tag is blacklisted") } - // Ensure that the policies on the RoleTag is a subset of policies on the image - if !strutil.StrListSubset(imageEntry.Policies, rTag.Policies) { - return nil, fmt.Errorf("policies on the role tag must be subset of policies on the image") + // 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{ @@ -438,17 +463,18 @@ func (b *backend) pathLoginRenew( return nil, err } - // Ensure that image entry is not deleted. - imageEntry, err := awsImage(req.Storage, storedIdentity.AmiID) + // Ensure that role entry is not deleted. + roleEntry, err := awsRole(req.Storage, storedIdentity.RoleName) if err != nil { return nil, err } - if imageEntry == nil { - return logical.ErrorResponse("image entry not found"), nil + if roleEntry == nil { + return logical.ErrorResponse("role entry not found"), nil } - // For now, rTagMaxTTL is cached in internal data during login and used in renewal for - // setting the MaxTTL for the stored login identity entry. + // 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 @@ -456,8 +482,8 @@ func (b *backend) pathLoginRenew( // Re-evaluate the maxTTL bounds. maxTTL := b.System().MaxLeaseTTL() - if imageEntry.MaxTTL > time.Duration(0) && imageEntry.MaxTTL < maxTTL { - maxTTL = imageEntry.MaxTTL + if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < maxTTL { + maxTTL = roleEntry.MaxTTL } if rTagMaxTTL > time.Duration(0) && maxTTL > rTagMaxTTL { maxTTL = rTagMaxTTL @@ -468,7 +494,7 @@ func (b *backend) pathLoginRenew( storedIdentity.LastUpdatedTime = currentTime storedIdentity.ExpirationTime = currentTime.Add(maxTTL) - if err = setWhitelistIdentityEntry(req.Storage, req.Auth.Metadata["instance_id"], storedIdentity); err != nil { + if err = setWhitelistIdentityEntry(req.Storage, instanceID, storedIdentity); err != nil { return nil, err } @@ -498,14 +524,14 @@ 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 'allow_instance_migration' option on the -registered AMI is enabled, in which case client nonce is optional. +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 looks for expired entries in the whitelist -and delete them. The duration to periodically run this is one hour by default. +By default, a cron task will periodically look for expired entries in the whitelist +and delete 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..e309aa0c27 --- /dev/null +++ b/builtin/credential/aws/path_role.go @@ -0,0 +1,301 @@ +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_name"), + Fields: map[string]*framework.FieldSchema{ + "role_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role.", + }, + + "bound_ami_id": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `If set, defines a constraint that the EC2 instances that are trying to +login, should be using the AMI ID specified by this parameter. +`, + }, + + "role_tag": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: "If set, enables the RoleTag for this AMI. 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 empty string.", + }, + + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Default: 0, + Description: "The maximum allowed lease duration.", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "default", + Description: "Policies to be associated with the 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/whitelist/identity/' 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, + } +} + +// pathListRoles creates a path that enables listing of all the AMIs that are +// registered with Vault. +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 := awsRole(req.Storage, strings.ToLower(data.Get("role_name").(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 awsRole(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_name").(string) + if roleName == "" { + return logical.ErrorResponse("missing role_name"), nil + } + + 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) { + 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 := awsRole(req.Storage, strings.ToLower(data.Get("role_name").(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_name").(string)) + if roleName == "" { + return logical.ErrorResponse("missing role_name"), nil + } + + roleEntry, err := awsRole(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) + } + + maxTTLInt, ok := data.GetOk("max_ttl") + if ok { + maxTTL := time.Duration(maxTTLInt.(int)) * time.Second + systemMaxTTL := b.System().MaxLeaseTTL() + if maxTTL > systemMaxTTL { + return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", maxTTL/time.Second, systemMaxTTL/time.Second)), nil + } + + 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("role tag 'key' is exceeding the limit of 127 characters"), nil + } + } else if req.Operation == logical.CreateOperation { + roleEntry.RoleTag = data.Get("role_tag").(string) + } + + roleEntry.HMACKey, err = uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("failed to generate uuid 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 + } + + return nil, 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 a 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_image_tag.go b/builtin/credential/aws/path_role_tag.go similarity index 70% rename from builtin/credential/aws/path_image_tag.go rename to builtin/credential/aws/path_role_tag.go index dbe88d9112..2c71d83786 100644 --- a/builtin/credential/aws/path_image_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -18,13 +18,13 @@ import ( const roleTagVersion = "v1" -func pathImageTag(b *backend) *framework.Path { +func pathRoleTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "image/" + framework.GenericNameRegex("ami_id") + "/roletag$", + Pattern: "role/" + framework.GenericNameRegex("role_name") + "/tag$", Fields: map[string]*framework.FieldSchema{ - "ami_id": &framework.FieldSchema{ + "role_name": &framework.FieldSchema{ Type: framework.TypeString, - Description: "AMI ID to create a tag for.", + Description: "Name of the role.", }, "instance_id": &framework.FieldSchema{ @@ -58,30 +58,36 @@ This is an optional field, but if set, the created tag can only be used by the i }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathImageTagUpdate, + logical.UpdateOperation: b.pathRoleTagUpdate, }, - HelpSynopsis: pathImageTagSyn, - HelpDescription: pathImageTagDesc, + HelpSynopsis: pathRoleTagSyn, + HelpDescription: pathRoleTagDesc, } } -// pathImageTagUpdate is used to create an EC2 instance tag which will +// 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) pathImageTagUpdate( +func (b *backend) pathRoleTagUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - amiID := strings.ToLower(data.Get("ami_id").(string)) - if amiID == "" { - return logical.ErrorResponse("missing ami_id"), nil + roleName := strings.ToLower(data.Get("role_name").(string)) + if roleName == "" { + return logical.ErrorResponse("missing role_name"), nil } // Instance ID is an optional field. instanceID := strings.ToLower(data.Get("instance_id").(string)) - // Parse the given policies into a slice and add 'default' if not provided. - // Remove all other policies if 'root' is present. - policies := policyutil.ParsePolicies(data.Get("policies").(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)) + } // This is an optional field. disallowReauthentication := data.Get("disallow_reauthentication").(bool) @@ -89,22 +95,22 @@ func (b *backend) pathImageTagUpdate( // This is an optional field. allowInstanceMigration := data.Get("allow_instance_migration").(bool) - // Fetch the image entry corresponding to the AMI ID - imageEntry, err := awsImage(req.Storage, amiID) + // Fetch the role entry + roleEntry, err := awsRole(req.Storage, roleName) if err != nil { return nil, err } - if imageEntry == nil { - return logical.ErrorResponse(fmt.Sprintf("entry not found for AMI %s", amiID)), nil + 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 imageEntry.RoleTag == "" { - return logical.ErrorResponse("tag creation is not enabled for this image"), nil + if roleEntry.RoleTag == "" { + return logical.ErrorResponse("tag creation is not enabled for this role"), nil } - // There should be a HMAC key present in the image entry - if imageEntry.HMACKey == "" { + // 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") } @@ -115,7 +121,7 @@ func (b *backend) pathImageTagUpdate( return nil, err } - // max_ttl for the role tag should be less than the max_ttl set on the image. + // 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. @@ -123,9 +129,9 @@ func (b *backend) pathImageTagUpdate( return logical.ErrorResponse(fmt.Sprintf("Registered AMI does not have a max_ttl set. So, the given TTL of %d seconds should be less than the max_ttl set for the corresponding backend mount of %d seconds.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second)), nil } - // If max_ttl is set for the image, check the bounds for tag's max_ttl value using that. - if imageEntry.MaxTTL != time.Duration(0) && maxTTL > imageEntry.MaxTTL { - return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than the max_ttl set for the corresponding image of %d seconds", maxTTL/time.Second, imageEntry.MaxTTL/time.Second)), nil + // 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 { + return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than the max_ttl set for the corresponding role of %d seconds", maxTTL/time.Second, roleEntry.MaxTTL/time.Second)), nil } if maxTTL < time.Duration(0) { @@ -135,14 +141,14 @@ func (b *backend) pathImageTagUpdate( // Create a role tag out of all the information provided. rTagValue, err := createRoleTagValue(&roleTag{ Version: roleTagVersion, - AmiID: amiID, + RoleName: roleName, Nonce: nonce, Policies: policies, MaxTTL: maxTTL, InstanceID: instanceID, DisallowReauthentication: disallowReauthentication, AllowInstanceMigration: allowInstanceMigration, - }, imageEntry) + }, roleEntry) if err != nil { return nil, err } @@ -151,7 +157,7 @@ func (b *backend) pathImageTagUpdate( // This key value pair should be set on the EC2 instance. return &logical.Response{ Data: map[string]interface{}{ - "tag_key": imageEntry.RoleTag, + "tag_key": roleEntry.RoleTag, "tag_value": rTagValue, }, }, nil @@ -159,13 +165,13 @@ func (b *backend) pathImageTagUpdate( // 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, imageEntry *awsImageEntry) (string, error) { +func createRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (string, error) { if rTag == nil { return "", fmt.Errorf("nil role tag") } - if imageEntry == nil { - return "", fmt.Errorf("nil image entry") + if roleEntry == nil { + return "", fmt.Errorf("nil role entry") } // Attach version, nonce, policies and maxTTL to the role tag value. @@ -175,22 +181,22 @@ func createRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (string, error } // Attach HMAC to tag's plaintext and return. - return appendHMAC(rTagPlaintext, imageEntry) + 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, imageEntry *awsImageEntry) (string, error) { +func appendHMAC(rTagPlaintext string, roleEntry *awsRoleEntry) (string, error) { if rTagPlaintext == "" { return "", fmt.Errorf("empty role tag plaintext string") } - if imageEntry == nil { - return "", fmt.Errorf("nil image entry") + if roleEntry == nil { + return "", fmt.Errorf("nil role entry") } // Create the HMAC of the value - hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlaintext) + hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext) if err != nil { return "", err } @@ -198,7 +204,7 @@ func appendHMAC(rTagPlaintext string, imageEntry *awsImageEntry) (string, error) // 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 it here. + // 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") } @@ -206,16 +212,15 @@ func appendHMAC(rTagPlaintext string, imageEntry *awsImageEntry) (string, error) return rTagValue, nil } -// verifyRoleTagValue rebuilds the role tag value without the HMAC, -// computes the HMAC from it using the backend specific key and -// compares it with the received HMAC. -func verifyRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (bool, error) { +// 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 imageEntry == nil { - return false, fmt.Errorf("nil image entry") + if roleEntry == nil { + return false, fmt.Errorf("nil role entry") } // Fetch the plaintext part of role tag @@ -225,7 +230,7 @@ func verifyRoleTagValue(rTag *roleTag, imageEntry *awsImageEntry) (bool, error) } // Compute the HMAC of the plaintext - hmacB64, err := createRoleTagHMACBase64(imageEntry.HMACKey, rTagPlaintext) + hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext) if err != nil { return false, err } @@ -244,17 +249,18 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { if rTag.Nonce == "" { return "", fmt.Errorf("missing nonce") } - if rTag.AmiID == "" { - return "", fmt.Errorf("missing ami_id") + if rTag.RoleName == "" { + return "", fmt.Errorf("missing role_name") } - // This avoids an empty policy, ":p=:" in the role tag. - if rTag.Policies == nil || len(rTag.Policies) == 0 { - rTag.Policies = []string{"default"} - } + // Attach Version, Nonce, RoleName, DisallowReauthentication and AllowInstanceMigration + // fields to the role tag. + value := fmt.Sprintf("%s:%s:r=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.RoleName, strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration)) - // Attach Version, Nonce, AMI ID, Policies, DisallowReauthentication fields. - value := fmt.Sprintf("%s:%s:a=%s:p=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.AmiID, strings.Join(rTag.Policies, ","), 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 != "" { @@ -304,8 +310,8 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) switch { case strings.Contains(tagItem, "i="): rTag.InstanceID = strings.TrimPrefix(tagItem, "i=") - case strings.Contains(tagItem, "a="): - rTag.AmiID = strings.TrimPrefix(tagItem, "a=") + case strings.Contains(tagItem, "r="): + rTag.RoleName = strings.TrimPrefix(tagItem, "r=") case strings.Contains(tagItem, "p="): rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",") case strings.Contains(tagItem, "d="): @@ -328,20 +334,20 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) } } - if rTag.AmiID == "" { - return nil, fmt.Errorf("missing image ID") + if rTag.RoleName == "" { + return nil, fmt.Errorf("missing role name") } - imageEntry, err := awsImage(s, rTag.AmiID) + roleEntry, err := awsRole(s, rTag.RoleName) if err != nil { return nil, err } - if imageEntry == nil { - return nil, fmt.Errorf("entry not found for AMI %s", rTag.AmiID) + if roleEntry == nil { + return nil, fmt.Errorf("entry not found for %s", rTag.RoleName) } // Create a HMAC of the plaintext value of role tag and compare it with the given value. - verified, err := verifyRoleTagValue(rTag, imageEntry) + verified, err := verifyRoleTagValue(rTag, roleEntry) if err != nil { return nil, err } @@ -352,7 +358,7 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) return rTag, nil } -// Creates base64 encoded HMAC using a backend specific key. +// Creates base64 encoded HMAC using a per-role key. func createRoleTagHMACBase64(key, value string) (string, error) { if key == "" { return "", fmt.Errorf("invalid HMAC key") @@ -380,7 +386,7 @@ type roleTag struct { 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"` - AmiID string `json:"ami_id" structs:"ami_id" mapstructure:"ami_id"` + RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` 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"` @@ -393,26 +399,26 @@ func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { rTag1.Nonce == rTag2.Nonce && policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && rTag1.MaxTTL == rTag2.MaxTTL && - rTag1.AmiID == rTag2.AmiID && + rTag1.RoleName == rTag2.RoleName && rTag1.HMAC == rTag2.HMAC && rTag1.InstanceID == rTag2.InstanceID && rTag1.DisallowReauthentication == rTag2.DisallowReauthentication && rTag1.AllowInstanceMigration == rTag2.AllowInstanceMigration } -const pathImageTagSyn = ` -Create a tag for an EC2 instance. +const pathRoleTagSyn = ` +Create a tag on a role in order to be able to further restrict the capabilities of a role. ` -const pathImageTagDesc = ` -When an AMI is used by more than one EC2 instance and there is a need -to apply only a subset of AMI's policies on the instance, create a -role tag using this endpoint and apply it on the instance. +const pathRoleTagDesc = ` +If there are needs to apply only a subset of role's capabilities on the instance, +create a role tag using this endpoint and attach the tag on the instance before +performing login. -A RoleTag setting needs to be enabled in 'image/' endpoint, to be able -to create a tag. Also, the policies to be associated with the tag should be -a subset of the policies associated with the regisred AMI. +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' to be set for the -EC2 instance tag. +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_tidy_identities.go b/builtin/credential/aws/path_tidy_identities.go index 355162373f..ac1a1dcd55 100644 --- a/builtin/credential/aws/path_tidy_identities.go +++ b/builtin/credential/aws/path_tidy_identities.go @@ -74,16 +74,15 @@ func (b *backend) pathTidyIdentitiesUpdate( } const pathTidyIdentitiesSyn = ` -Clean-up the whitelisted instance identity entries. +Clean-up the whitelist instance identity entries. ` const pathTidyIdentitiesDesc = ` When an instance identity is whitelisted, the expiration time of the whitelist -entry is set based on the least 'max_ttl' value set on: AMI entry, the role tag +entry is set based on the least '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_roletags.go b/builtin/credential/aws/path_tidy_roletags.go index 1fa3688a3f..1e12f7a03e 100644 --- a/builtin/credential/aws/path_tidy_roletags.go +++ b/builtin/credential/aws/path_tidy_roletags.go @@ -73,16 +73,15 @@ func (b *backend) pathTidyRoleTagsUpdate( } const pathTidyRoleTagsSyn = ` -Clean-up the blacklisted role tag entries. +Clean-up the blacklist role tag entries. ` const pathTidyRoleTagsDesc = ` When a role tag is blacklisted, the expiration time of the blacklist entry is -set based on the least 'max_ttl' value set on: AMI entry, the role tag and the +set based on the least '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. - +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_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 09816bcd79..2c8ab1a128 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -86,7 +86,6 @@ func setWhitelistIdentityEntry(s logical.Storage, instanceID string, identity *w // pathWhitelistIdentityDelete is used to delete an entry from the identity whitelist given an instance ID. func (b *backend) pathWhitelistIdentityDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - instanceID := data.Get("instance_id").(string) if instanceID == "" { return logical.ErrorResponse("missing instance_id"), nil @@ -118,7 +117,7 @@ func (b *backend) pathWhitelistIdentityRead( // Struct to represent each item in the identity whitelist. type whitelistIdentity struct { - AmiID string `json:"ami_id" structs:"ami_id" mapstructure:"ami_id"` + RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` 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"` diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 0d6568ee62..bc68f282af 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -10,7 +10,7 @@ description: |- 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 +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 @@ -22,7 +22,7 @@ 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 +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 @@ -40,22 +40,26 @@ security, as detailed later in this documentation. ## Authorization Workflow -The basic mechanism of operation is per-AMI. AMI IDs are registered in the -backend and associated with various optional restrictions, such as the set of -allowed policies and max TTLs on the generated tokens. +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 bound against AMI +ID. The roles with this bound can only be used to login by the instances +that are running on the specified 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 -AMI entry in the backend can also be associated with a "role tag". These tags +role entry in the backend can also be associated with a "role tag". 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 image, but cannot be used to grant additional privileges. -If the "role tag" is enabled on the AMI and the EC2 instance performing login -does not have an expected tag on it, or if the tag on the instance is deleted, -authentication fails. +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-AMI key stored within the backend, allowing +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. @@ -74,11 +78,13 @@ 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. However, -this has consequences for token rotation, as it means that once a token has -expired, subsequent authentication attempts would fail. +authentication attempt for an instance ID contained in the whitelist, using the +'disallow_reauthentication' option on the role. 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. -The backend addresses this problem by sharing the responsibility with clients. 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. @@ -98,9 +104,7 @@ 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). By default, reauthentication -is enabled in this backend, and can be turned off using 'disallow_reauthentication' -parameter on the registered AMI. +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 @@ -117,28 +121,28 @@ access. 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 with the given AMI. When this option is set, during login, along with +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 AMI and are used to further restrict the set of the AMI's +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/image//roletag` endpoint +A `role_tag` can be created using `auth/aws/role//tag` endpoint and is immutable. The information present in the tag is SHA256 hashed and HMAC -protected. The per-AMI key to HMAC is only maintained in the backend. This prevents +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 set on an AMI, the instances are required to have a +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 removing the tag. If the role tag has no policy component, -the client will inherit the allowed policies set on the AMI. If the role tag has a -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). +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. @@ -159,7 +163,7 @@ 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-AMI. When this option is enabled, if the client nonce +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 @@ -174,15 +178,15 @@ 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-AMI, and can also be +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 AMI, a value of `true` in the role tag takes -effect; however, if the option is set to `true` on the AMI, a value set in 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 +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 @@ -193,41 +197,41 @@ 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, `disable_reauthentication` should be set explicitly -on the registered AMI or on the role tag. +on the role or on the role tag. -The `disallow_reauthentication` option is set per-AMI, and can also be +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 AMI, a value of `true` in the role tag takes -effect; however, if the option is set to `true` on the AMI, a value set in 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 AMI, but the backend has no control over which -instances using that AMI should have any particular role tag; that is purely up -to the operator. Although role tags are only restrictive, 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/blacklist/roletag/`. Note that this will -not invalidate the tokens that were already issued; this only blocks any -further login requests. +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/blacklist/roletag/`. Note that this will not invalidate the +tokens that were already issued; this only blocks any further login requests. ### 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 AMI, +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/identities` and -`aws/auth/tidy/roletags` 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. +The endpoints `aws/auth/tidy/identities` and `aws/auth/tidy/roletags` 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 entired is performed by the periodic function +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 @@ -247,7 +251,7 @@ via the `auth/aws/config/certificate/` endpoint. ### Dangling Tokens -An instance, after authenticating itself with the backend gets a Vault token. +An EC2 instance, after authenticating itself with the backend gets a Vault token. After that, if the instance terminates or goes down for any reason, the backend will not be aware of such events. The token issued will still be valid, until it expires. The token will likely be expired sooner than its lifetime when the @@ -266,23 +270,22 @@ $ 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. In addition, the `AWS_REGION` -environment variable will be honored if available. +IAM role-provided credentials if available. ``` $ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA ``` -#### Configure the policies on the AMI. +#### Configure the policies on the role. ``` -$ vault write auth/aws/image/ami-fce3c696 policies=prod,dev max_ttl=500h +$ vault write auth/aws/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h ``` #### Perform the login operation ``` -$ vault write auth/aws/login pkcs7=MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA nonce=vault-client-nonce +$ vault write auth/aws/login role_name=dev-role pkcs7=MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA nonce=vault-client-nonce ``` @@ -300,16 +303,16 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/sys/auth/aws" -d ' 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 AMI. +#### Configure the policies on the role. ``` -curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/image/ami-fce3c696" -d '{"policies":"prod,dev","max_ttl":"500h"}' +curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev-role -d '{"bound_ami_id":"ami-fce3c696","policies":"prod,dev","max_ttl":"500h"}' ``` #### Perform the login operation ``` -curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"vault-client-nonce"}' +curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role_name":"dev-role","pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"vault-client-nonce"}' ``` @@ -323,6 +326,8 @@ The response will be in JSON. For example: "metadata": { "role_tag_max_ttl": "0", "instance_id": "i-de0f1344" + "ami_id": "ami-fce3c696" + "role_name": "dev-prod" }, "policies": [ "default", @@ -782,32 +787,39 @@ The response will be in JSON. For example: -### /auth/aws/image/ +### /auth/aws/role/ #### POST
Description
- Registers an AMI ID in the backend. Only those instances which are using the AMIs registered using this endpoint, - will be able to perform login operation. If each EC2 instance is using unique AMI ID, then all those AMI IDs should - be registered beforehand. In case the same AMI is shared among many EC2 instances, then that AMI should be registered - using this endpoint with the option `role_tag` (refer API section), then a `roletag` should be created using - `auth/aws/image//roletag` endpoint, and this tag should be attached to the EC2 instance before the login operation - is performed. + 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 that are 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, not necessarily 'bound_ami_id'.
Method
POST
URL
-
`/auth/aws/image/`
+
`/auth/aws/role/`
Parameters
  • - ami_id + role_name required - AMI ID to be mapped. + Name of the role. +
  • +
+
    +
  • + bound_ami_id + required + If set, defines a constraint that the EC2 instances that are trying to login, + should be using the AMI ID specified by this parameter.
    @@ -864,14 +876,14 @@ The response will be in JSON. For example:
    Description
    - Returns the previously registered AMI ID configuration. + Returns the previously registered role configuration.
    Method
    GET
    URL
    -
    `/auth/aws/image/`
    +
    `/auth/aws/role/`
    Parameters
    @@ -886,6 +898,7 @@ The response will be in JSON. For example: "auth": null, "warnings": null, "data": { + "bound_ami_id": "ami-fce36987", "role_tag": "", "policies": [ "default", @@ -910,14 +923,14 @@ The response will be in JSON. For example:
    Description
    - Lists all the AMI IDs that are registered with the backend. + Lists all the roles that are registered with the backend.
    Method
    GET
    URL
    -
    `/auth/aws/images?list=true`
    +
    `/auth/aws/roles?list=true`
    Parameters
    @@ -933,8 +946,8 @@ The response will be in JSON. For example: "warnings": null, "data": { "keys": [ - "ami-fce3c696", - "ami-hei3d687" + "dev-role", + "prod-role" ] }, "lease_duration": 0, @@ -958,7 +971,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws/image/`
    +
    `/auth/aws/role/`
    Parameters
    @@ -971,29 +984,28 @@ The response will be in JSON. For example:
    -### /auth/aws/image//roletag +### /auth/aws/role//tag #### POST
    Description
    - Creates a `roletag` for the AMI_ID. Role tags provide an effective way to restrict the - options that are set on the AMI ID. This is of use when AMI is shared by multiple instances - and there is need to customize the options for specific instances. + Creates a `roletag` on the role. Role tags provide an effective way to restrict the + policies that are set on the role.
    Method
    POST
    URL
    -
    `/auth/aws/image//roletag`
    +
    `/auth/aws/role//tag`
    Parameters
    • - ami_id + role_name required - AMI ID to create a tag for. + Name of the role.
      @@ -1034,7 +1046,7 @@ The response will be in JSON. For example: "auth": null, "warnings": null, "data": { - "tag_value": "v1:09Vp0qGuyB8=:a=ami-fce3c696:p=default,prod:d=false:t=300h0m0s:uPLKCQxqsefRhrp1qmVa1wsQVUXXJG8UZP/pJIdVyOI=", + "tag_value": "v1:09Vp0qGuyB8=:r=dev-role:p=default,prod:d=false:t=300h0m0s:uPLKCQxqsefRhrp1qmVa1wsQVUXXJG8UZP/pJIdVyOI=", "tag_key": "VaultRole" }, "lease_duration": 0, @@ -1052,8 +1064,9 @@ The response will be in JSON. For example:
      Description
      - Login and fetch a token. If the instance metadata signature is valid - along with a few other conditions, a token will be issued. + 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
      @@ -1064,6 +1077,16 @@ The response will be in JSON. For example:
      Parameters
      +
        +
      • + role_name + optional + Name of the role against which the login is being attempted. + If `role_name` is not specified, then the login endpoint assumes that there + is a role by the name matching the AMI ID of the EC2 instance that is trying + to login. If a matching role is not found, login fails. +
      • +
      • pkcs7 @@ -1093,11 +1116,12 @@ The response will be in JSON. For example: "metadata": { "role_tag_max_ttl": "0", "instance_id": "i-de0f1344" + "ami_id": "ami-fce36983" + "role_name": "dev-role" }, "policies": [ "default", "dev", - "prod" ], "accessor": "20b89871-e6f2-1160-fb29-31c2f6d4645e", "client_token": "c9368254-3f21-aded-8a6f-7c818e81b17a" @@ -1320,7 +1344,7 @@ The response will be in JSON. For example: "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", - "ami_id": "ami-fce3c696" + "role_name": "dev-role" }, "lease_duration": 0, "renewable": false, From 9a662ff493e7a702affdc67d56406f4b60a0418e Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 10:40:49 -0400 Subject: [PATCH 61/79] Switch client code to shared awsutil code --- builtin/credential/aws/client.go | 46 +++++++++----------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index c4d23f8885..3178c44ce6 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -4,12 +4,10 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" - "github.com/aws/aws-sdk-go/aws/ec2metadata" "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" ) @@ -21,14 +19,16 @@ import ( // * 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.clientConfigEntry(s) if err != nil { return nil, err } - var providers []credentials.Provider - endpoint := aws.String("") if config != nil { // Override the default endpoint with the configured endpoint. @@ -36,38 +36,18 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config endpoint = aws.String(config.Endpoint) } - switch { - case config.AccessKey != "" && config.SecretKey != "": - // Add the static credential provider - providers = append(providers, &credentials.StaticProvider{ - Value: credentials.Value{ - AccessKeyID: config.AccessKey, - SecretAccessKey: config.SecretKey, - }}) - case config.AccessKey == "" && config.AccessKey == "": - // Attempt to get credentials from the IAM instance role below - default: // Have one or the other but not both and not neither - return nil, fmt.Errorf( - "static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both); configure or remove them at the 'config/client' endpoint") - } + credsConfig.AccessKey = config.AccessKey + credsConfig.SecretKey = config.SecretKey } - // Add the environment credential provider - providers = append(providers, &credentials.EnvProvider{}) + credsConfig.HTTPClient = cleanhttp.DefaultClient() - // Add the instance metadata role provider - // Create the credentials required to access the API. - providers = append(providers, &ec2rolecreds.EC2RoleProvider{ - Client: ec2metadata.New(session.New(&aws.Config{ - Region: aws.String(region), - HTTPClient: cleanhttp.DefaultClient(), - })), - ExpiryWindow: 15, - }) - - creds := credentials.NewChainCredentials(providers) + 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, or instance metadata") + 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. From 3390dca953aa9e99227b7629cdb73277749211e1 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 11:00:41 -0400 Subject: [PATCH 62/79] Make the roletag blacklist the longest duration, not least --- .../credential/aws/path_blacklist_roletag.go | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 13dcda8aa9..cc1c226d22 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -143,7 +143,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( tag = string(tagBytes) } - // Parse and the role tag from string form to a struct form and verify it. + // Parse and verify the role tag from string form to a struct form and verify it. rTag, err := parseAndVerifyRoleTagValue(req.Storage, tag) if err != nil { return nil, err @@ -180,21 +180,17 @@ func (b *backend) pathBlacklistRoleTagUpdate( blEntry.CreationTime = currentTime } - // Decide the expiration time based on the max_ttl values. - - // If max_ttl is not set for the role tag, fall back on the mount's max_ttl. - if rTag.MaxTTL == time.Duration(0) { - rTag.MaxTTL = b.System().MaxLeaseTTL() + // 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() } - // The max_ttl value on the role tag is scoped by the value set on the role entry. - if roleEntry.MaxTTL > time.Duration(0) && rTag.MaxTTL > roleEntry.MaxTTL { - rTag.MaxTTL = roleEntry.MaxTTL - } - - // Expiration time is decided by least of the max_ttl values set on: - // role tag, role entry, backend's mount. - blEntry.ExpirationTime = currentTime.Add(rTag.MaxTTL) + blEntry.ExpirationTime = currentTime.Add(maxDur) entry, err := logical.StorageEntryJSON("blacklist/roletag/"+base64.StdEncoding.EncodeToString([]byte(tag)), blEntry) if err != nil { From 96cebf9cd16405b73650bb13eebca9803b635086 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 11:22:36 -0400 Subject: [PATCH 63/79] Update commenting --- builtin/credential/aws/path_config_certificate.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 78e7be5e7d..c2c2c100f5 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -21,7 +21,10 @@ type dsaSignature struct { // 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) +// 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 @@ -117,7 +120,7 @@ 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; failed to decode certificate") + return nil, fmt.Errorf("invalid certificate; should be one PEM block only") } // Check if the certificate can be parsed. From 1c60388900dc5517ec62148099863b6d6cd03a3e Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 11:36:28 -0400 Subject: [PATCH 64/79] Fall back to non-base64 cert if it can't be decoded (it's checked later anyways) --- builtin/credential/aws/path_config_certificate.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index c2c2c100f5..74b67c1c7c 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -248,13 +248,13 @@ func (b *backend) pathConfigCertificateCreateUpdate( } // Check if the value is provided by the client. - certStrB64, ok := data.GetOk("aws_public_cert") + certStrData, ok := data.GetOk("aws_public_cert") if ok { - certBytes, err := base64.StdEncoding.DecodeString(certStrB64.(string)) - if err != nil { - return nil, err + if certBytes, err := base64.StdEncoding.DecodeString(certStrData.(string)); err == nil { + certEntry.AWSPublicCert = string(certBytes) + } else { + certEntry.AWSPublicCert = certStrData } - certEntry.AWSPublicCert = string(certBytes) } else { // aws_public_cert should be supplied for both create and update operations. // If it is not provided, throw an error. From c4f26c901103764abd421b47c91c870af2bb3e20 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 12:44:40 -0400 Subject: [PATCH 65/79] Update some mutexes in client config --- builtin/credential/aws/path_config_certificate.go | 2 +- builtin/credential/aws/path_config_client.go | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 74b67c1c7c..2460897469 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -253,7 +253,7 @@ func (b *backend) pathConfigCertificateCreateUpdate( if certBytes, err := base64.StdEncoding.DecodeString(certStrData.(string)); err == nil { certEntry.AWSPublicCert = string(certBytes) } else { - certEntry.AWSPublicCert = certStrData + certEntry.AWSPublicCert = certStrData.(string) } } else { // aws_public_cert should be supplied for both create and update operations. diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 5ea5af0000..b73a912fb3 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -98,7 +98,6 @@ func (b *backend) pathConfigClientDelete( defer b.configMutex.Unlock() if err := req.Storage.Delete("config/client"); err != nil { - b.configMutex.Unlock() return nil, err } @@ -112,6 +111,9 @@ func (b *backend) pathConfigClientDelete( // 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.clientConfigEntry(req.Storage) if err != nil { return nil, err @@ -153,9 +155,6 @@ func (b *backend) pathConfigClientCreateUpdate( configEntry.Endpoint = data.Get("endpoint").(string) } - b.configMutex.Lock() - defer b.configMutex.Unlock() - // 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. From 3d20107e548438a6d6cdf98142dfcc4ed2866aad Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 12:53:27 -0400 Subject: [PATCH 66/79] Move some mutexes around --- builtin/credential/aws/path_config_tidy_identities.go | 6 +++--- builtin/credential/aws/path_config_tidy_roletags.go | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index 2b8bc193a3..33822adee2 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -65,6 +65,9 @@ func (b *backend) configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentit } func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + configEntry, err := b.configTidyIdentities(req.Storage) if err != nil { return nil, err @@ -87,9 +90,6 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } - b.configMutex.Lock() - defer b.configMutex.Unlock() - entry, err := logical.StorageEntryJSON("config/tidy/identities", configEntry) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index c3b3ec01f6..0a5a0ccebb 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -16,6 +16,7 @@ func pathConfigTidyRoleTags(b *backend) *framework.Path { Description: `The amount of extra time that must have passed beyond the roletag expiration, before it is removed from the backend storage.`, }, + "disable_periodic_tidy": &framework.FieldSchema{ Type: framework.TypeBool, Default: false, @@ -66,6 +67,9 @@ func (b *backend) configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagCo } func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.configMutex.Lock() + defer b.configMutex.Unlock() + configEntry, err := b.configTidyRoleTags(req.Storage) if err != nil { return nil, err @@ -86,9 +90,6 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } - b.configMutex.Lock() - defer b.configMutex.Unlock() - entry, err := logical.StorageEntryJSON("config/tidy/roletags", configEntry) if err != nil { return nil, err From 68b76b99c80dc1edcce487266f27da5506855180 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 13:34:50 -0400 Subject: [PATCH 67/79] Rename identity whitelist and roletag blacklist api endpoints --- builtin/credential/aws/backend.go | 4 ++-- builtin/credential/aws/path_blacklist_roletag.go | 4 ++-- .../credential/aws/path_config_tidy_identities.go | 14 ++++++++++---- .../credential/aws/path_config_tidy_roletags.go | 14 ++++++++++---- builtin/credential/aws/path_whitelist_identity.go | 4 ++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 3501d099f6..67985fd8ac 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -76,12 +76,12 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { pathConfigTidyRoleTags(b), pathConfigTidyIdentities(b), pathListCertificates(b), - pathBlacklistRoleTag(b), pathListBlacklistRoleTags(b), + pathBlacklistRoleTag(b), pathTidyRoleTags(b), + pathListWhitelistIdentities(b), pathWhitelistIdentity(b), pathTidyIdentities(b), - pathListWhitelistIdentities(b), }, } diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index cc1c226d22..5507282d7d 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -11,7 +11,7 @@ import ( func pathBlacklistRoleTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "blacklist/roletag/(?P.*)", + Pattern: "roletag-blacklist/(?P.*)", Fields: map[string]*framework.FieldSchema{ "role_tag": &framework.FieldSchema{ Type: framework.TypeString, @@ -34,7 +34,7 @@ to avoid any encoding problems, it can be base64 encoded.`, // Path to list all the blacklisted tags. func pathListBlacklistRoleTags(b *backend) *framework.Path { return &framework.Path{ - Pattern: "blacklist/roletags/?", + Pattern: "roletag-blacklist/?", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ListOperation: b.pathBlacklistRoleTagsList, diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index 33822adee2..bd59ddca30 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -1,14 +1,20 @@ 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 pathConfigTidyIdentities(b *backend) *framework.Path { return &framework.Path{ - Pattern: "config/tidy/identities$", + Pattern: fmt.Sprintf("%s$", identityWhitelistConfigPath), Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -49,7 +55,7 @@ func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, d } func (b *backend) configTidyIdentities(s logical.Storage) (*tidyWhitelistIdentityConfig, error) { - entry, err := s.Get("config/tidy/identities") + entry, err := s.Get(identityWhitelistConfigPath) if err != nil { return nil, err } @@ -90,7 +96,7 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } - entry, err := logical.StorageEntryJSON("config/tidy/identities", configEntry) + entry, err := logical.StorageEntryJSON(identityWhitelistConfigPath, configEntry) if err != nil { return nil, err } @@ -123,7 +129,7 @@ func (b *backend) pathConfigTidyIdentitiesDelete(req *logical.Request, data *fra b.configMutex.Lock() defer b.configMutex.Unlock() - return nil, req.Storage.Delete("config/tidy/identities") + return nil, req.Storage.Delete(identityWhitelistConfigPath) } type tidyWhitelistIdentityConfig struct { diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index 0a5a0ccebb..6ea693c884 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -1,14 +1,20 @@ 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 pathConfigTidyRoleTags(b *backend) *framework.Path { return &framework.Path{ - Pattern: "config/tidy/roletags$", + Pattern: fmt.Sprintf("%s$", roletagBlacklistConfigPath), Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -50,7 +56,7 @@ func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, dat } func (b *backend) configTidyRoleTags(s logical.Storage) (*tidyBlacklistRoleTagConfig, error) { - entry, err := s.Get("config/tidy/roletags") + entry, err := s.Get(roletagBlacklistConfigPath) if err != nil { return nil, err } @@ -90,7 +96,7 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data configEntry.DisablePeriodicTidy = data.Get("disable_periodic_tidy").(bool) } - entry, err := logical.StorageEntryJSON("config/tidy/roletags", configEntry) + entry, err := logical.StorageEntryJSON(roletagBlacklistConfigPath, configEntry) if err != nil { return nil, err } @@ -123,7 +129,7 @@ func (b *backend) pathConfigTidyRoleTagsDelete(req *logical.Request, data *frame b.configMutex.Lock() defer b.configMutex.Unlock() - return nil, req.Storage.Delete("config/tidy/roletags") + return nil, req.Storage.Delete(roletagBlacklistConfigPath) } type tidyBlacklistRoleTagConfig struct { diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index 2c8ab1a128..b41c8b1aae 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -10,7 +10,7 @@ import ( func pathWhitelistIdentity(b *backend) *framework.Path { return &framework.Path{ - Pattern: "whitelist/identity/" + framework.GenericNameRegex("instance_id"), + Pattern: "identity-whitelist/" + framework.GenericNameRegex("instance_id"), Fields: map[string]*framework.FieldSchema{ "instance_id": &framework.FieldSchema{ Type: framework.TypeString, @@ -30,7 +30,7 @@ func pathWhitelistIdentity(b *backend) *framework.Path { func pathListWhitelistIdentities(b *backend) *framework.Path { return &framework.Path{ - Pattern: "whitelist/identities/?", + Pattern: "identity-whitelist/?", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ListOperation: b.pathWhitelistIdentitiesList, From c41b024f3688609c24aadc02cc27653b4ad8f2cd Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 14:12:22 -0400 Subject: [PATCH 68/79] More updates to mutexes and adjust blacklisted roletag default safety buffer --- builtin/credential/aws/backend.go | 7 ++++-- .../credential/aws/path_blacklist_roletag.go | 22 ++++++++++++++++--- .../credential/aws/path_config_certificate.go | 21 ++++++++++++++---- builtin/credential/aws/path_config_client.go | 14 +++++++----- .../aws/path_config_tidy_identities.go | 15 +++++++------ .../aws/path_config_tidy_roletags.go | 22 ++++++++++--------- builtin/credential/aws/path_login.go | 2 +- builtin/credential/aws/path_role_tag.go | 8 +++---- 8 files changed, 75 insertions(+), 36 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 67985fd8ac..9f26257909 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -25,6 +25,9 @@ type backend struct { // Lock to make changes to any of the backend's configuration endpoints. configMutex sync.RWMutex + // Lock to make changes to the blacklist entries + blacklistMutex sync.RWMutex + // Duration after which the periodic function of the backend needs to // tidy the blacklist and whitelist entries. tidyCooldownPeriod time.Duration @@ -101,8 +104,8 @@ 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 72h - safety_buffer := 259200 + // safety_buffer defaults to 180 days for roletag blacklist + safety_buffer := 15552000 tidyBlacklistConfigEntry, err := b.configTidyRoleTags(req.Storage) if err != nil { return err diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 5507282d7d..775854eb43 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -48,6 +48,9 @@ func pathListBlacklistRoleTags(b *backend) *framework.Path { // Lists all the blacklisted role tags. func (b *backend) pathBlacklistRoleTagsList( 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 @@ -69,7 +72,14 @@ func (b *backend) pathBlacklistRoleTagsList( // 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 blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { +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 @@ -88,6 +98,9 @@ func blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntr // Deletes an entry from the role tag blacklist for a given tag. func (b *backend) pathBlacklistRoleTagDelete( 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 @@ -106,7 +119,7 @@ func (b *backend) pathBlacklistRoleTagRead( return logical.ErrorResponse("missing role_tag"), nil } - entry, err := blacklistRoleTagEntry(req.Storage, tag) + entry, err := b.blacklistRoleTagEntry(req.Storage, tag) if err != nil { return nil, err } @@ -161,8 +174,11 @@ func (b *backend) pathBlacklistRoleTagUpdate( 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 := blacklistRoleTagEntry(req.Storage, tag) + blEntry, err := b.blacklistRoleTagEntryInternal(req.Storage, tag) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 2460897469..9adaa50c04 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -95,6 +95,7 @@ func (b *backend) pathConfigCertificateExistenceCheck(req *logical.Request, data if certName == "" { return false, fmt.Errorf("missing cert_name") } + entry, err := b.awsPublicCertificateEntry(req.Storage, certName) if err != nil { return false, err @@ -138,6 +139,11 @@ func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) // 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. @@ -155,7 +161,7 @@ func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, // Iterate through each certificate, parse and append it to a slice. for _, cert := range registeredCerts { - certEntry, err := b.awsPublicCertificateEntry(s, cert) + certEntry, err := b.awsPublicCertificateEntryInternal(s, cert) if err != nil { return nil, err } @@ -178,6 +184,11 @@ func (b *backend) awsPublicCertificateEntry(s logical.Storage, certName string) 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 @@ -238,8 +249,11 @@ func (b *backend) pathConfigCertificateCreateUpdate( 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.awsPublicCertificateEntry(req.Storage, certName) + certEntry, err := b.awsPublicCertificateEntryInternal(req.Storage, certName) if err != nil { return nil, err } @@ -275,8 +289,7 @@ func (b *backend) pathConfigCertificateCreateUpdate( return logical.ErrorResponse("invalid certificate; failed to decode and parse certificate"), nil } - b.configMutex.Lock() - defer b.configMutex.Unlock() + // 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 { diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index b73a912fb3..6fe624eb44 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -47,8 +47,6 @@ func pathConfigClient(b *backend) *framework.Path { // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) pathConfigClientExistenceCheck( req *logical.Request, data *framework.FieldData) (bool, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() entry, err := b.clientConfigEntry(req.Storage) if err != nil { @@ -59,6 +57,14 @@ func (b *backend) pathConfigClientExistenceCheck( // 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.clientConfigEntry(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 @@ -76,8 +82,6 @@ func (b *backend) clientConfigEntry(s logical.Storage) (*clientConfig, error) { func (b *backend) pathConfigClientRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() clientConfig, err := b.clientConfigEntry(req.Storage) if err != nil { return nil, err @@ -114,7 +118,7 @@ func (b *backend) pathConfigClientCreateUpdate( b.configMutex.Lock() defer b.configMutex.Unlock() - configEntry, err := b.clientConfigEntry(req.Storage) + configEntry, err := b.clientConfigEntryInternal(req.Storage) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index bd59ddca30..86076d50c6 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -44,9 +44,6 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - entry, err := b.configTidyIdentities(req.Storage) if err != nil { return false, err @@ -55,6 +52,13 @@ func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, d } 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 @@ -74,7 +78,7 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat b.configMutex.Lock() defer b.configMutex.Unlock() - configEntry, err := b.configTidyIdentities(req.Storage) + configEntry, err := b.configTidyIdentitiesInternal(req.Storage) if err != nil { return nil, err } @@ -109,9 +113,6 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat } func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - clientConfig, err := b.configTidyIdentities(req.Storage) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index 6ea693c884..bc02551409 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -18,15 +18,16 @@ func pathConfigTidyRoleTags(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, - Default: 259200, //72h + 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.`, +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 the 'blacklist/roletag/' entries.", + Description: "If set to 'true', disables the periodic tidying of blacklisted entries.", }, }, @@ -45,9 +46,6 @@ expiration, before it is removed from the backend storage.`, } func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - entry, err := b.configTidyRoleTags(req.Storage) if err != nil { return false, err @@ -56,6 +54,13 @@ func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, dat } 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 @@ -76,7 +81,7 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data b.configMutex.Lock() defer b.configMutex.Unlock() - configEntry, err := b.configTidyRoleTags(req.Storage) + configEntry, err := b.configTidyRoleTagsInternal(req.Storage) if err != nil { return nil, err } @@ -109,9 +114,6 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data } func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.configMutex.RLock() - defer b.configMutex.RUnlock() - clientConfig, err := b.configTidyRoleTags(req.Storage) if err != nil { return nil, err diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index d13d2eedaf..42ff38e851 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -419,7 +419,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc } // Check if the role tag is blacklisted. - blacklistEntry, err := blacklistRoleTagEntry(s, rTagValue) + blacklistEntry, err := b.blacklistRoleTagEntry(s, rTagValue) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index 2c71d83786..814d8e41ab 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -30,18 +30,18 @@ func pathRoleTag(b *backend) *framework.Path { "instance_id": &framework.FieldSchema{ Type: framework.TypeString, Description: `Instance ID for which this tag is intended for. -This is an optional field, but if set, the created tag can only be used by the instance with the given ID.`, +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.", + Description: "Policies to be associated with the tag. If set, must be a subset of the role's policies.", }, "max_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Default: 0, - Description: "The maximum allowed lease duration.", + Description: "If set, specifies the maximum allowed token lifetime.", }, "allow_instance_migration": &framework.FieldSchema{ @@ -53,7 +53,7 @@ This is an optional field, but if set, the created tag can only be used by the i "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/whitelist/identity/' endpoint.", + 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.", }, }, From e7c5966da8aacc6366c847fa15b8a84efdbc606a Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 14:28:46 -0400 Subject: [PATCH 69/79] Guard tidy functions --- builtin/credential/aws/backend.go | 8 ++++++-- builtin/credential/aws/path_tidy_identities.go | 12 ++++++++++-- builtin/credential/aws/path_tidy_roletags.go | 12 ++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 9f26257909..74aa1fab31 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -28,6 +28,10 @@ type backend struct { // 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 @@ -122,7 +126,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { } // tidy role tags if explicitly not disabled if !skipBlacklistTidy { - tidyBlacklistRoleTag(req.Storage, safety_buffer) + b.tidyBlacklistRoleTag(req.Storage, safety_buffer) } // reset the safety_buffer to 72h @@ -143,7 +147,7 @@ func (b *backend) periodicFunc(req *logical.Request) error { } // tidy identities if explicitly not disabled if !skipWhitelistTidy { - tidyWhitelistIdentity(req.Storage, safety_buffer) + b.tidyWhitelistIdentity(req.Storage, safety_buffer) } // Update the time at which to run the tidy functions again. diff --git a/builtin/credential/aws/path_tidy_identities.go b/builtin/credential/aws/path_tidy_identities.go index ac1a1dcd55..5f7f935ba4 100644 --- a/builtin/credential/aws/path_tidy_identities.go +++ b/builtin/credential/aws/path_tidy_identities.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "sync/atomic" "time" "github.com/hashicorp/vault/logical" @@ -30,7 +31,14 @@ expiration, before it is removed from the backend storage.`, } // tidyWhitelistIdentity is used to delete entries in the whitelist that are expired. -func tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error { +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/") @@ -70,7 +78,7 @@ func tidyWhitelistIdentity(s logical.Storage, safety_buffer int) error { // pathTidyIdentitiesUpdate is used to delete entries in the whitelist that are expired. func (b *backend) pathTidyIdentitiesUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - return nil, tidyWhitelistIdentity(req.Storage, data.Get("safety_buffer").(int)) + return nil, b.tidyWhitelistIdentity(req.Storage, data.Get("safety_buffer").(int)) } const pathTidyIdentitiesSyn = ` diff --git a/builtin/credential/aws/path_tidy_roletags.go b/builtin/credential/aws/path_tidy_roletags.go index 1e12f7a03e..1df01307fa 100644 --- a/builtin/credential/aws/path_tidy_roletags.go +++ b/builtin/credential/aws/path_tidy_roletags.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "sync/atomic" "time" "github.com/hashicorp/vault/logical" @@ -30,7 +31,14 @@ expiration, before it is removed from the backend storage.`, } // tidyBlacklistRoleTag is used to clean-up the entries in the role tag blacklist. -func tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error { +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 { @@ -69,7 +77,7 @@ func tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) error { // pathTidyRoleTagsUpdate is used to clean-up the entries in the role tag blacklist. func (b *backend) pathTidyRoleTagsUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - return nil, tidyBlacklistRoleTag(req.Storage, data.Get("safety_buffer").(int)) + return nil, b.tidyBlacklistRoleTag(req.Storage, data.Get("safety_buffer").(int)) } const pathTidyRoleTagsSyn = ` From e83dbbe532b3e59639fc3b946891bbfaa471548f Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 14:51:09 -0400 Subject: [PATCH 70/79] Fix HMAC being overwritten. Also some documentation, and add a lock to role operations --- builtin/credential/aws/backend.go | 6 +- .../credential/aws/path_blacklist_roletag.go | 4 +- builtin/credential/aws/path_login.go | 6 +- builtin/credential/aws/path_role.go | 65 +++++++++++++------ builtin/credential/aws/path_role_tag.go | 6 +- 5 files changed, 59 insertions(+), 28 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 74aa1fab31..8ffa208f94 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -25,6 +25,9 @@ type backend struct { // 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 @@ -75,8 +78,9 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { }, Paths: []*framework.Path{ pathLogin(b), - pathRole(b), + pathListRole(b), pathListRoles(b), + pathRole(b), pathRoleTag(b), pathConfigClient(b), pathConfigCertificate(b), diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 775854eb43..4ec853ac13 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -157,7 +157,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( } // Parse and verify the role tag from string form to a struct form and verify it. - rTag, err := parseAndVerifyRoleTagValue(req.Storage, tag) + rTag, err := b.parseAndVerifyRoleTagValue(req.Storage, tag) if err != nil { return nil, err } @@ -166,7 +166,7 @@ func (b *backend) pathBlacklistRoleTagUpdate( } // Get the entry for the role mentioned in the role tag. - roleEntry, err := awsRole(req.Storage, rTag.RoleName) + roleEntry, err := b.awsRole(req.Storage, rTag.RoleName) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 42ff38e851..32adc3256c 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -233,7 +233,7 @@ func (b *backend) pathLoginUpdate( } // Get the entry for the role used by the instance. - roleEntry, err := awsRole(req.Storage, roleName) + roleEntry, err := b.awsRole(req.Storage, roleName) if err != nil { return nil, err } @@ -402,7 +402,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc } // Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC. - rTag, err := parseAndVerifyRoleTagValue(s, rTagValue) + rTag, err := b.parseAndVerifyRoleTagValue(s, rTagValue) if err != nil { return nil, err } @@ -464,7 +464,7 @@ func (b *backend) pathLoginRenew( } // Ensure that role entry is not deleted. - roleEntry, err := awsRole(req.Storage, storedIdentity.RoleName) + roleEntry, err := b.awsRole(req.Storage, storedIdentity.RoleName) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index e309aa0c27..9d1d996c09 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -22,28 +22,26 @@ func pathRole(b *backend) *framework.Path { }, "bound_ami_id": &framework.FieldSchema{ - Type: framework.TypeString, - Description: `If set, defines a constraint that the EC2 instances that are trying to -login, should be using the AMI ID specified by this parameter. -`, + Type: framework.TypeString, + Description: `If set, instances attempting login must be running the given AMI.`, }, "role_tag": &framework.FieldSchema{ Type: framework.TypeString, Default: "", - Description: "If set, enables the RoleTag for this AMI. 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 empty string.", + 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 lease duration.", + Description: "The maximum allowed lifetime for tokens issued due to logins using this role.", }, "policies": &framework.FieldSchema{ Type: framework.TypeString, Default: "default", - Description: "Policies to be associated with the role.", + Description: "Policies to be set on tokens logging in using this role.", }, "allow_instance_migration": &framework.FieldSchema{ @@ -55,7 +53,7 @@ login, should be using the AMI ID specified by this parameter. "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/whitelist/identity/' endpoint.", + 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.", }, }, @@ -73,8 +71,19 @@ login, should be using the AMI ID specified by this parameter. } } -// pathListRoles creates a path that enables listing of all the AMIs that are -// registered with Vault. +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/?", @@ -91,7 +100,7 @@ func pathListRoles(b *backend) *framework.Path { // 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 := awsRole(req.Storage, strings.ToLower(data.Get("role_name").(string))) + entry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role_name").(string))) if err != nil { return false, err } @@ -99,7 +108,14 @@ func (b *backend) pathRoleExistenceCheck(req *logical.Request, data *framework.F } // awsRole is used to get the information registered for the given AMI ID. -func awsRole(s logical.Storage, role string) (*awsRoleEntry, error) { +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 @@ -123,12 +139,18 @@ func (b *backend) pathRoleDelete( return logical.ErrorResponse("missing role_name"), 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 @@ -139,7 +161,7 @@ func (b *backend) pathRoleList( // 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 := awsRole(req.Storage, strings.ToLower(data.Get("role_name").(string))) + roleEntry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role_name").(string))) if err != nil { return nil, err } @@ -170,7 +192,10 @@ func (b *backend) pathRoleCreateUpdate( return logical.ErrorResponse("missing role_name"), nil } - roleEntry, err := awsRole(req.Storage, roleName) + b.roleMutex.Lock() + defer b.roleMutex.Unlock() + + roleEntry, err := b.awsRoleInternal(req.Storage, roleName) if err != nil { return nil, err } @@ -235,15 +260,17 @@ func (b *backend) pathRoleCreateUpdate( // 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("role tag 'key' is exceeding the limit of 127 characters"), nil + 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) } - roleEntry.HMACKey, err = uuid.GenerateUUID() - if err != nil { - return nil, fmt.Errorf("failed to generate uuid HMAC key: %v", err) + 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) @@ -288,7 +315,7 @@ 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 a upper +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. ` diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index 814d8e41ab..76820c3f84 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -96,7 +96,7 @@ func (b *backend) pathRoleTagUpdate( allowInstanceMigration := data.Get("allow_instance_migration").(bool) // Fetch the role entry - roleEntry, err := awsRole(req.Storage, roleName) + roleEntry, err := b.awsRole(req.Storage, roleName) if err != nil { return nil, err } @@ -277,7 +277,7 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { // Parses the tag from string form into a struct form. This method // also verifies the correctness of the parsed role tag. -func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { +func (b *backend) parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) { tagItems := strings.Split(tag, ":") // Tag must contain version, nonce, policies and HMAC @@ -338,7 +338,7 @@ func parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*roleTag, error) return nil, fmt.Errorf("missing role name") } - roleEntry, err := awsRole(s, rTag.RoleName) + roleEntry, err := b.awsRole(s, rTag.RoleName) if err != nil { return nil, err } From a5cae16b5ffab327f70de2882d584d055ed52853 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 15:32:14 -0400 Subject: [PATCH 71/79] Role tag updates --- builtin/credential/aws/path_role.go | 10 +++- builtin/credential/aws/path_role_tag.go | 78 ++++++++++++++----------- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 9d1d996c09..8f1e21fc5c 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -237,12 +237,14 @@ func (b *backend) pathRoleCreateUpdate( 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 { - return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", maxTTL/time.Second, systemMaxTTL/time.Second)), nil + 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) { @@ -282,7 +284,11 @@ func (b *backend) pathRoleCreateUpdate( return nil, err } - return nil, nil + if len(resp.Warnings()) == 0 { + return nil, nil + } + + return &resp, nil } // Struct to hold the information associated with an AMI ID in Vault. diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index 76820c3f84..0132b98646 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -12,6 +12,7 @@ import ( "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" ) @@ -35,7 +36,7 @@ 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.", + 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{ @@ -76,25 +77,6 @@ func (b *backend) pathRoleTagUpdate( return logical.ErrorResponse("missing role_name"), nil } - // 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)) - } - - // This is an optional field. - disallowReauthentication := data.Get("disallow_reauthentication").(bool) - - // This is an optional field. - allowInstanceMigration := data.Get("allow_instance_migration").(bool) - // Fetch the role entry roleEntry, err := b.awsRole(req.Storage, roleName) if err != nil { @@ -115,10 +97,31 @@ func (b *backend) pathRoleTagUpdate( return nil, fmt.Errorf("failed to find the HMAC key") } - // Create a random nonce. - nonce, err := createRoleTagNonce() - if err != nil { - return nil, err + 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. @@ -126,18 +129,23 @@ func (b *backend) pathRoleTagUpdate( // max_ttl on the tag should not be greater than the system view's max_ttl value. if maxTTL > b.System().MaxLeaseTTL() { - return logical.ErrorResponse(fmt.Sprintf("Registered AMI does not have a max_ttl set. So, the given TTL of %d seconds should be less than the max_ttl set for the corresponding backend mount of %d seconds.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second)), nil + 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 { - return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than the max_ttl set for the corresponding role of %d seconds", maxTTL/time.Second, roleEntry.MaxTTL/time.Second)), nil + 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, @@ -155,12 +163,12 @@ func (b *backend) pathRoleTagUpdate( // 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. - return &logical.Response{ - Data: map[string]interface{}{ - "tag_key": roleEntry.RoleTag, - "tag_value": rTagValue, - }, - }, nil + resp.Data = map[string]interface{}{ + "tag_key": roleEntry.RoleTag, + "tag_value": rTagValue, + } + + return resp, nil } // createRoleTagValue prepares the plaintext version of the role tag, @@ -269,7 +277,7 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { // Attach max_ttl if it is provided. if rTag.MaxTTL > time.Duration(0) { - value = fmt.Sprintf("%s:t=%s", value, rTag.MaxTTL) + value = fmt.Sprintf("%s:t=%d", value, rTag.MaxTTL.Seconds()) } return value, nil @@ -325,7 +333,7 @@ func (b *backend) parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*ro return nil, err } case strings.Contains(tagItem, "t="): - rTag.MaxTTL, err = time.ParseDuration(strings.TrimPrefix(tagItem, "t=")) + rTag.MaxTTL, err = time.ParseDuration(fmt.Sprintf("%ss", strings.TrimPrefix(tagItem, "t="))) if err != nil { return nil, err } From dd5321a86a46d0ad1202d22dfe3c5f983ec41c01 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 5 May 2016 20:44:48 -0400 Subject: [PATCH 72/79] Switch whitelist to use longest max TTL --- builtin/credential/aws/path_login.go | 61 ++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 32adc3256c..2c71f3e6e2 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -261,15 +261,32 @@ func (b *backend) pathLoginUpdate( } // Load the current values for max TTL and policies from the role entry, - // before checking for overriding max TTL in the role tag. - maxTTL := b.System().MaxLeaseTTL() - if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < maxTTL { - maxTTL = roleEntry.MaxTTL + // 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. @@ -300,9 +317,12 @@ func (b *backend) pathLoginUpdate( // Cache the value of role tag's max_ttl value. rTagMaxTTL = resp.MaxTTL - // Scope the maxTTL to the value set on the role tag. - if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < maxTTL { - maxTTL = 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 } } @@ -320,7 +340,7 @@ func (b *backend) pathLoginUpdate( // DisallowReauthentication, PendingTime, LastUpdatedTime and ExpirationTime may change. storedIdentity.LastUpdatedTime = currentTime - storedIdentity.ExpirationTime = currentTime.Add(maxTTL) + storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL) storedIdentity.PendingTime = identityDoc.PendingTime storedIdentity.DisallowReauthentication = disallowReauthentication @@ -357,8 +377,8 @@ func (b *backend) pathLoginUpdate( } // Cap the TTL value. - if maxTTL < resp.Auth.TTL { - resp.Auth.TTL = maxTTL + if shortestMaxTTL < resp.Auth.TTL { + resp.Auth.TTL = shortestMaxTTL } return resp, nil @@ -481,24 +501,31 @@ func (b *backend) pathLoginRenew( } // Re-evaluate the maxTTL bounds. - maxTTL := b.System().MaxLeaseTTL() - if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < maxTTL { - maxTTL = roleEntry.MaxTTL + shortestMaxTTL := b.System().MaxLeaseTTL() + longestMaxTTL := b.System().MaxLeaseTTL() + if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL { + shortestMaxTTL = roleEntry.MaxTTL } - if rTagMaxTTL > time.Duration(0) && maxTTL > rTagMaxTTL { - maxTTL = rTagMaxTTL + 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(maxTTL) + storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL) if err = setWhitelistIdentityEntry(req.Storage, instanceID, storedIdentity); err != nil { return nil, err } - return framework.LeaseExtend(req.Auth.TTL, maxTTL, b.System())(req, data) + return framework.LeaseExtend(req.Auth.TTL, shortestMaxTTL, b.System())(req, data) } // Struct to represent items of interest from the EC2 instance identity document. From 0c6f45d94cb4f18efd24d64e28badcc95c3eb956 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 9 May 2016 23:26:00 +0000 Subject: [PATCH 73/79] Update client code to use internal entry fetching --- builtin/credential/aws/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/credential/aws/client.go b/builtin/credential/aws/client.go index 3178c44ce6..f08b7c04ae 100644 --- a/builtin/credential/aws/client.go +++ b/builtin/credential/aws/client.go @@ -24,7 +24,7 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config } // Read the configured secret key and access key - config, err := b.clientConfigEntry(s) + config, err := b.clientConfigEntryInternal(s) if err != nil { return nil, err } From ebaff78305ca46242d9ab32fb2d122846bbf1aa2 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 9 May 2016 21:01:57 -0400 Subject: [PATCH 74/79] Call client config internal from the locking method --- builtin/credential/aws/path_config_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index 6fe624eb44..240bd87302 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -60,7 +60,7 @@ func (b *backend) clientConfigEntry(s logical.Storage) (*clientConfig, error) { b.configMutex.RLock() defer b.configMutex.RUnlock() - return b.clientConfigEntry(s) + return b.clientConfigEntryInternal(s) } // Internal version that does no locking From c1dd991ccffd738e2f4731bc5e9fceece67d9fc8 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Mon, 9 May 2016 22:07:46 -0400 Subject: [PATCH 75/79] Fix the acceptance tests --- builtin/credential/aws/backend_test.go | 164 +++++++++++++++++++----- builtin/credential/aws/path_role_tag.go | 4 +- 2 files changed, 134 insertions(+), 34 deletions(-) diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 0b39f52e85..f07fcdc6e4 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -6,19 +6,75 @@ import ( "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), + pathConfigTidyRoleTags(b), + pathConfigTidyIdentities(b), + pathListCertificates(b), + pathListBlacklistRoleTags(b), + pathBlacklistRoleTag(b), + pathTidyRoleTags(b), + pathListWhitelistIdentities(b), + pathWhitelistIdentity(b), + pathTidyIdentities(b), + }, + } + + return b, nil +} + func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { // create a backend config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -42,7 +98,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { } // read the created role entry - roleEntry, err := awsRole(storage, "abcd-123") + roleEntry, err := b.awsRole(storage, "abcd-123") if err != nil { t.Fatal(err) } @@ -57,7 +113,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { RoleName: "abcd-123", Nonce: nonce, Policies: []string{"p", "q", "r"}, - MaxTTL: 200, + MaxTTL: 200000000000, // 200s } // create a role tag against the role entry @@ -70,7 +126,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { } // parse the created role tag - rTag2, err := parseAndVerifyRoleTagValue(storage, val) + rTag2, err := b.parseAndVerifyRoleTagValue(storage, val) if err != nil { t.Fatal(err) } @@ -79,7 +135,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { if rTag2.Version != "v1" || rTag2.Nonce != nonce || rTag2.RoleName != "abcd-123" || - rTag2.MaxTTL != 200 || + rTag2.MaxTTL != 200000000000 || // 200s !policyutil.EquivalentPolicies(rTag2.Policies, []string{"p", "q", "r"}) || len(rTag2.HMAC) == 0 { t.Fatalf("parsed role tag is invalid") @@ -109,7 +165,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { } // get the entry of the newly created role entry - roleEntry2, err := awsRole(storage, "ami-6789") + roleEntry2, err := b.awsRole(storage, "ami-6789") if err != nil { t.Fatal(err) } @@ -182,8 +238,8 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { // verify if it contains known fields if !strings.Contains(val, "r=") || - !strings.Contains(val, "p=") || !strings.Contains(val, "d=") || + !strings.Contains(val, "m=") || !strings.HasPrefix(val, "v1") { t.Fatalf("incorrect information in role tag plaintext value") } @@ -199,7 +255,7 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { t.Fatalf("missing instance ID in role tag plaintext value") } - rTag.MaxTTL = 200 + rTag.MaxTTL = 200000000000 // create the role tag with max_ttl specified val, err = prepareRoleTagPlaintextValue(rTag) if err != nil { @@ -207,7 +263,7 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { } // verify it if !strings.Contains(val, "t=") { - t.Fatalf("missing instance ID in role tag plaintext value") + t.Fatalf("missing max_ttl field in role tag plaintext value") } } @@ -237,7 +293,11 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -245,7 +305,7 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { // test update operation tidyRequest := &logical.Request{ Operation: logical.UpdateOperation, - Path: "config/tidy/identities", + Path: "config/tidy/identity-whitelist", Storage: storage, } data := map[string]interface{}{ @@ -265,7 +325,7 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { t.Fatal(err) } if resp == nil || resp.IsError() { - t.Fatalf("failed to read config/tidy/identities endpoint") + 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)) @@ -278,7 +338,7 @@ func TestBackend_ConfigTidyIdentities(t *testing.T) { t.Fatal(err) } if resp != nil { - t.Fatalf("failed to delete config/tidy/identities") + t.Fatalf("failed to delete config/tidy/identity-whitelist") } } @@ -287,7 +347,11 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -295,7 +359,7 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { // test update operation tidyRequest := &logical.Request{ Operation: logical.UpdateOperation, - Path: "config/tidy/roletags", + Path: "config/tidy/roletag-blacklist", Storage: storage, } data := map[string]interface{}{ @@ -315,7 +379,7 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { t.Fatal(err) } if resp == nil || resp.IsError() { - t.Fatalf("failed to read config/tidy/roletags endpoint") + 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)) @@ -328,7 +392,7 @@ func TestBackend_ConfigTidyRoleTags(t *testing.T) { t.Fatal(err) } if resp != nil { - t.Fatalf("failed to delete config/tidy/roletags") + t.Fatalf("failed to delete config/tidy/roletag-blacklist") } } @@ -337,7 +401,11 @@ func TestBackend_TidyIdentities(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -358,7 +426,11 @@ func TestBackend_TidyRoleTags(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -379,7 +451,11 @@ func TestBackend_ConfigClient(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -512,7 +588,11 @@ func TestBackend_pathConfigCertificate(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -659,7 +739,11 @@ func TestBackend_pathRole(t *testing.T) { storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -781,7 +865,11 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -836,7 +924,7 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { tagValue := resp.Data["tag_value"].(string) // parse the value and check if the verifiable values match - rTag, err := parseAndVerifyRoleTagValue(storage, tagValue) + rTag, err := b.parseAndVerifyRoleTagValue(storage, tagValue) if err != nil { t.Fatalf("err: %s", err) } @@ -854,7 +942,11 @@ func TestBackend_PathRoleTag(t *testing.T) { config := logical.TestBackendConfig() storage := &logical.InmemStorage{} config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -914,7 +1006,11 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { storage := &logical.InmemStorage{} config := logical.TestBackendConfig() config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -965,7 +1061,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { // blacklist that role tag resp, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "blacklist/roletag/" + tag, + Path: "roletag-blacklist/" + tag, Storage: storage, }) if err != nil { @@ -978,7 +1074,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { // read the blacklist entry resp, err = b.HandleRequest(&logical.Request{ Operation: logical.ReadOperation, - Path: "blacklist/roletag/" + tag, + Path: "roletag-blacklist/" + tag, Storage: storage, }) if err != nil { @@ -994,7 +1090,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { // delete the blacklisted entry _, err = b.HandleRequest(&logical.Request{ Operation: logical.DeleteOperation, - Path: "blacklist/roletag/" + tag, + Path: "roletag-blacklist/" + tag, Storage: storage, }) if err != nil { @@ -1002,7 +1098,7 @@ func TestBackend_PathBlacklistRoleTag(t *testing.T) { } // try to read the deleted entry - tagEntry, err := blacklistRoleTagEntry(storage, tag) + tagEntry, err := b.blacklistRoleTagEntry(storage, tag) if err != nil { t.Fatal(err) } @@ -1039,7 +1135,11 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { storage := &logical.InmemStorage{} config := logical.TestBackendConfig() config.StorageView = storage - b, err := Factory(config) + b, err := createBackend(config) + if err != nil { + t.Fatal(err) + } + _, err = b.Backend.Setup(config) if err != nil { t.Fatal(err) } @@ -1141,7 +1241,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { // Check if a whitelist identity entry is created after the login. wlRequest := &logical.Request{ Operation: logical.ReadOperation, - Path: "whitelist/identity/" + instanceID, + Path: "identity-whitelist/" + instanceID, Storage: storage, } resp, err = b.HandleRequest(wlRequest) diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index 0132b98646..c48392d9c8 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -276,8 +276,8 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { } // Attach max_ttl if it is provided. - if rTag.MaxTTL > time.Duration(0) { - value = fmt.Sprintf("%s:t=%d", value, rTag.MaxTTL.Seconds()) + if int(rTag.MaxTTL.Seconds()) > 0 { + value = fmt.Sprintf("%s:t=%d", value, int(rTag.MaxTTL.Seconds())) } return value, nil From 4aa01d390a9f244d792d332fd05f52ed80796650 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 12 May 2016 07:19:29 -0400 Subject: [PATCH 76/79] Update docs and path names to the new patterns --- builtin/credential/aws/backend.go | 27 +-- builtin/credential/aws/backend_test.go | 20 +- .../credential/aws/path_blacklist_roletag.go | 52 ++--- .../credential/aws/path_config_certificate.go | 14 +- builtin/credential/aws/path_config_client.go | 12 +- .../aws/path_config_tidy_identities.go | 32 ++-- .../aws/path_config_tidy_roletags.go | 30 +-- builtin/credential/aws/path_login.go | 17 +- builtin/credential/aws/path_role.go | 9 +- builtin/credential/aws/path_role_tag.go | 6 +- .../credential/aws/path_tidy_identities.go | 20 +- builtin/credential/aws/path_tidy_roletags.go | 20 +- .../credential/aws/path_whitelist_identity.go | 43 ++--- website/source/docs/auth/aws.html.md | 178 +++++++++--------- 14 files changed, 245 insertions(+), 235 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 8ffa208f94..56e34d94de 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -84,15 +84,15 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { pathRoleTag(b), pathConfigClient(b), pathConfigCertificate(b), - pathConfigTidyRoleTags(b), - pathConfigTidyIdentities(b), + pathConfigTidyRoletagBlacklist(b), + pathConfigTidyIdentityWhitelist(b), pathListCertificates(b), - pathListBlacklistRoleTags(b), - pathBlacklistRoleTag(b), - pathTidyRoleTags(b), - pathListWhitelistIdentities(b), - pathWhitelistIdentity(b), - pathTidyIdentities(b), + pathListRoletagBlacklist(b), + pathRoletagBlacklist(b), + pathTidyRoletagBlacklist(b), + pathListIdentityWhitelist(b), + pathIdentityWhitelist(b), + pathTidyIdentityWhitelist(b), }, } @@ -168,9 +168,10 @@ 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 policies set on 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 that is performing the login. +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 index f07fcdc6e4..231360f9ef 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -49,15 +49,15 @@ func createBackend(conf *logical.BackendConfig) (*backend, error) { pathRoleTag(b), pathConfigClient(b), pathConfigCertificate(b), - pathConfigTidyRoleTags(b), - pathConfigTidyIdentities(b), + pathConfigTidyRoletagBlacklist(b), + pathConfigTidyIdentityWhitelist(b), pathListCertificates(b), - pathListBlacklistRoleTags(b), - pathBlacklistRoleTag(b), - pathTidyRoleTags(b), - pathListWhitelistIdentities(b), - pathWhitelistIdentity(b), - pathTidyIdentities(b), + pathListRoletagBlacklist(b), + pathRoletagBlacklist(b), + pathTidyRoletagBlacklist(b), + pathListIdentityWhitelist(b), + pathIdentityWhitelist(b), + pathTidyIdentityWhitelist(b), }, } @@ -413,7 +413,7 @@ func TestBackend_TidyIdentities(t *testing.T) { // test update operation _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "tidy/identities", + Path: "tidy/identity-whitelist", Storage: storage, }) if err != nil { @@ -438,7 +438,7 @@ func TestBackend_TidyRoleTags(t *testing.T) { // test update operation _, err = b.HandleRequest(&logical.Request{ Operation: logical.UpdateOperation, - Path: "tidy/roletags", + Path: "tidy/roletag-blacklist", Storage: storage, }) if err != nil { diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_blacklist_roletag.go index 4ec853ac13..83d2f7ede6 100644 --- a/builtin/credential/aws/path_blacklist_roletag.go +++ b/builtin/credential/aws/path_blacklist_roletag.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathBlacklistRoleTag(b *backend) *framework.Path { +func pathRoletagBlacklist(b *backend) *framework.Path { return &framework.Path{ Pattern: "roletag-blacklist/(?P.*)", Fields: map[string]*framework.FieldSchema{ @@ -21,32 +21,32 @@ to avoid any encoding problems, it can be base64 encoded.`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathBlacklistRoleTagUpdate, - logical.ReadOperation: b.pathBlacklistRoleTagRead, - logical.DeleteOperation: b.pathBlacklistRoleTagDelete, + logical.UpdateOperation: b.pathRoletagBlacklistUpdate, + logical.ReadOperation: b.pathRoletagBlacklistRead, + logical.DeleteOperation: b.pathRoletagBlacklistDelete, }, - HelpSynopsis: pathBlacklistRoleTagSyn, - HelpDescription: pathBlacklistRoleTagDesc, + HelpSynopsis: pathRoletagBlacklistSyn, + HelpDescription: pathRoletagBlacklistDesc, } } // Path to list all the blacklisted tags. -func pathListBlacklistRoleTags(b *backend) *framework.Path { +func pathListRoletagBlacklist(b *backend) *framework.Path { return &framework.Path{ Pattern: "roletag-blacklist/?", Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.pathBlacklistRoleTagsList, + logical.ListOperation: b.pathRoletagBlacklistsList, }, - HelpSynopsis: pathListBlacklistRoleTagsHelpSyn, - HelpDescription: pathListBlacklistRoleTagsHelpDesc, + HelpSynopsis: pathListRoletagBlacklistHelpSyn, + HelpDescription: pathListRoletagBlacklistHelpDesc, } } // Lists all the blacklisted role tags. -func (b *backend) pathBlacklistRoleTagsList( +func (b *backend) pathRoletagBlacklistsList( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.blacklistMutex.RLock() defer b.blacklistMutex.RUnlock() @@ -96,7 +96,7 @@ func (b *backend) blacklistRoleTagEntryInternal(s logical.Storage, tag string) ( } // Deletes an entry from the role tag blacklist for a given tag. -func (b *backend) pathBlacklistRoleTagDelete( +func (b *backend) pathRoletagBlacklistDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.blacklistMutex.Lock() defer b.blacklistMutex.Unlock() @@ -111,7 +111,7 @@ func (b *backend) pathBlacklistRoleTagDelete( // If the given role tag is blacklisted, returns the details of the blacklist entry. // Returns 'nil' otherwise. -func (b *backend) pathBlacklistRoleTagRead( +func (b *backend) pathRoletagBlacklistRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { tag := data.Get("role_tag").(string) @@ -132,10 +132,10 @@ func (b *backend) pathBlacklistRoleTagRead( }, nil } -// pathBlacklistRoleTagUpdate is used to blacklist a given role tag. +// 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) pathBlacklistRoleTagUpdate( +func (b *backend) pathRoletagBlacklistUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // The role_tag value provided, optionally can be base64 encoded. @@ -226,17 +226,17 @@ type roleTagBlacklistEntry struct { ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` } -const pathBlacklistRoleTagSyn = ` +const pathRoletagBlacklistSyn = ` Blacklist a previously created role tag. ` -const pathBlacklistRoleTagDesc = ` -Blacklist a role tag so that it cannot be used by any EC2 instance to perform logins -in the future. This can be used if the role tag is suspected or believed to be possessed -by an unintended party. +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 looks for expired entries in the blacklist -and delete them. The duration to periodically run this, is one hour by default. +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. @@ -244,12 +244,12 @@ Also note that delete operation is supported on this endpoint to remove specific entries from the blacklist. ` -const pathListBlacklistRoleTagsHelpSyn = ` -List the blacklisted role tags. +const pathListRoletagBlacklistHelpSyn = ` +Lists the blacklisted role tags. ` -const pathListBlacklistRoleTagsHelpDesc = ` -List all the entries present in the blacklist. This will show both the valid +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_config_certificate.go b/builtin/credential/aws/path_config_certificate.go index 9adaa50c04..86c91ce9c5 100644 --- a/builtin/credential/aws/path_config_certificate.go +++ b/builtin/credential/aws/path_config_certificate.go @@ -310,24 +310,24 @@ type awsPublicCert struct { } const pathConfigCertificateSyn = ` -Configure the AWS Public Key that is used to verify the PKCS#7 signature of the identidy document. +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 can be found in AWS EC2 instance metadata documentation. +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 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 registered certificates -added using this endpoint. +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 Vault. +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 index 240bd87302..38e2ad7ffa 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -13,19 +13,19 @@ func pathConfigClient(b *backend) *framework.Path { "access_key": &framework.FieldSchema{ Type: framework.TypeString, Default: "", - Description: "AWS Access key with permissions to query EC2 instance metadata.", + 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 instance metadata.", + Description: "AWS Secret key with permissions to query EC2 DescribeInstances API.", }, "endpoint": &framework.FieldSchema{ Type: framework.TypeString, Default: "", - Description: "The endpoint to be used to make API calls to AWS EC2.", + Description: "URL to override the default generated endpoint for making AWS EC2 API calls.", }, }, @@ -193,7 +193,7 @@ Configure the client credentials that are used to query instance details from AW ` const pathConfigClientHelpDesc = ` -AWS auth backend makes API calls to retrieve EC2 instance metadata. -The aws_secret_key and aws_access_key registered with Vault should have the -permissions to make these API calls. +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_identities.go b/builtin/credential/aws/path_config_tidy_identities.go index 86076d50c6..700e5fa21a 100644 --- a/builtin/credential/aws/path_config_tidy_identities.go +++ b/builtin/credential/aws/path_config_tidy_identities.go @@ -12,7 +12,7 @@ const ( identityWhitelistConfigPath = "config/tidy/identity-whitelist" ) -func pathConfigTidyIdentities(b *backend) *framework.Path { +func pathConfigTidyIdentityWhitelist(b *backend) *framework.Path { return &framework.Path{ Pattern: fmt.Sprintf("%s$", identityWhitelistConfigPath), Fields: map[string]*framework.FieldSchema{ @@ -25,25 +25,25 @@ 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 'whitelist/identity/' entries.", + Description: "If set to 'true', disables the periodic tidying of the 'identity-whitelist/' entries.", }, }, - ExistenceCheck: b.pathConfigTidyIdentitiesExistenceCheck, + ExistenceCheck: b.pathConfigTidyIdentityWhitelistExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathConfigTidyIdentitiesCreateUpdate, - logical.UpdateOperation: b.pathConfigTidyIdentitiesCreateUpdate, - logical.ReadOperation: b.pathConfigTidyIdentitiesRead, - logical.DeleteOperation: b.pathConfigTidyIdentitiesDelete, + logical.CreateOperation: b.pathConfigTidyIdentityWhitelistCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyIdentityWhitelistCreateUpdate, + logical.ReadOperation: b.pathConfigTidyIdentityWhitelistRead, + logical.DeleteOperation: b.pathConfigTidyIdentityWhitelistDelete, }, - HelpSynopsis: pathConfigTidyIdentitiesHelpSyn, - HelpDescription: pathConfigTidyIdentitiesHelpDesc, + HelpSynopsis: pathConfigTidyIdentityWhitelistHelpSyn, + HelpDescription: pathConfigTidyIdentityWhitelistHelpDesc, } } -func (b *backend) pathConfigTidyIdentitiesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { +func (b *backend) pathConfigTidyIdentityWhitelistExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { entry, err := b.configTidyIdentities(req.Storage) if err != nil { return false, err @@ -74,7 +74,7 @@ func (b *backend) configTidyIdentitiesInternal(s logical.Storage) (*tidyWhitelis return &result, nil } -func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyIdentityWhitelistCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() @@ -112,7 +112,7 @@ func (b *backend) pathConfigTidyIdentitiesCreateUpdate(req *logical.Request, dat return nil, nil } -func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +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 @@ -126,7 +126,7 @@ func (b *backend) pathConfigTidyIdentitiesRead(req *logical.Request, data *frame }, nil } -func (b *backend) pathConfigTidyIdentitiesDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyIdentityWhitelistDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() @@ -138,12 +138,12 @@ type tidyWhitelistIdentityConfig struct { DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"` } -const pathConfigTidyIdentitiesHelpSyn = ` +const pathConfigTidyIdentityWhitelistHelpSyn = ` Configures the periodic tidying operation of the whitelisted identity entries. ` -const pathConfigTidyIdentitiesHelpDesc = ` +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 purge them. +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_roletags.go b/builtin/credential/aws/path_config_tidy_roletags.go index bc02551409..6932f5ec96 100644 --- a/builtin/credential/aws/path_config_tidy_roletags.go +++ b/builtin/credential/aws/path_config_tidy_roletags.go @@ -12,7 +12,7 @@ const ( roletagBlacklistConfigPath = "config/tidy/roletag-blacklist" ) -func pathConfigTidyRoleTags(b *backend) *framework.Path { +func pathConfigTidyRoletagBlacklist(b *backend) *framework.Path { return &framework.Path{ Pattern: fmt.Sprintf("%s$", roletagBlacklistConfigPath), Fields: map[string]*framework.FieldSchema{ @@ -31,21 +31,21 @@ Defaults to 4320h (180 days).`, }, }, - ExistenceCheck: b.pathConfigTidyRoleTagsExistenceCheck, + ExistenceCheck: b.pathConfigTidyRoletagBlacklistExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathConfigTidyRoleTagsCreateUpdate, - logical.UpdateOperation: b.pathConfigTidyRoleTagsCreateUpdate, - logical.ReadOperation: b.pathConfigTidyRoleTagsRead, - logical.DeleteOperation: b.pathConfigTidyRoleTagsDelete, + logical.CreateOperation: b.pathConfigTidyRoletagBlacklistCreateUpdate, + logical.UpdateOperation: b.pathConfigTidyRoletagBlacklistCreateUpdate, + logical.ReadOperation: b.pathConfigTidyRoletagBlacklistRead, + logical.DeleteOperation: b.pathConfigTidyRoletagBlacklistDelete, }, - HelpSynopsis: pathConfigTidyRoleTagsHelpSyn, - HelpDescription: pathConfigTidyRoleTagsHelpDesc, + HelpSynopsis: pathConfigTidyRoletagBlacklistHelpSyn, + HelpDescription: pathConfigTidyRoletagBlacklistHelpDesc, } } -func (b *backend) pathConfigTidyRoleTagsExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { +func (b *backend) pathConfigTidyRoletagBlacklistExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { entry, err := b.configTidyRoleTags(req.Storage) if err != nil { return false, err @@ -77,7 +77,7 @@ func (b *backend) configTidyRoleTagsInternal(s logical.Storage) (*tidyBlacklistR return &result, nil } -func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyRoletagBlacklistCreateUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() @@ -113,7 +113,7 @@ func (b *backend) pathConfigTidyRoleTagsCreateUpdate(req *logical.Request, data return nil, nil } -func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +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 @@ -127,7 +127,7 @@ func (b *backend) pathConfigTidyRoleTagsRead(req *logical.Request, data *framewo }, nil } -func (b *backend) pathConfigTidyRoleTagsDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigTidyRoletagBlacklistDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.configMutex.Lock() defer b.configMutex.Unlock() @@ -139,12 +139,12 @@ type tidyBlacklistRoleTagConfig struct { DisablePeriodicTidy bool `json:"disable_periodic_tidy" structs:"disable_periodic_tidy" mapstructure:"disable_periodic_tidy"` } -const pathConfigTidyRoleTagsHelpSyn = ` +const pathConfigTidyRoletagBlacklistHelpSyn = ` Configures the periodic tidying operation of the blacklisted role tag entries. ` -const pathConfigTidyRoleTagsHelpDesc = ` +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 purge them. +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_login.go b/builtin/credential/aws/path_login.go index 2c71f3e6e2..c33af90ee9 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -20,9 +20,10 @@ func pathLogin(b *backend) *framework.Path { Fields: map[string]*framework.FieldSchema{ "role_name": &framework.FieldSchema{ Type: framework.TypeString, - Description: `Name of the pre-registered role in this backend against which the login -is being attempted. If this is not supplied, the name of the AMI ID in -the instance identity document will be assumed to be the name of the role.`, + Description: `Name of the role against which the login is being attempted. +If 'role_name' 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{ @@ -31,8 +32,10 @@ the instance identity document will be assumed to be the name of the role.`, }, "nonce": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "The nonce created by a client of this backend. Nonce is used to avoid replay attacks. When the instances are configured to be allowed to login only once, nonce parameter is of no use and hence can be skipped.", + 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.`, }, }, @@ -550,7 +553,7 @@ 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 'allow_instance_migration' option on the +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 @@ -558,7 +561,7 @@ provided. All future logins will succeed only if the client nonce matches the no whitelisted entry. By default, a cron task will periodically look for expired entries in the whitelist -and delete them. The duration to periodically run this, is one hour by default. +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 index 8f1e21fc5c..5a2390a59e 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -22,8 +22,9 @@ func pathRole(b *backend) *framework.Path { }, "bound_ami_id": &framework.FieldSchema{ - Type: framework.TypeString, - Description: `If set, instances attempting login must be running the given AMI.`, + 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{ @@ -35,13 +36,13 @@ func pathRole(b *backend) *framework.Path { "max_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Default: 0, - Description: "The maximum allowed lifetime for tokens issued due to logins using this role.", + 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 logging in using this role.", + Description: "Policies to be set on tokens issued using this role.", }, "allow_instance_migration": &framework.FieldSchema{ diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index c48392d9c8..121dbbf6c1 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -419,9 +419,9 @@ Create a tag on a role in order to be able to further restrict the capabilities ` const pathRoleTagDesc = ` -If there are needs to apply only a subset of role's capabilities on the instance, -create a role tag using this endpoint and attach the tag on the instance before -performing login. +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 diff --git a/builtin/credential/aws/path_tidy_identities.go b/builtin/credential/aws/path_tidy_identities.go index 5f7f935ba4..58c6353787 100644 --- a/builtin/credential/aws/path_tidy_identities.go +++ b/builtin/credential/aws/path_tidy_identities.go @@ -9,9 +9,9 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathTidyIdentities(b *backend) *framework.Path { +func pathTidyIdentityWhitelist(b *backend) *framework.Path { return &framework.Path{ - Pattern: "tidy/identities$", + Pattern: "tidy/identity-whitelist$", Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -22,11 +22,11 @@ expiration, before it is removed from the backend storage.`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathTidyIdentitiesUpdate, + logical.UpdateOperation: b.pathTidyIdentityWhitelistUpdate, }, - HelpSynopsis: pathTidyIdentitiesSyn, - HelpDescription: pathTidyIdentitiesDesc, + HelpSynopsis: pathTidyIdentityWhitelistSyn, + HelpDescription: pathTidyIdentityWhitelistDesc, } } @@ -75,19 +75,19 @@ func (b *backend) tidyWhitelistIdentity(s logical.Storage, safety_buffer int) er return nil } -// pathTidyIdentitiesUpdate is used to delete entries in the whitelist that are expired. -func (b *backend) pathTidyIdentitiesUpdate( +// 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 pathTidyIdentitiesSyn = ` +const pathTidyIdentityWhitelistSyn = ` Clean-up the whitelist instance identity entries. ` -const pathTidyIdentitiesDesc = ` +const pathTidyIdentityWhitelistDesc = ` When an instance identity is whitelisted, the expiration time of the whitelist -entry is set based on the least 'max_ttl' value set on: the role, the role tag +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. diff --git a/builtin/credential/aws/path_tidy_roletags.go b/builtin/credential/aws/path_tidy_roletags.go index 1df01307fa..6856b3473e 100644 --- a/builtin/credential/aws/path_tidy_roletags.go +++ b/builtin/credential/aws/path_tidy_roletags.go @@ -9,9 +9,9 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathTidyRoleTags(b *backend) *framework.Path { +func pathTidyRoletagBlacklist(b *backend) *framework.Path { return &framework.Path{ - Pattern: "tidy/roletags$", + Pattern: "tidy/roletag-blacklist$", Fields: map[string]*framework.FieldSchema{ "safety_buffer": &framework.FieldSchema{ Type: framework.TypeDurationSecond, @@ -22,11 +22,11 @@ expiration, before it is removed from the backend storage.`, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathTidyRoleTagsUpdate, + logical.UpdateOperation: b.pathTidyRoletagBlacklistUpdate, }, - HelpSynopsis: pathTidyRoleTagsSyn, - HelpDescription: pathTidyRoleTagsDesc, + HelpSynopsis: pathTidyRoletagBlacklistSyn, + HelpDescription: pathTidyRoletagBlacklistDesc, } } @@ -74,19 +74,19 @@ func (b *backend) tidyBlacklistRoleTag(s logical.Storage, safety_buffer int) err return nil } -// pathTidyRoleTagsUpdate is used to clean-up the entries in the role tag blacklist. -func (b *backend) pathTidyRoleTagsUpdate( +// 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 pathTidyRoleTagsSyn = ` +const pathTidyRoletagBlacklistSyn = ` Clean-up the blacklist role tag entries. ` -const pathTidyRoleTagsDesc = ` +const pathTidyRoletagBlacklistDesc = ` When a role tag is blacklisted, the expiration time of the blacklist entry is -set based on the least 'max_ttl' value set on: the role, the role tag and the +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. diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_whitelist_identity.go index b41c8b1aae..e3aac2bd3e 100644 --- a/builtin/credential/aws/path_whitelist_identity.go +++ b/builtin/credential/aws/path_whitelist_identity.go @@ -8,27 +8,28 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathWhitelistIdentity(b *backend) *framework.Path { +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.", + 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.pathWhitelistIdentityRead, - logical.DeleteOperation: b.pathWhitelistIdentityDelete, + logical.ReadOperation: b.pathIdentityWhitelistRead, + logical.DeleteOperation: b.pathIdentityWhitelistDelete, }, - HelpSynopsis: pathWhitelistIdentitySyn, - HelpDescription: pathWhitelistIdentityDesc, + HelpSynopsis: pathIdentityWhitelistSyn, + HelpDescription: pathIdentityWhitelistDesc, } } -func pathListWhitelistIdentities(b *backend) *framework.Path { +func pathListIdentityWhitelist(b *backend) *framework.Path { return &framework.Path{ Pattern: "identity-whitelist/?", @@ -36,8 +37,8 @@ func pathListWhitelistIdentities(b *backend) *framework.Path { logical.ListOperation: b.pathWhitelistIdentitiesList, }, - HelpSynopsis: pathListWhitelistIdentitiesHelpSyn, - HelpDescription: pathListWhitelistIdentitiesHelpDesc, + HelpSynopsis: pathListIdentityWhitelistHelpSyn, + HelpDescription: pathListIdentityWhitelistHelpDesc, } } @@ -83,8 +84,8 @@ func setWhitelistIdentityEntry(s logical.Storage, instanceID string, identity *w return nil } -// pathWhitelistIdentityDelete is used to delete an entry from the identity whitelist given an instance ID. -func (b *backend) pathWhitelistIdentityDelete( +// 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 == "" { @@ -94,8 +95,8 @@ func (b *backend) pathWhitelistIdentityDelete( return nil, req.Storage.Delete("whitelist/identity/" + instanceID) } -// pathWhitelistIdentityRead is used to view an entry in the identity whitelist given an instance ID. -func (b *backend) pathWhitelistIdentityRead( +// 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 == "" { @@ -126,26 +127,26 @@ type whitelistIdentity struct { LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` } -const pathWhitelistIdentitySyn = ` +const pathIdentityWhitelistSyn = ` Read or delete entries in the identity whitelist. ` -const pathWhitelistIdentityDesc = ` +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 looks for expired entries in the whitelist -and delete them. The duration to periodically run this is one hour by default. +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 pathListWhitelistIdentitiesHelpSyn = ` -List the items present in the identity whitelist. +const pathListIdentityWhitelistHelpSyn = ` +Lists the items present in the identity whitelist. ` -const pathListWhitelistIdentitiesHelpDesc = ` +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 diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index bc68f282af..14d1d3b308 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -31,7 +31,7 @@ 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. +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. @@ -44,9 +44,9 @@ 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 bound against AMI -ID. The roles with this bound can only be used to login by the instances -that are running on the specified AMI. +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 @@ -79,11 +79,11 @@ investigation. During the first login, the backend stores the instance ID that authenticated in a `whitelist`. One method of operation of the backend is to disallow any authentication attempt for an instance ID contained in the whitelist, using the -'disallow_reauthentication' option on the role. 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. +'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 @@ -153,7 +153,7 @@ 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/whitelist/identity/` endpoint. This allows a new +the `auth/aws/identity-whitelist/` endpoint. This allows a new client nonce to be accepted by the backend during the next login request. Under certain circumstances there is another useful setting. When the instance @@ -196,7 +196,7 @@ 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, `disable_reauthentication` should be set explicitly +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 @@ -207,14 +207,15 @@ 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 +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/blacklist/roletag/`. Note that this will not invalidate the -tokens that were already issued; this only blocks any further login requests. +`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 @@ -225,7 +226,7 @@ time which is dynamically determined by three factors: `max_ttl` set on the role 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/identities` and `aws/auth/tidy/roletags` are +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 @@ -236,14 +237,14 @@ 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/roletags` and `config/tidy/identities` +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. Users of instances whose +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), @@ -355,15 +356,12 @@ The response will be in JSON. For example: 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 - DescribeInstanceStatus API. Also, if the login is performed using - the role tag, then these credentials will also be used to fetch the - tags that are set on the EC2 instance via DescribeTags API. If the - 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. + 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
      @@ -378,14 +376,14 @@ The response will be in JSON. For example:
    • access_key required - AWS Access key with permissions to query EC2 instance metadata. + AWS Access key with permissions to query EC2 DescribeInstances API.
    • secret_key required - AWS Secret key with permissions to query EC2 instance metadata. + AWS Secret key with permissions to query EC2 DescribeInstances API.
      @@ -582,7 +580,7 @@ The response will be in JSON. For example:
    -### /auth/aws/config/tidy/identities +### /auth/aws/config/tidy/identity-whitelist ##### POST
    Description
    @@ -594,7 +592,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/config/tidy/identities`
    +
    `/auth/aws/config/tidy/identity-whitelist`
    Parameters
    @@ -610,8 +608,8 @@ The response will be in JSON. For example:
  • disable_periodic_tidy optional - If set to 'true', disables the periodic tidying of the 'whitelist/identity/' - entries and 'whitelist/identity/' entries. + If set to 'true', disables the periodic tidying of the 'identity-whitelist/' + entries.
@@ -633,7 +631,7 @@ The response will be in JSON. For example:
GET
URL
-
`/auth/aws/config/tidy/identities`
+
`/auth/aws/config/tidy/identity-whitelist`
Parameters
@@ -671,7 +669,7 @@ The response will be in JSON. For example:
DELETE
URL
-
`/auth/aws/config/tidy/identities`
+
`/auth/aws/config/tidy/identity-whitelist`
Parameters
@@ -685,7 +683,7 @@ The response will be in JSON. For example: -### /auth/aws/config/tidy/roletags +### /auth/aws/config/tidy/roletag-blacklist ##### POST
Description
@@ -697,7 +695,7 @@ The response will be in JSON. For example:
POST
URL
-
`/auth/aws/config/tidy/roletags`
+
`/auth/aws/config/tidy/roletag-blacklist`
Parameters
@@ -713,7 +711,7 @@ The response will be in JSON. For example:
  • disable_periodic_tidy optional - If set to 'true', disables the periodic tidying of the 'blacklist/roletag/' entries and 'whitelist/identity/' entries. + If set to 'true', disables the periodic tidying of the 'roletag-blacklist/' entries.
  • @@ -735,7 +733,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/config/tidy/roletags`
    +
    `/auth/aws/config/tidy/roletag-blacklist`
    Parameters
    @@ -773,7 +771,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws/config/tidy/roletags`
    +
    `/auth/aws/config/tidy/roletag-blacklist`
    Parameters
    @@ -794,9 +792,9 @@ The response will be in JSON. For example:
    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 that are 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, not necessarily 'bound_ami_id'. + 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
    @@ -818,36 +816,31 @@ The response will be in JSON. For example:
  • bound_ami_id required - If set, defines a constraint that the EC2 instances that are trying to login, - should be using the AMI ID specified by this parameter. -
  • - -
      -
    • - 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. + 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 `roletag` login for this AMI, meaning that this AMI is shared among many EC2 instances. The value set for this field should be the `key` of the tag on the EC2 instance and the `tag_value` returned from `auth/aws/image//roletag` should be the `value` of the tag on the instance. Defaults to empty string, meaning that this AMI is not shared among instances. + 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 lease duration. + The maximum allowed lifetime of tokens issued using this role.
    • policies optional - Policies to be associated with the AMI. + Policies to be set on tokens issued using this role.
      @@ -861,7 +854,7 @@ The response will be in JSON. For example:
    • disallow_reauthentication optional - If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws/whitelist/identity/' endpoint. Defaults to 'false'. + If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using 'auth/aws/identity-whitelist/' endpoint. Defaults to 'false'.
    @@ -964,7 +957,7 @@ The response will be in JSON. For example:
    Description
    - Deletes the previously registered AMI ID. + Deletes the previously registered role.
    Method
    @@ -989,8 +982,8 @@ The response will be in JSON. For example:
    Description
    - Creates a `roletag` on the role. Role tags provide an effective way to restrict the - policies that are set on the role. + Creates a role tag on the role. Role tags provide an effective way to restrict the + capabilities that are set on the role.
    Method
    @@ -1012,21 +1005,30 @@ The response will be in JSON. For example:
  • policies optional - Policies to be associated with the tag. + 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 - The maximum allowed lease duration. + 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/whitelist/identity endpoint. Defaults to 'false'. + If set, only allows a single token to be granted per instance ID. This can be cleared with the auth/aws/identity-whitelist endpoint. Defaults to 'false'.
      @@ -1082,9 +1084,9 @@ The response will be in JSON. For example: role_name optional Name of the role against which the login is being attempted. - If `role_name` is not specified, then the login endpoint assumes that there - is a role by the name matching the AMI ID of the EC2 instance that is trying - to login. If a matching role is not found, login fails. + If `role_name` 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.
      @@ -1099,7 +1101,7 @@ The response will be in JSON. For example: nonce required/optional, depends The `nonce` created by a client of this backend. When `disallow_reauthentication` - option is enabled on either the AMI or the role tag, then `nonce` parameter is + option is enabled on either the role or the role tag, then `nonce` parameter is optional. It is a required parameter otherwise.
    @@ -1138,15 +1140,15 @@ The response will be in JSON. For example:
    -### /auth/aws/blacklist/roletag/ +### /auth/aws/roletag-blacklist/ #### POST
    Description
    - Places a valid roletag in a blacklist. This ensures that the `roletag` + 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 this `roletag` was previousy used to perfom a successful - login, placing the `roletag` in the blacklist does not invalidate the + 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.
    @@ -1154,7 +1156,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/blacklist/roletag/`
    +
    `/auth/aws/roletag-blacklist/`
    Parameters
    @@ -1162,7 +1164,8 @@ The response will be in JSON. For example:
  • role_tag required - Role tag that needs be blacklisted. The tag can be supplied as-is, or can be base64 encoded. + Role tag to be blacklisted. The tag can be supplied as-is. In order + to avoid any encoding problems, it can be base64 encoded.
  • @@ -1177,14 +1180,14 @@ The response will be in JSON. For example:
    Description
    - Returns the blacklist entry of a previously blacklisted `roletag`. + Returns the blacklist entry of a previously blacklisted role tag.
    Method
    GET
    URL
    -
    `/auth/aws/blacklist/roletag/`
    +
    `/auth/aws/broletag-blacklist/`
    Parameters
    @@ -1216,14 +1219,14 @@ The response will be in JSON. For example:
    Description
    - Lists all the `roletags` that are blacklisted. + Lists all the role tags that are blacklisted.
    Method
    GET
    URL
    -
    `/auth/aws/blacklist/roletags?list=true`
    +
    `/auth/aws/roletag-blacklist?list=true`
    Parameters
    @@ -1256,14 +1259,14 @@ The response will be in JSON. For example:
    Description
    - Deletes a blacklisted `roletag`. + Deletes a blacklisted role tag.
    Method
    DELETE
    URL
    -
    `/auth/aws/blacklist/roletag/`
    +
    `/auth/aws/roletag-blacklist/`
    Parameters
    @@ -1276,7 +1279,7 @@ The response will be in JSON. For example:
    -### /auth/aws/tidy/roletags +### /auth/aws/tidy/roletag-blacklist #### POST
    Description
    @@ -1288,7 +1291,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/tidy/roletags`
    +
    `/auth/aws/tidy/roletag-blacklist`
    Parameters
    @@ -1307,7 +1310,7 @@ The response will be in JSON. For example:
    -### /auth/aws/whitelist/identity/ +### /auth/aws/identity-whitelist/ #### GET
    Description
    @@ -1319,7 +1322,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/whitelist/identity/`
    +
    `/auth/aws/identity-whitelist/`
    Parameters
    @@ -1327,7 +1330,8 @@ The response will be in JSON. For example:
  • instance_id required - EC2 instance ID. A successful login operation from an EC2 instance gets cached in this whitelist, keyed off of instance ID. + EC2 instance ID. A successful login operation from an EC2 instance + gets cached in this whitelist, keyed off of instance ID.
  • @@ -1367,7 +1371,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/whitelist/identities?list=true`
    +
    `/auth/aws/identity-whitelist?list=true`
    Parameters
    @@ -1407,7 +1411,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws/whitelist/identity/`
    +
    `/auth/aws/identity-whitelist/`
    Parameters
    @@ -1420,7 +1424,7 @@ The response will be in JSON. For example:
    -### /auth/aws/tidy/identities +### /auth/aws/tidy/identity-whitelist #### POST
    Description
    @@ -1432,7 +1436,7 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/tidy/identities`
    +
    `/auth/aws/tidy/identity-whitelist`
    Parameters
    From 33b7c1a641252f67357564ce52e8f7b1f8eba45a Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 12 May 2016 11:52:07 -0400 Subject: [PATCH 77/79] Name the files based on changed path patterns --- ..._tidy_identities.go => path_config_tidy_identity_whitelist.go} | 0 ...fig_tidy_roletags.go => path_config_tidy_roletag_blacklist.go} | 0 .../{path_whitelist_identity.go => path_identity_whitelist.go} | 0 .../aws/{path_blacklist_roletag.go => path_roletag_blacklist.go} | 0 .../{path_tidy_identities.go => path_tidy_identity_whitelist.go} | 0 .../aws/{path_tidy_roletags.go => path_tidy_roletag_blacklist.go} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename builtin/credential/aws/{path_config_tidy_identities.go => path_config_tidy_identity_whitelist.go} (100%) rename builtin/credential/aws/{path_config_tidy_roletags.go => path_config_tidy_roletag_blacklist.go} (100%) rename builtin/credential/aws/{path_whitelist_identity.go => path_identity_whitelist.go} (100%) rename builtin/credential/aws/{path_blacklist_roletag.go => path_roletag_blacklist.go} (100%) rename builtin/credential/aws/{path_tidy_identities.go => path_tidy_identity_whitelist.go} (100%) rename builtin/credential/aws/{path_tidy_roletags.go => path_tidy_roletag_blacklist.go} (100%) diff --git a/builtin/credential/aws/path_config_tidy_identities.go b/builtin/credential/aws/path_config_tidy_identity_whitelist.go similarity index 100% rename from builtin/credential/aws/path_config_tidy_identities.go rename to builtin/credential/aws/path_config_tidy_identity_whitelist.go diff --git a/builtin/credential/aws/path_config_tidy_roletags.go b/builtin/credential/aws/path_config_tidy_roletag_blacklist.go similarity index 100% rename from builtin/credential/aws/path_config_tidy_roletags.go rename to builtin/credential/aws/path_config_tidy_roletag_blacklist.go diff --git a/builtin/credential/aws/path_whitelist_identity.go b/builtin/credential/aws/path_identity_whitelist.go similarity index 100% rename from builtin/credential/aws/path_whitelist_identity.go rename to builtin/credential/aws/path_identity_whitelist.go diff --git a/builtin/credential/aws/path_blacklist_roletag.go b/builtin/credential/aws/path_roletag_blacklist.go similarity index 100% rename from builtin/credential/aws/path_blacklist_roletag.go rename to builtin/credential/aws/path_roletag_blacklist.go diff --git a/builtin/credential/aws/path_tidy_identities.go b/builtin/credential/aws/path_tidy_identity_whitelist.go similarity index 100% rename from builtin/credential/aws/path_tidy_identities.go rename to builtin/credential/aws/path_tidy_identity_whitelist.go diff --git a/builtin/credential/aws/path_tidy_roletags.go b/builtin/credential/aws/path_tidy_roletag_blacklist.go similarity index 100% rename from builtin/credential/aws/path_tidy_roletags.go rename to builtin/credential/aws/path_tidy_roletag_blacklist.go From b53f0cb62419e0bbe7f4f91ececaec6f7db3f1b0 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Fri, 13 May 2016 14:31:13 -0400 Subject: [PATCH 78/79] Rename 'role_name' to 'role' --- builtin/credential/aws/backend.go | 4 +-- builtin/credential/aws/backend_test.go | 22 ++++++------ .../credential/aws/path_identity_whitelist.go | 2 +- builtin/credential/aws/path_login.go | 18 +++++----- builtin/credential/aws/path_role.go | 20 +++++------ builtin/credential/aws/path_role_tag.go | 32 ++++++++--------- .../credential/aws/path_roletag_blacklist.go | 2 +- website/source/docs/auth/aws.html.md | 34 +++++++++---------- 8 files changed, 67 insertions(+), 67 deletions(-) diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 56e34d94de..e0f56d3e8d 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -166,11 +166,11 @@ 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. +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 +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 index 231360f9ef..06a27a3084 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -110,7 +110,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { } rTag1 := &roleTag{ Version: "v1", - RoleName: "abcd-123", + Role: "abcd-123", Nonce: nonce, Policies: []string{"p", "q", "r"}, MaxTTL: 200000000000, // 200s @@ -134,7 +134,7 @@ func TestBackend_CreateParseVerifyRoleTag(t *testing.T) { // check the values in parsed role tag if rTag2.Version != "v1" || rTag2.Nonce != nonce || - rTag2.RoleName != "abcd-123" || + rTag2.Role != "abcd-123" || rTag2.MaxTTL != 200000000000 || // 200s !policyutil.EquivalentPolicies(rTag2.Policies, []string{"p", "q", "r"}) || len(rTag2.HMAC) == 0 { @@ -198,9 +198,9 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { t.Fatal(err) } rTag := &roleTag{ - Version: "v1", - Nonce: nonce, - RoleName: "abcd-123", + Version: "v1", + Nonce: nonce, + Role: "abcd-123", } rTag.Version = "" @@ -221,14 +221,14 @@ func TestBackend_prepareRoleTagPlaintextValue(t *testing.T) { } rTag.Nonce = nonce - rTag.RoleName = "" + rTag.Role = "" // try to create plaintext part of role tag - // without specifying role_name + // without specifying role val, err = prepareRoleTagPlaintextValue(rTag) if err == nil { - t.Fatalf("expected error for missing role_name") + t.Fatalf("expected error for missing role") } - rTag.RoleName = "abcd-123" + rTag.Role = "abcd-123" // create the plaintext part of the tag val, err = prepareRoleTagPlaintextValue(rTag) @@ -933,7 +933,7 @@ func TestBackend_parseAndVerifyRoleTagValue(t *testing.T) { } if rTag.Version != "v1" || !policyutil.EquivalentPolicies(rTag.Policies, []string{"p", "q", "r", "s"}) || - rTag.RoleName != "abcd-123" { + rTag.Role != "abcd-123" { t.Fatalf("bad: parsed role tag contains incorrect values. Got: %#v\n", rTag) } } @@ -1248,7 +1248,7 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) { if err != nil { t.Fatal(err) } - if resp == nil || resp.Data == nil || resp.Data["role_name"] != roleName { + if resp == nil || resp.Data == nil || resp.Data["role"] != roleName { t.Fatalf("failed to read whitelist identity") } diff --git a/builtin/credential/aws/path_identity_whitelist.go b/builtin/credential/aws/path_identity_whitelist.go index e3aac2bd3e..ba7b861b78 100644 --- a/builtin/credential/aws/path_identity_whitelist.go +++ b/builtin/credential/aws/path_identity_whitelist.go @@ -118,7 +118,7 @@ func (b *backend) pathIdentityWhitelistRead( // Struct to represent each item in the identity whitelist. type whitelistIdentity struct { - RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` + 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"` diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index c33af90ee9..7aecff4461 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -18,10 +18,10 @@ func pathLogin(b *backend) *framework.Path { return &framework.Path{ Pattern: "login$", Fields: map[string]*framework.FieldSchema{ - "role_name": &framework.FieldSchema{ + "role": &framework.FieldSchema{ Type: framework.TypeString, Description: `Name of the role against which the login is being attempted. -If 'role_name' is not specified, then the login endpoint looks for a role +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.`, }, @@ -220,7 +220,7 @@ func (b *backend) pathLoginUpdate( return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil } - roleName := data.Get("role_name").(string) + 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 == "" { @@ -332,10 +332,10 @@ func (b *backend) pathLoginUpdate( // Save the login attempt in the identity whitelist. currentTime := time.Now().UTC() if storedIdentity == nil { - // RoleName, ClientNonce and CreationTime of the identity entry, + // Role, ClientNonce and CreationTime of the identity entry, // once set, should never change. storedIdentity = &whitelistIdentity{ - RoleName: roleName, + Role: roleName, ClientNonce: clientNonce, CreationTime: currentTime, } @@ -369,7 +369,7 @@ func (b *backend) pathLoginUpdate( "instance_id": identityDoc.InstanceID, "region": identityDoc.Region, "role_tag_max_ttl": rTagMaxTTL.String(), - "role_name": roleName, + "role": roleName, "ami_id": identityDoc.AmiID, }, LeaseOptions: logical.LeaseOptions{ @@ -432,8 +432,8 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc // Check if the role name with which this login is being made is same // as the role name embedded in the tag. - if rTag.RoleName != roleName { - return nil, fmt.Errorf("role_name on the tag is not matching the role_name supplied") + 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. @@ -487,7 +487,7 @@ func (b *backend) pathLoginRenew( } // Ensure that role entry is not deleted. - roleEntry, err := b.awsRole(req.Storage, storedIdentity.RoleName) + roleEntry, err := b.awsRole(req.Storage, storedIdentity.Role) if err != nil { return nil, err } diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index 5a2390a59e..adb8698763 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -14,9 +14,9 @@ import ( func pathRole(b *backend) *framework.Path { return &framework.Path{ - Pattern: "role/" + framework.GenericNameRegex("role_name"), + Pattern: "role/" + framework.GenericNameRegex("role"), Fields: map[string]*framework.FieldSchema{ - "role_name": &framework.FieldSchema{ + "role": &framework.FieldSchema{ Type: framework.TypeString, Description: "Name of the role.", }, @@ -30,7 +30,7 @@ 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.", + 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{ @@ -101,7 +101,7 @@ func pathListRoles(b *backend) *framework.Path { // 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_name").(string))) + entry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role").(string))) if err != nil { return false, err } @@ -135,9 +135,9 @@ func (b *backend) awsRoleInternal(s logical.Storage, role string) (*awsRoleEntry // 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_name").(string) + roleName := data.Get("role").(string) if roleName == "" { - return logical.ErrorResponse("missing role_name"), nil + return logical.ErrorResponse("missing role"), nil } b.roleMutex.Lock() @@ -162,7 +162,7 @@ func (b *backend) pathRoleList( // 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_name").(string))) + roleEntry, err := b.awsRole(req.Storage, strings.ToLower(data.Get("role").(string))) if err != nil { return nil, err } @@ -188,9 +188,9 @@ func (b *backend) pathRoleRead( func (b *backend) pathRoleCreateUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - roleName := strings.ToLower(data.Get("role_name").(string)) + roleName := strings.ToLower(data.Get("role").(string)) if roleName == "" { - return logical.ErrorResponse("missing role_name"), nil + return logical.ErrorResponse("missing role"), nil } b.roleMutex.Lock() @@ -316,7 +316,7 @@ 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 +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. diff --git a/builtin/credential/aws/path_role_tag.go b/builtin/credential/aws/path_role_tag.go index 121dbbf6c1..4929bc07b8 100644 --- a/builtin/credential/aws/path_role_tag.go +++ b/builtin/credential/aws/path_role_tag.go @@ -21,9 +21,9 @@ const roleTagVersion = "v1" func pathRoleTag(b *backend) *framework.Path { return &framework.Path{ - Pattern: "role/" + framework.GenericNameRegex("role_name") + "/tag$", + Pattern: "role/" + framework.GenericNameRegex("role") + "/tag$", Fields: map[string]*framework.FieldSchema{ - "role_name": &framework.FieldSchema{ + "role": &framework.FieldSchema{ Type: framework.TypeString, Description: "Name of the role.", }, @@ -72,9 +72,9 @@ If set, the created tag can only be used by the instance with the given ID.`, func (b *backend) pathRoleTagUpdate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - roleName := strings.ToLower(data.Get("role_name").(string)) + roleName := strings.ToLower(data.Get("role").(string)) if roleName == "" { - return logical.ErrorResponse("missing role_name"), nil + return logical.ErrorResponse("missing role"), nil } // Fetch the role entry @@ -149,7 +149,7 @@ func (b *backend) pathRoleTagUpdate( // Create a role tag out of all the information provided. rTagValue, err := createRoleTagValue(&roleTag{ Version: roleTagVersion, - RoleName: roleName, + Role: roleName, Nonce: nonce, Policies: policies, MaxTTL: maxTTL, @@ -257,13 +257,13 @@ func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) { if rTag.Nonce == "" { return "", fmt.Errorf("missing nonce") } - if rTag.RoleName == "" { - return "", fmt.Errorf("missing role_name") + if rTag.Role == "" { + return "", fmt.Errorf("missing role") } - // Attach Version, Nonce, RoleName, DisallowReauthentication and AllowInstanceMigration + // 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.RoleName, strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration)) + 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 { @@ -319,7 +319,7 @@ func (b *backend) parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*ro case strings.Contains(tagItem, "i="): rTag.InstanceID = strings.TrimPrefix(tagItem, "i=") case strings.Contains(tagItem, "r="): - rTag.RoleName = strings.TrimPrefix(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="): @@ -342,16 +342,16 @@ func (b *backend) parseAndVerifyRoleTagValue(s logical.Storage, tag string) (*ro } } - if rTag.RoleName == "" { + if rTag.Role == "" { return nil, fmt.Errorf("missing role name") } - roleEntry, err := b.awsRole(s, rTag.RoleName) + 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.RoleName) + 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. @@ -394,7 +394,7 @@ type roleTag struct { 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"` - RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` + 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"` @@ -407,7 +407,7 @@ func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { rTag1.Nonce == rTag2.Nonce && policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && rTag1.MaxTTL == rTag2.MaxTTL && - rTag1.RoleName == rTag2.RoleName && + rTag1.Role == rTag2.Role && rTag1.HMAC == rTag2.HMAC && rTag1.InstanceID == rTag2.InstanceID && rTag1.DisallowReauthentication == rTag2.DisallowReauthentication && @@ -424,7 +424,7 @@ instance, create a role tag using this endpoint and attach the tag on the instan 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 +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 diff --git a/builtin/credential/aws/path_roletag_blacklist.go b/builtin/credential/aws/path_roletag_blacklist.go index 83d2f7ede6..ff7362449d 100644 --- a/builtin/credential/aws/path_roletag_blacklist.go +++ b/builtin/credential/aws/path_roletag_blacklist.go @@ -166,7 +166,7 @@ func (b *backend) pathRoletagBlacklistUpdate( } // Get the entry for the role mentioned in the role tag. - roleEntry, err := b.awsRole(req.Storage, rTag.RoleName) + roleEntry, err := b.awsRole(req.Storage, rTag.Role) if err != nil { return nil, err } diff --git a/website/source/docs/auth/aws.html.md b/website/source/docs/auth/aws.html.md index 14d1d3b308..37452036b6 100644 --- a/website/source/docs/auth/aws.html.md +++ b/website/source/docs/auth/aws.html.md @@ -128,7 +128,7 @@ instance. The tag holds information that represents a *subset* of privileges tha are set on the role and are used to further restrict the set of the role's privileges for that particular instance. -A `role_tag` can be created using `auth/aws/role//tag` endpoint +A `role_tag` can be created using `auth/aws/role//tag` endpoint and is immutable. The information present in the tag is SHA256 hashed and HMAC protected. The per-role key to HMAC is only maintained in the backend. This prevents an adversarial operator from modifying the tag when setting it on the EC2 instance @@ -286,7 +286,7 @@ $ vault write auth/aws/role/dev-role bound_ami_id=ami-fce3c696 policies=prod,dev #### Perform the login operation ``` -$ vault write auth/aws/login role_name=dev-role pkcs7=MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA nonce=vault-client-nonce +$ vault write auth/aws/login role=dev-role pkcs7=MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA nonce=vault-client-nonce ``` @@ -313,7 +313,7 @@ curl -X POST -H "x-vault-token:123" "http://127.0.0.1:8200/v1/auth/aws/role/dev- #### Perform the login operation ``` -curl -X POST "http://127.0.0.1:8200/v1/auth/aws/login" -d '{"role_name":"dev-role","pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGmewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAicHJpdmF0ZUlwIiA6ICIxNzIuMzEuNjMuNjAiLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1cy1lYXN0LTFjIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3RhbmNlSWQiIDogImktZGUwZjEzNDQiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIgOiAiMjQxNjU2NjE1ODU5IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwKICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDVUMTY6MjY6NTVaIiwKICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDUxNjI3MDBaMCMGCSqGSIb3DQEJBDEWBBRtiynzMTNfTw1TV/d8NvfgVw+XfTAJBgcqhkjOOAQDBC4wLAIUVfpVcNYoOKzN1c+h1Vsm/c5U0tQCFAK/K72idWrONIqMOVJ8Uen0wYg4AAAAAAAA","nonce":"vault-client-nonce"}' +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"}' ``` @@ -328,7 +328,7 @@ The response will be in JSON. For example: "role_tag_max_ttl": "0", "instance_id": "i-de0f1344" "ami_id": "ami-fce3c696" - "role_name": "dev-prod" + "role": "dev-prod" }, "policies": [ "default", @@ -785,7 +785,7 @@ The response will be in JSON. For example: -### /auth/aws/role/ +### /auth/aws/role/ #### POST
    Description
    @@ -801,13 +801,13 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/role/`
    +
    `/auth/aws/role/`
    Parameters
    • - role_name + role required Name of the role.
    • @@ -825,7 +825,7 @@ The response will be in JSON. For example: optional If set, enables the role tags for this role. The value set for this field should be the 'key' of the tag on the EC2 instance. The 'value' - of the tag should be generated using 'role//tag' endpoint. + of the tag should be generated using 'role//tag' endpoint. Defaults to an empty string, meaning that role tags are disabled.
    @@ -876,7 +876,7 @@ The response will be in JSON. For example:
    GET
    URL
    -
    `/auth/aws/role/`
    +
    `/auth/aws/role/`
    Parameters
    @@ -964,7 +964,7 @@ The response will be in JSON. For example:
    DELETE
    URL
    -
    `/auth/aws/role/`
    +
    `/auth/aws/role/`
    Parameters
    @@ -977,7 +977,7 @@ The response will be in JSON. For example:
    -### /auth/aws/role//tag +### /auth/aws/role//tag #### POST
    Description
    @@ -990,13 +990,13 @@ The response will be in JSON. For example:
    POST
    URL
    -
    `/auth/aws/role//tag`
    +
    `/auth/aws/role//tag`
    Parameters
    • - role_name + role required Name of the role.
    • @@ -1081,10 +1081,10 @@ The response will be in JSON. For example:
      • - role_name + role optional Name of the role against which the login is being attempted. - If `role_name` is not specified, then the login endpoint looks for a role + 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.
      • @@ -1119,7 +1119,7 @@ The response will be in JSON. For example: "role_tag_max_ttl": "0", "instance_id": "i-de0f1344" "ami_id": "ami-fce36983" - "role_name": "dev-role" + "role": "dev-role" }, "policies": [ "default", @@ -1348,7 +1348,7 @@ The response will be in JSON. For example: "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_name": "dev-role" + "role": "dev-role" }, "lease_duration": 0, "renewable": false, From 116e2cf0242dd26dacf3c86e6bc9c9eb2ff2e371 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Sat, 14 May 2016 19:35:36 -0400 Subject: [PATCH 79/79] Fix framework rollback manager tests --- logical/framework/backend_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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)