Skip to content

Commit

Permalink
secret/aws: Support permissions boundaries on iam_user creds (hashico…
Browse files Browse the repository at this point in the history
…rp#6786)

* secrets/aws: Support permissions boundaries on iam_user creds

This allows configuring Vault to attach a permissions boundary policy to
IAM users that it creates, configured on a per-Vault-role basis.

* Fix indentation of policy in docs

Use spaces instead of tabs
  • Loading branch information
joelthompson authored and tyrannosaurus-becks committed Sep 19, 2019
1 parent ad9e5eb commit 961f446
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 41 deletions.
65 changes: 44 additions & 21 deletions builtin/logical/aws/backend_test.go
Expand Up @@ -57,6 +57,25 @@ func TestBackend_basic(t *testing.T) {
})
}

func TestBackend_IamUserWithPermissionsBoundary(t *testing.T) {
t.Parallel()
roleData := map[string]interface{}{
"credential_type": iamUserCred,
"policy_arns": adminAccessPolicyArn,
"permissions_boundary_arn": iamPolicyArn,
}
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() { testAccPreCheck(t) },
LogicalBackend: getBackend(t),
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepWriteRole(t, "test", roleData),
testAccStepRead(t, "creds", "test", []credentialTestFunc{listIamUsersTest, describeAzsTestUnauthorized}),
},
})
}

func TestBackend_basicSTS(t *testing.T) {
t.Parallel()
awsAccountID, err := getAccountID()
Expand Down Expand Up @@ -681,13 +700,14 @@ func testAccStepReadPolicy(t *testing.T, name string, value string) logicaltest.
}

