Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add role_id as an alias name source for AWS and change the defaults #6133

Merged
merged 1 commit into from
Jan 30, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@

CHANGES:

* New AWS authentication plugin mounts will default to using the generated
role ID as the Identity alias name. This applies to both EC2 and IAM auth.
Existing mounts will not be affected.
* The default policy now allows a token to look up its associated identity
entity either by name or by id [GH-6105]

26 changes: 11 additions & 15 deletions builtin/credential/aws/path_config_identity.go
Original file line number Diff line number Diff line change
@@ -16,12 +16,12 @@ func pathConfigIdentity(b *backend) *framework.Path {
"iam_alias": {
Type: framework.TypeString,
Default: identityAliasIAMUniqueID,
Description: fmt.Sprintf("Configure how the AWS auth method generates entity aliases when using IAM auth. Valid values are %q and %q", identityAliasIAMUniqueID, identityAliasIAMFullArn),
Description: fmt.Sprintf("Configure how the AWS auth method generates entity aliases when using IAM auth. Valid values are %q, %q, and %q. Defaults to %q.", identityAliasRoleID, identityAliasIAMUniqueID, identityAliasIAMFullArn, identityAliasRoleID),
},
"ec2_alias": {
Type: framework.TypeString,
Default: identityAliasEC2InstanceID,
Description: fmt.Sprintf("Configure how the AWS auth method generates entity alias when using EC2 auth. Valid values are %q and %q", identityAliasEC2InstanceID, identityAliasEC2ImageID),
Description: fmt.Sprintf("Configure how the AWS auth method generates entity alias when using EC2 auth. Valid values are %q, %q, and %q. Defaults ot %q.", identityAliasRoleID, identityAliasEC2InstanceID, identityAliasEC2ImageID, identityAliasRoleID),
},
},

@@ -42,23 +42,18 @@ func identityConfigEntry(ctx context.Context, s logical.Storage) (*identityConfi
}

var entry identityConfig
if entryRaw == nil {
entry.IAMAlias = identityAliasIAMUniqueID
entry.EC2Alias = identityAliasEC2InstanceID
return &entry, nil
}

