Change image/ to a more flexible /role endpoint

This commit is contained in:
vishalnayak 2016-05-03 12:14:07 -04:00
parent 9f2a111e85
commit b7c48ba109
14 changed files with 794 additions and 655 deletions

View file

@ -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/<ami_id>' 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/<role_name>' 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/<ami_id>/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/<role_name>/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.
`

View file

@ -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")
}

View file

@ -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

View file

@ -14,8 +14,9 @@ func pathBlacklistRoleTag(b *backend) *framework.Path {
Pattern: "blacklist/roletag/(?P<role_tag>.*)",
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.
`

View file

@ -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/<cert_name>` 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

View file

@ -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

View file

@ -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/<ami_id>/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/<instance_id>' 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/<ami_id>/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.
`

View file

@ -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/<cert_name>' endpoint
// and all the registered certificates via 'config/certificate/<cert_name>' 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.
`

View file

@ -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/<role_name>/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/<instance_id>' 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/<role_name>/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.
`

View file

@ -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/<ami_id>' 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/<role_name>'. 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.
`

View file

@ -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.
`

View file

@ -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.
`

View file

@ -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"`

View file

@ -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/<ami_id>/roletag` endpoint
A `role_tag` can be created using `auth/aws/role/<role_name>/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/<role_tag>`. 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/<role_tag>`. 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/<cert_name>` 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/<ami_id>
### /auth/aws/role/<role_name>
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
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/<ami_id>/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'.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/auth/aws/image/<ami_id>`</dd>
<dd>`/auth/aws/role/<role_name>`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">ami_id</span>
<span class="param">role_name</span>
<span class="param-flags">required</span>
AMI ID to be mapped.
Name of the role.
</li>
</ul>
<ul>
<li>
<span class="param">bound_ami_id</span>
<span class="param-flags">required</span>
If set, defines a constraint that the EC2 instances that are trying to login,
should be using the AMI ID specified by this parameter.
</li>
</ul>
<ul>
@ -864,14 +876,14 @@ The response will be in JSON. For example:
<dl class="api">
<dt>Description</dt>
<dd>
Returns the previously registered AMI ID configuration.
Returns the previously registered role configuration.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/auth/aws/image/<ami_id>`</dd>
<dd>`/auth/aws/role/<role_name>`</dd>
<dt>Parameters</dt>
<dd>
@ -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:
<dl class="api">
<dt>Description</dt>
<dd>
Lists all the AMI IDs that are registered with the backend.
Lists all the roles that are registered with the backend.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/auth/aws/images?list=true`</dd>
<dd>`/auth/aws/roles?list=true`</dd>
<dt>Parameters</dt>
<dd>
@ -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:
<dd>DELETE</dd>
<dt>URL</dt>
<dd>`/auth/aws/image/<ami_id>`</dd>
<dd>`/auth/aws/role/<role_name>`</dd>
<dt>Parameters</dt>
<dd>
@ -971,29 +984,28 @@ The response will be in JSON. For example:
</dl>
### /auth/aws/image/<ami_id>/roletag
### /auth/aws/role/<role_name>/tag
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
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.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/auth/aws/image/<ami_id>/roletag`</dd>
<dd>`/auth/aws/role/<role_name>/tag`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">ami_id</span>
<span class="param">role_name</span>
<span class="param-flags">required</span>
AMI ID to create a tag for.
Name of the role.
</li>
</ul>
<ul>
@ -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:
<dl class="api">
<dt>Description</dt>
<dd>
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.
</dd>
<dt>Method</dt>
@ -1064,6 +1077,16 @@ The response will be in JSON. For example:
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">role_name</span>
<span class="param-flags">optional</span>
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.
</li>
</ul>
<ul>
<li>
<span class="param">pkcs7</span>
@ -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,