expected := map[string]interface{}{
"policy_arns": []string(nil),
"role_arns": []string(nil),
"policy_document": value,
"credential_type": strings.Join([]string{iamUserCred, federationTokenCred}, ","),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "",
"policy_arns": []string(nil),
"role_arns": []string(nil),
"policy_document": value,
"credential_type": strings.Join([]string{iamUserCred, federationTokenCred}, ","),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "",
"permissions_boundary_arn": "",
}
if !reflect.DeepEqual(resp.Data, expected) {
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
Expand All @@ -714,6 +734,7 @@ const testDynamoPolicy = `{
}
`

const adminAccessPolicyArn = "arn:aws:iam::aws:policy/AdministratorAccess"
const ec2PolicyArn = "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
const iamPolicyArn = "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
const dynamoPolicyArn = "arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess"
Expand Down Expand Up @@ -782,13 +803,14 @@ func TestBackend_iamUserManagedInlinePolicies(t *testing.T) {
"user_path": "/path/",
}
expectedRoleData := map[string]interface{}{
"policy_document": compacted,
"policy_arns": []string{ec2PolicyArn, iamPolicyArn},
"credential_type": iamUserCred,
"role_arns": []string(nil),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "/path/",
"policy_document": compacted,
"policy_arns": []string{ec2PolicyArn, iamPolicyArn},
"credential_type": iamUserCred,
"role_arns": []string(nil),
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "/path/",
"permissions_boundary_arn": "",
}

logicaltest.Test(t, logicaltest.TestCase{
Expand Down Expand Up @@ -986,13 +1008,14 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte
}

expected := map[string]interface{}{
"policy_arns": []string{value},
"role_arns": []string(nil),
"policy_document": "",
"credential_type": iamUserCred,
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "",
"policy_arns": []string{value},
"role_arns": []string(nil),
"policy_document": "",
"credential_type": iamUserCred,
"default_sts_ttl": int64(0),
"max_sts_ttl": int64(0),
"user_path": "",
"permissions_boundary_arn": "",
}
if !reflect.DeepEqual(resp.Data, expected) {
return fmt.Errorf("bad: got: %#v\nexpected: %#v", resp.Data, expected)
Expand Down
54 changes: 47 additions & 7 deletions builtin/logical/aws/path_roles.go
Expand Up @@ -96,6 +96,14 @@ GetFederationToken API call, acting as a filter on permissions available.`,
},
},

"permissions_boundary_arn": &framework.FieldSchema{
Type: framework.TypeString,
Description: "ARN of an IAM policy to attach as a permissions boundary on IAM user credentials; only valid when credential_type is" + iamUserCred,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Permissions Boundary ARN",
},
},

"arn": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Use role_arns or policy_arns instead.`,
Expand Down Expand Up @@ -269,6 +277,13 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f
roleEntry.UserPath = userPathRaw.(string)
}

if permissionsBoundaryARNRaw, ok := d.GetOk("permissions_boundary_arn"); ok {
if legacyRole != "" {
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with permissions_boundary_arn"), nil
}
roleEntry.PermissionsBoundaryARN = permissionsBoundaryARNRaw.(string)
}

if legacyRole != "" {
roleEntry = upgradeLegacyPolicyEntry(legacyRole)
if roleEntry.InvalidData != "" {
Expand Down Expand Up @@ -414,6 +429,20 @@ func upgradeLegacyPolicyEntry(entry string) *awsRoleEntry {
return newRoleEntry
}

func validateAWSManagedPolicy(policyARN string) error {
parsedARN, err := arn.Parse(policyARN)
if err != nil {
return err
}
if parsedARN.Service != "iam" {
return fmt.Errorf("expected a service of iam but got %s", parsedARN.Service)
}
if !strings.HasPrefix(parsedARN.Resource, "policy/") {
return fmt.Errorf("expected a resource type of policy but got %s", parsedARN.Resource)
}
return nil
}

func setAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEntry *awsRoleEntry) error {
if roleName == "" {
return fmt.Errorf("empty role name")
Expand Down Expand Up @@ -445,17 +474,19 @@ type awsRoleEntry struct {
DefaultSTSTTL time.Duration `json:"default_sts_ttl"` // Default TTL for STS credentials
MaxSTSTTL time.Duration `json:"max_sts_ttl"` // Max allowed TTL for STS credentials
UserPath string `json:"user_path"` // The path for the IAM user when using "iam_user" credential type
PermissionsBoundaryARN string `json:"permissions_boundary_arn"` // ARN of an IAM policy to attach as a permissions boundary
}

func (r *awsRoleEntry) toResponseData() map[string]interface{} {
respData := map[string]interface{}{
"credential_type": strings.Join(r.CredentialTypes, ","),
"policy_arns": r.PolicyArns,
"role_arns": r.RoleArns,
"policy_document": r.PolicyDocument,
"default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()),
"max_sts_ttl": int64(r.MaxSTSTTL.Seconds()),
"user_path": r.UserPath,
"credential_type": strings.Join(r.CredentialTypes, ","),
"policy_arns": r.PolicyArns,
"role_arns": r.RoleArns,
"policy_document": r.PolicyDocument,
"default_sts_ttl": int64(r.DefaultSTSTTL.Seconds()),
"max_sts_ttl": int64(r.MaxSTSTTL.Seconds()),
"user_path": r.UserPath,
"permissions_boundary_arn": r.PermissionsBoundaryARN,
}

if r.InvalidData != "" {
Expand Down Expand Up @@ -501,6 +532,15 @@ func (r *awsRoleEntry) validate() error {
}
}

if r.PermissionsBoundaryARN != "" {
if !strutil.StrListContains(r.CredentialTypes, iamUserCred) {
errors = multierror.Append(errors, fmt.Errorf("cannot supply permissions_boundary_arn when credential_type isn't %s", iamUserCred))
}
if err := validateAWSManagedPolicy(r.PermissionsBoundaryARN); err != nil {
errors = multierror.Append(fmt.Errorf("invalid permissions_boundary_arn parameter: %v", err))
}
}

if len(r.RoleArns) > 0 && !strutil.StrListContains(r.CredentialTypes, assumedRoleCred) {
errors = multierror.Append(errors, fmt.Errorf("cannot supply role_arns when credential_type isn't %s", assumedRoleCred))
}
Expand Down
131 changes: 124 additions & 7 deletions builtin/logical/aws/path_roles_test.go
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/hashicorp/vault/sdk/logical"
)

const adminAccessPolicyARN = "arn:aws:iam::aws:policy/AdministratorAccess"

func TestBackend_PathListRoles(t *testing.T) {
var resp *logical.Response
var err error
Expand Down Expand Up @@ -214,10 +216,114 @@ func TestUserPathValidity(t *testing.T) {
}
}

func TestRoleCRUDWithPermissionsBoundary(t *testing.T) {
roleName := "test_perm_boundary"

config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}

b := Backend()
if err := b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}

permissionsBoundaryARN := "arn:aws:iam::aws:policy/EC2FullAccess"

roleData := map[string]interface{}{
"credential_type": iamUserCred,
"policy_arns": []string{adminAccessPolicyARN},
"permissions_boundary_arn": permissionsBoundaryARN,
}
request := &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/" + roleName,
Storage: config.StorageView,
Data: roleData,
}
resp, err := b.HandleRequest(context.Background(), request)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: role creation failed. resp:%#v\nerr:%v", resp, err)
}

request = &logical.Request{
Operation: logical.ReadOperation,
Path: "roles/" + roleName,
Storage: config.StorageView,
}
resp, err = b.HandleRequest(context.Background(), request)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: reading role failed. resp:%#v\nerr:%v", resp, err)
}
if resp.Data["credential_type"] != iamUserCred {
t.Errorf("bad: expected credential_type of %s, got %s instead", iamUserCred, resp.Data["credential_type"])
}
if resp.Data["permissions_boundary_arn"] != permissionsBoundaryARN {
t.Errorf("bad: expected permissions_boundary_arn of %s, got %s instead", permissionsBoundaryARN, resp.Data["permissions_boundary_arn"])
}
}