err = entryRaw.DecodeJSON(&entry)
if err != nil {
return nil, err
if entryRaw != nil {
if err := entryRaw.DecodeJSON(&entry); err != nil {
return nil, err
}
}

if entry.IAMAlias == "" {
entry.IAMAlias = identityAliasIAMUniqueID
entry.IAMAlias = identityAliasRoleID
}

if entry.EC2Alias == "" {
entry.EC2Alias = identityAliasEC2InstanceID
entry.EC2Alias = identityAliasRoleID
}

return &entry, nil
@@ -87,7 +82,7 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f
iamAliasRaw, ok := data.GetOk("iam_alias")
if ok {
iamAlias := iamAliasRaw.(string)
allowedIAMAliasValues := []string{identityAliasIAMUniqueID, identityAliasIAMFullArn}
allowedIAMAliasValues := []string{identityAliasRoleID, identityAliasIAMUniqueID, identityAliasIAMFullArn}
if !strutil.StrListContains(allowedIAMAliasValues, iamAlias) {
return logical.ErrorResponse(fmt.Sprintf("iam_alias of %q not in set of allowed values: %v", iamAlias, allowedIAMAliasValues)), nil
}
@@ -97,7 +92,7 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f
ec2AliasRaw, ok := data.GetOk("ec2_alias")
if ok {
ec2Alias := ec2AliasRaw.(string)
allowedEC2AliasValues := []string{identityAliasEC2InstanceID, identityAliasEC2ImageID}
allowedEC2AliasValues := []string{identityAliasRoleID, identityAliasEC2InstanceID, identityAliasEC2ImageID}
if !strutil.StrListContains(allowedEC2AliasValues, ec2Alias) {
return logical.ErrorResponse(fmt.Sprintf("ec2_alias of %q not in set of allowed values: %v", ec2Alias, allowedEC2AliasValues)), nil
}
@@ -126,6 +121,7 @@ const identityAliasIAMUniqueID = "unique_id"
const identityAliasIAMFullArn = "full_arn"
const identityAliasEC2InstanceID = "instance_id"
const identityAliasEC2ImageID = "image_id"
const identityAliasRoleID = "role_id"

const pathConfigIdentityHelpSyn = `
Configure the way the AWS auth method interacts with the identity store
4 changes: 2 additions & 2 deletions builtin/credential/aws/path_config_identity_test.go
Original file line number Diff line number Diff line change
@@ -31,10 +31,10 @@ func TestBackend_pathConfigIdentity(t *testing.T) {
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp.Data["iam_alias"] == nil || resp.Data["iam_alias"] != identityAliasIAMUniqueID {
if resp.Data["iam_alias"] == nil || resp.Data["iam_alias"] != identityAliasRoleID {
t.Fatalf("bad: iam_alias; expected: %q, actual: %q", identityAliasIAMUniqueID, resp.Data["iam_alias"])
}
if resp.Data["ec2_alias"] == nil || resp.Data["ec2_alias"] != identityAliasEC2InstanceID {
if resp.Data["ec2_alias"] == nil || resp.Data["ec2_alias"] != identityAliasRoleID {
t.Fatalf("bad: ec2_alias; expected: %q, actual: %q", identityAliasIAMUniqueID, resp.Data["ec2_alias"])
}

88 changes: 46 additions & 42 deletions builtin/credential/aws/path_login.go
Original file line number Diff line number Diff line change
@@ -589,6 +589,26 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request,
}
}

roleName := data.Get("role").(string)

// If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for
if roleName == "" {
roleName = identityDocParsed.AmiID
}

// Get the entry for the role used by the instance
roleEntry, err := b.lockedAWSRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil
}

if roleEntry.AuthType != ec2AuthType {
return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil
}

identityConfigEntry, err := identityConfigEntry(ctx, req.Storage)
if err != nil {
return nil, err
@@ -597,6 +617,8 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request,
identityAlias := ""

switch identityConfigEntry.EC2Alias {
case identityAliasRoleID:
identityAlias = roleEntry.RoleID
case identityAliasEC2InstanceID:
identityAlias = identityDocParsed.InstanceID
case identityAliasEC2ImageID:
@@ -614,26 +636,6 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request,
}, nil
}

roleName := data.Get("role").(string)

// If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for
if roleName == "" {
roleName = identityDocParsed.AmiID
}

// Get the entry for the role used by the instance
roleEntry, err := b.lockedAWSRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil
}

if roleEntry.AuthType != ec2AuthType {
return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil
}

// 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.
@@ -1193,6 +1195,28 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request,
return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %v", err)), nil
}

entity, err := parseIamArn(callerID.Arn)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("error parsing arn %q: %v", callerID.Arn, err)), nil
}

roleName := data.Get("role").(string)
if roleName == "" {
roleName = entity.FriendlyName
}

roleEntry, err := b.lockedAWSRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil
}

if roleEntry.AuthType != iamAuthType {
return logical.ErrorResponse(fmt.Sprintf("auth method iam not allowed for role %s", roleName)), nil
}

identityConfigEntry, err := identityConfigEntry(ctx, req.Storage)
if err != nil {
return nil, err
@@ -1203,6 +1227,8 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request,
callerUniqueId := strings.Split(callerID.UserId, ":")[0]
identityAlias := ""
switch identityConfigEntry.IAMAlias {
case identityAliasRoleID:
identityAlias = roleEntry.RoleID
case identityAliasIAMUniqueID:
identityAlias = callerUniqueId
case identityAliasIAMFullArn:
@@ -1220,28 +1246,6 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request,
}, nil
}

entity, err := parseIamArn(callerID.Arn)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("error parsing arn %q: %v", callerID.Arn, err)), nil
}

roleName := data.Get("role").(string)
if roleName == "" {
roleName = entity.FriendlyName
}

roleEntry, err := b.lockedAWSRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil
}

if roleEntry.AuthType != iamAuthType {
return logical.ErrorResponse(fmt.Sprintf("auth method iam not allowed for role %s", roleName)), nil
}

// The role creation should ensure that either we're inferring this is an EC2 instance
// or that we're binding an ARN
if len(roleEntry.BoundIamPrincipalARNs) > 0 {
26 changes: 21 additions & 5 deletions builtin/credential/aws/path_role.go
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ import (
)

var (
currentRoleStorageVersion = 2
currentRoleStorageVersion = 3
)

func pathRole(b *backend) *framework.Path {
@@ -391,8 +391,8 @@ func (b *backend) upgradeRoleEntry(ctx context.Context, s logical.Storage, roleE
roleEntry.BoundVpcIDs = []string{roleEntry.BoundVpcID}
roleEntry.BoundVpcID = ""
}
roleEntry.Version = 1
fallthrough

case 1:
// Make BoundIamRoleARNs and BoundIamInstanceProfileARNs explicitly prefix-matched
for i, arn := range roleEntry.BoundIamRoleARNs {
@@ -401,15 +401,24 @@ func (b *backend) upgradeRoleEntry(ctx context.Context, s logical.Storage, roleE
for i, arn := range roleEntry.BoundIamInstanceProfileARNs {
roleEntry.BoundIamInstanceProfileARNs[i] = fmt.Sprintf("%s*", arn)
}
roleEntry.Version = 2
fallthrough

case 2:
roleID, err := uuid.GenerateUUID()
if err != nil {
return false, err
}
roleEntry.RoleID = roleID
fallthrough

case currentRoleStorageVersion:
roleEntry.Version = currentRoleStorageVersion

default:
return false, fmt.Errorf("unrecognized role version: %q", roleEntry.Version)
}

return upgraded, nil

}

// nonLockedAWSRole returns the properties set on the given role. This method
@@ -494,7 +503,12 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request
return nil, err
}
if roleEntry == nil {
roleID, err := uuid.GenerateUUID()
if err != nil {
return nil, err
}
roleEntry = &awsRoleEntry{
RoleID: roleID,
Version: currentRoleStorageVersion,
}
} else {
@@ -807,7 +821,8 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request

// Struct to hold the information associated with a Vault role
type awsRoleEntry struct {
AuthType string `json:"auth_type" `
RoleID string `json:"role_id"`
AuthType string `json:"auth_type"`
BoundAmiIDs []string `json:"bound_ami_id_list"`
BoundAccountIDs []string `json:"bound_account_id_list"`
BoundEc2InstanceIDs []string `json:"bound_ec2_instance_id_list"`
@@ -858,6 +873,7 @@ func (r *awsRoleEntry) ToResponseData() map[string]interface{} {
"inferred_entity_type": r.InferredEntityType,
"inferred_aws_region": r.InferredAWSRegion,
"resolve_aws_unique_ids": r.ResolveAWSUniqueIDs,
"role_id": r.RoleID,
"role_tag": r.RoleTag,
"allow_instance_migration": r.AllowInstanceMigration,
"ttl": r.TTL / time.Second,
20 changes: 15 additions & 5 deletions builtin/credential/aws/path_role_test.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/go-test/deep"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
@@ -620,8 +621,13 @@ func TestAwsEc2_RoleCrud(t *testing.T) {
"period": time.Duration(60),
}

if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data)
if resp.Data["role_id"] == nil {
t.Fatal("role_id not found in repsonse")
}
expected["role_id"] = resp.Data["role_id"]

if diff := deep.Equal(expected, resp.Data); diff != nil {
t.Fatal(diff)
}

roleData["bound_vpc_id"] = "newvpcid"
@@ -711,7 +717,7 @@ func TestAwsEc2_RoleDurationSeconds(t *testing.T) {
}
}

func TestRoleEntryUpgradeV1(t *testing.T) {
func TestRoleEntryUpgradeV(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
@@ -743,8 +749,12 @@ func TestRoleEntryUpgradeV1(t *testing.T) {
if !upgraded {
t.Fatalf("expected to upgrade role entry %#v but got no upgrade", roleEntryToUpgrade)
}
if !reflect.DeepEqual(*roleEntryToUpgrade, *expected) {
t.Fatalf("bad: expected upgraded role of %#v, got %#v instead", expected, roleEntryToUpgrade)
if roleEntryToUpgrade.RoleID == "" {
t.Fatal("expected role ID to be populated")
}
expected.RoleID = roleEntryToUpgrade.RoleID
if diff := deep.Equal(*roleEntryToUpgrade, *expected); diff != nil {
t.Fatal(diff)
}
}

21 changes: 12 additions & 9 deletions website/source/api/auth/aws/index.html.md
Original file line number Diff line number Diff line change
@@ -135,7 +135,8 @@ $ curl \
## Configure Identity Integration

This configures the way that Vault interacts with the
[Identity](/docs/secrets/identity/index.html) store.
[Identity](/docs/secrets/identity/index.html) store. The default (as of Vault
1.0.3) is `role_id` for both values.

| Method | Path | Produces |
| :------- | :--------------------------- | :--------------------- |
@@ -144,8 +145,9 @@ This configures the way that Vault interacts with the
### Parameters

- `iam_alias` `(string: "unique_id")` - How to generate the identity alias when
using the `iam` auth method. Valid choices are `unique_id` and `full_arn`.
When `unique_id` is selected, the [IAM Unique
using the `iam` auth method. Valid choices are `role_id`, `unique_id`, and
`full_arn` When `role_id` is selected, the randomly generated ID of the role
is used. When `unique_id` is selected, the [IAM Unique
ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids)
of the IAM principal (either the user or role) is used as the identity alias
name. When `full_arn` is selected, the ARN returned by the
@@ -156,17 +158,18 @@ This configures the way that Vault interacts with the
Vault won't be aware and any identity aliases set up for the role name will
still be valid.

- `ec2_alias (string: "instance_id")` - Configures how to generate the identity alias when
using the `ec2` auth method. Valid choices are `instance_id` and `image_id`.
When `instance_id` is selected, the instance identifier is used as the
identity alias name. When `image_id` is selected, AMI ID of the instance is
used as the identity alias name.
- `ec2_alias (string: "instance_id")` - Configures how to generate the identity
alias when using the `ec2` auth method. Valid choices are `role_id`,
`instance_id`, and `image_id`. When `role_id` is selected, the randomly
generated ID of the role is used. When `instance_id` is selected, the
instance identifier is used as the identity alias name. When `image_id` is
selected, AMI ID of the instance is used as the identity alias name.

### Sample Payload

```json
{
"iam_alias": "full_arn"
"iam_alias": "unique_id"
}
```