Change image/ to a more flexible /role endpoint
This commit is contained in:
parent
9f2a111e85
commit
b7c48ba109
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
`
|
|
@ -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.
|
||||
`
|
||||
|
|
301
builtin/credential/aws/path_role.go
Normal file
301
builtin/credential/aws/path_role.go
Normal 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.
|
||||
`
|
|
@ -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.
|
||||
`
|
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue