From b6f40b5fec5435fccc218c61082026cd875f02d6 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 30 Jan 2019 15:46:43 -0500 Subject: [PATCH] Add role_id as an alias name source for AWS and change the defaults --- CHANGELOG.md | 3 + .../credential/aws/path_config_identity.go | 26 +++--- .../aws/path_config_identity_test.go | 4 +- builtin/credential/aws/path_login.go | 88 ++++++++++--------- builtin/credential/aws/path_role.go | 26 ++++-- builtin/credential/aws/path_role_test.go | 20 +++-- website/source/api/auth/aws/index.html.md | 21 +++-- 7 files changed, 110 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d16d68996f5d..121449264ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/builtin/credential/aws/path_config_identity.go b/builtin/credential/aws/path_config_identity.go index 6e06b55b379a..584c2e65c656 100644 --- a/builtin/credential/aws/path_config_identity.go +++ b/builtin/credential/aws/path_config_identity.go @@ -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 diff --git a/builtin/credential/aws/path_config_identity_test.go b/builtin/credential/aws/path_config_identity_test.go index c08292a12da1..835bc81a2ac0 100644 --- a/builtin/credential/aws/path_config_identity_test.go +++ b/builtin/credential/aws/path_config_identity_test.go @@ -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"]) } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index d06c1b120322..09dca1dabc1c 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -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 { diff --git a/builtin/credential/aws/path_role.go b/builtin/credential/aws/path_role.go index f35175212a18..d8330a965317 100644 --- a/builtin/credential/aws/path_role.go +++ b/builtin/credential/aws/path_role.go @@ -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, diff --git a/builtin/credential/aws/path_role_test.go b/builtin/credential/aws/path_role_test.go index d14b59357f75..65462b8b21fa 100644 --- a/builtin/credential/aws/path_role_test.go +++ b/builtin/credential/aws/path_role_test.go @@ -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) } } diff --git a/website/source/api/auth/aws/index.html.md b/website/source/api/auth/aws/index.html.md index 2085c40fe388..4807b2b6500e 100644 --- a/website/source/api/auth/aws/index.html.md +++ b/website/source/api/auth/aws/index.html.md @@ -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" } ```