func TestRoleWithPermissionsBoundaryValidation(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}

b := Backend()
if err := b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}

roleData := map[string]interface{}{
"credential_type": assumedRoleCred, // only iamUserCred supported with permissions_boundary_arn
"role_arns": []string{"arn:aws:iam::123456789012:role/VaultRole"},
"permissions_boundary_arn": "arn:aws:iam::aws:policy/FooBar",
}
request := &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/test_perm_boundary",
Storage: config.StorageView,
Data: roleData,
}
resp, err := b.HandleRequest(context.Background(), request)
if err == nil && (resp == nil || !resp.IsError()) {
t.Fatalf("bad: expected role creation to fail due to bad credential_type, but it didn't. resp:%#v\nerr:%v", resp, err)
}

roleData = map[string]interface{}{
"credential_type": iamUserCred,
"policy_arns": []string{adminAccessPolicyARN},
"permissions_boundary_arn": "arn:aws:notiam::aws:policy/FooBar",
}
request.Data = roleData
resp, err = b.HandleRequest(context.Background(), request)
if err == nil && (resp == nil || !resp.IsError()) {
t.Fatalf("bad: expected role creation to fail due to malformed permissions_boundary_arn, but it didn't. resp:%#v\nerr:%v", resp, err)
}
}

func TestValidateAWSManagedPolicy(t *testing.T) {
expectErr := func(arn string) {
err := validateAWSManagedPolicy(arn)
if err == nil {
t.Errorf("bad: expected arn of %s to return an error but it didn't", arn)
}
}

expectErr("not_an_arn")
expectErr("notarn:aws:iam::aws:policy/FooBar")
expectErr("arn:aws:notiam::aws:policy/FooBar")
expectErr("arn:aws:iam::aws:notpolicy/FooBar")
expectErr("arn:aws:iam::aws:policynot/FooBar")

arn := "arn:aws:iam::aws:policy/FooBar"
err := validateAWSManagedPolicy(arn)
if err != nil {
t.Errorf("bad: expected arn of %s to not return an error but it did: %#v", arn, err)
}
}

