From 47accf80865a03667f3acd1e74fec8f1add4b4ef 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 | 108 +++++++++--------- 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, 120 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d16d68996..121449264 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 6e06b55b3..584c2e65c 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 c08292a12..835bc81a2 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 d06c1b120..09dca1dab 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -589,31 +589,6 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request, } } - identityConfigEntry, err := identityConfigEntry(ctx, req.Storage) - if err != nil { - return nil, err - } - - identityAlias := "" - - switch identityConfigEntry.EC2Alias { - case identityAliasEC2InstanceID: - identityAlias = identityDocParsed.InstanceID - case identityAliasEC2ImageID: - identityAlias = identityDocParsed.AmiID - } - - // If we're just looking up for MFA, return the Alias info - if req.Operation == logical.AliasLookaheadOperation { - return &logical.Response{ - Auth: &logical.Auth{ - Alias: &logical.Alias{ - Name: identityAlias, - }, - }, - }, 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 @@ -634,6 +609,33 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request, 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 + } + + identityAlias := "" + + switch identityConfigEntry.EC2Alias { + case identityAliasRoleID: + identityAlias = roleEntry.RoleID + case identityAliasEC2InstanceID: + identityAlias = identityDocParsed.InstanceID + case identityAliasEC2ImageID: + identityAlias = identityDocParsed.AmiID + } + + // If we're just looking up for MFA, return the Alias info + if req.Operation == logical.AliasLookaheadOperation { + return &logical.Response{ + Auth: &logical.Auth{ + Alias: &logical.Alias{ + Name: identityAlias, + }, + }, + }, 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,33 +1195,6 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %v", err)), nil } - identityConfigEntry, err := identityConfigEntry(ctx, req.Storage) - if err != nil { - return nil, err - } - - // This could either be a "userID:SessionID" (in the case of an assumed role) or just a "userID" - // (in the case of an IAM user). - callerUniqueId := strings.Split(callerID.UserId, ":")[0] - identityAlias := "" - switch identityConfigEntry.IAMAlias { - case identityAliasIAMUniqueID: - identityAlias = callerUniqueId - case identityAliasIAMFullArn: - identityAlias = callerID.Arn - } - - // If we're just looking up for MFA, return the Alias info - if req.Operation == logical.AliasLookaheadOperation { - return &logical.Response{ - Auth: &logical.Auth{ - Alias: &logical.Alias{ - Name: identityAlias, - }, - }, - }, nil - } - entity, err := parseIamArn(callerID.Arn) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error parsing arn %q: %v", callerID.Arn, err)), nil @@ -1242,6 +1217,35 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, 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 + } + + // This could either be a "userID:SessionID" (in the case of an assumed role) or just a "userID" + // (in the case of an IAM user). + callerUniqueId := strings.Split(callerID.UserId, ":")[0] + identityAlias := "" + switch identityConfigEntry.IAMAlias { + case identityAliasRoleID: + identityAlias = roleEntry.RoleID + case identityAliasIAMUniqueID: + identityAlias = callerUniqueId + case identityAliasIAMFullArn: + identityAlias = callerID.Arn + } + + // If we're just looking up for MFA, return the Alias info + if req.Operation == logical.AliasLookaheadOperation { + return &logical.Response{ + Auth: &logical.Auth{ + Alias: &logical.Alias{ + Name: identityAlias, + }, + }, + }, 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 f35175212..d8330a965 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 d14b59357..65462b8b2 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 2085c40fe..4807b2b65 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" } ```