func TestRoleEntryValidationCredTypes(t *testing.T) {
roleEntry := awsRoleEntry{
CredentialTypes: []string{},
PolicyArns: []string{"arn:aws:iam::aws:policy/AdministratorAccess"},
PolicyArns: []string{adminAccessPolicyARN},
}
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with no CredentialTypes %#v passed validation", roleEntry)
Expand All @@ -234,10 +340,10 @@ func TestRoleEntryValidationCredTypes(t *testing.T) {

func TestRoleEntryValidationIamUserCred(t *testing.T) {
var allowAllPolicyDocument = `{"Version": "2012-10-17", "Statement": [{"Sid": "AllowAll", "Effect": "Allow", "Action": "*", "Resource": "*"}]}`

roleEntry := awsRoleEntry{
CredentialTypes: []string{iamUserCred},
PolicyArns: []string{"arn:aws:iam::aws:policy/AdministratorAccess"},
CredentialTypes: []string{iamUserCred},
PolicyArns: []string{adminAccessPolicyARN},
PermissionsBoundaryARN: adminAccessPolicyARN,
}
err := roleEntry.validate()
if err != nil {
Expand All @@ -264,7 +370,7 @@ func TestRoleEntryValidationIamUserCred(t *testing.T) {

roleEntry = awsRoleEntry{
CredentialTypes: []string{iamUserCred},
PolicyArns: []string{"arn:aws:iam::aws:policy/AdministratorAccess"},
PolicyArns: []string{adminAccessPolicyARN},
DefaultSTSTTL: 1,
}
if roleEntry.validate() == nil {
Expand All @@ -282,7 +388,7 @@ func TestRoleEntryValidationAssumedRoleCred(t *testing.T) {
roleEntry := awsRoleEntry{
CredentialTypes: []string{assumedRoleCred},
RoleArns: []string{"arn:aws:iam::123456789012:role/SomeRole"},
PolicyArns: []string{"arn:aws:iam::aws:policy/AdministratorAccess"},
PolicyArns: []string{adminAccessPolicyARN},
PolicyDocument: allowAllPolicyDocument,
DefaultSTSTTL: 2,
MaxSTSTTL: 3,
Expand All @@ -300,14 +406,19 @@ func TestRoleEntryValidationAssumedRoleCred(t *testing.T) {
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with unrecognized UserPath %#v passed validation", roleEntry)
}
roleEntry.UserPath = ""
roleEntry.PermissionsBoundaryARN = adminAccessPolicyARN
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with unrecognized PermissionsBoundary %#v passed validation", roleEntry)
}
}

func TestRoleEntryValidationFederationTokenCred(t *testing.T) {
var allowAllPolicyDocument = `{"Version": "2012-10-17", "Statement": [{"Sid": "AllowAll", "Effect": "Allow", "Action": "*", "Resource": "*"}]}`
roleEntry := awsRoleEntry{
CredentialTypes: []string{federationTokenCred},
PolicyDocument: allowAllPolicyDocument,
PolicyArns: []string{"arn:aws:iam::aws:policy/AdministratorAccess"},
PolicyArns: []string{adminAccessPolicyARN},
DefaultSTSTTL: 2,
MaxSTSTTL: 3,
}
Expand All @@ -330,4 +441,10 @@ func TestRoleEntryValidationFederationTokenCred(t *testing.T) {
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with MaxSTSTTL < DefaultSTSTTL %#v passed validation", roleEntry)
}
roleEntry.MaxSTSTTL = 0
roleEntry.PermissionsBoundaryARN = adminAccessPolicyARN
if roleEntry.validate() == nil {
t.Errorf("bad: invalid roleEntry with unrecognized PermissionsBoundary %#v passed validation", roleEntry)
}

}

0 comments on commit 961f446

Please sign in to comment.