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

This commit is contained in:
Jeff Mitchell 2019-01-30 15:46:43 -05:00
parent 4363453017
commit 47accf8086
7 changed files with 120 additions and 88 deletions

View File

@ -2,6 +2,9 @@
CHANGES: 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 * The default policy now allows a token to look up its associated identity
entity either by name or by id [GH-6105] entity either by name or by id [GH-6105]

View File

@ -16,12 +16,12 @@ func pathConfigIdentity(b *backend) *framework.Path {
"iam_alias": { "iam_alias": {
Type: framework.TypeString, Type: framework.TypeString,
Default: identityAliasIAMUniqueID, 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": { "ec2_alias": {
Type: framework.TypeString, Type: framework.TypeString,
Default: identityAliasEC2InstanceID, 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 var entry identityConfig
if entryRaw == nil { if entryRaw != nil {
entry.IAMAlias = identityAliasIAMUniqueID if err := entryRaw.DecodeJSON(&entry); err != nil {
entry.EC2Alias = identityAliasEC2InstanceID return nil, err
return &entry, nil }
}
err = entryRaw.DecodeJSON(&entry)
if err != nil {
return nil, err
} }
if entry.IAMAlias == "" { if entry.IAMAlias == "" {
entry.IAMAlias = identityAliasIAMUniqueID entry.IAMAlias = identityAliasRoleID
} }
if entry.EC2Alias == "" { if entry.EC2Alias == "" {
entry.EC2Alias = identityAliasEC2InstanceID entry.EC2Alias = identityAliasRoleID
} }
return &entry, nil return &entry, nil
@ -87,7 +82,7 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f
iamAliasRaw, ok := data.GetOk("iam_alias") iamAliasRaw, ok := data.GetOk("iam_alias")
if ok { if ok {
iamAlias := iamAliasRaw.(string) iamAlias := iamAliasRaw.(string)
allowedIAMAliasValues := []string{identityAliasIAMUniqueID, identityAliasIAMFullArn} allowedIAMAliasValues := []string{identityAliasRoleID, identityAliasIAMUniqueID, identityAliasIAMFullArn}
if !strutil.StrListContains(allowedIAMAliasValues, iamAlias) { if !strutil.StrListContains(allowedIAMAliasValues, iamAlias) {
return logical.ErrorResponse(fmt.Sprintf("iam_alias of %q not in set of allowed values: %v", iamAlias, allowedIAMAliasValues)), nil 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") ec2AliasRaw, ok := data.GetOk("ec2_alias")
if ok { if ok {
ec2Alias := ec2AliasRaw.(string) ec2Alias := ec2AliasRaw.(string)
allowedEC2AliasValues := []string{identityAliasEC2InstanceID, identityAliasEC2ImageID} allowedEC2AliasValues := []string{identityAliasRoleID, identityAliasEC2InstanceID, identityAliasEC2ImageID}
if !strutil.StrListContains(allowedEC2AliasValues, ec2Alias) { if !strutil.StrListContains(allowedEC2AliasValues, ec2Alias) {
return logical.ErrorResponse(fmt.Sprintf("ec2_alias of %q not in set of allowed values: %v", ec2Alias, allowedEC2AliasValues)), nil 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 identityAliasIAMFullArn = "full_arn"
const identityAliasEC2InstanceID = "instance_id" const identityAliasEC2InstanceID = "instance_id"
const identityAliasEC2ImageID = "image_id" const identityAliasEC2ImageID = "image_id"
const identityAliasRoleID = "role_id"
const pathConfigIdentityHelpSyn = ` const pathConfigIdentityHelpSyn = `
Configure the way the AWS auth method interacts with the identity store Configure the way the AWS auth method interacts with the identity store

View File

@ -31,10 +31,10 @@ func TestBackend_pathConfigIdentity(t *testing.T) {
if err != nil || (resp != nil && resp.IsError()) { if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp) 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"]) 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"]) t.Fatalf("bad: ec2_alias; expected: %q, actual: %q", identityAliasIAMUniqueID, resp.Data["ec2_alias"])
} }

View File

@ -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) 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 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 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 // Validate the instance ID by making a call to AWS EC2 DescribeInstances API
// and fetching the instance description. Validation succeeds only if the // and fetching the instance description. Validation succeeds only if the
// instance is in 'running' state. // 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 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) entity, err := parseIamArn(callerID.Arn)
if err != nil { if err != nil {
return logical.ErrorResponse(fmt.Sprintf("error parsing arn %q: %v", callerID.Arn, 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 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 // The role creation should ensure that either we're inferring this is an EC2 instance
// or that we're binding an ARN // or that we're binding an ARN
if len(roleEntry.BoundIamPrincipalARNs) > 0 { if len(roleEntry.BoundIamPrincipalARNs) > 0 {

View File

@ -15,7 +15,7 @@ import (
) )
var ( var (
currentRoleStorageVersion = 2 currentRoleStorageVersion = 3
) )
func pathRole(b *backend) *framework.Path { 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.BoundVpcIDs = []string{roleEntry.BoundVpcID}
roleEntry.BoundVpcID = "" roleEntry.BoundVpcID = ""
} }
roleEntry.Version = 1
fallthrough fallthrough
case 1: case 1:
// Make BoundIamRoleARNs and BoundIamInstanceProfileARNs explicitly prefix-matched // Make BoundIamRoleARNs and BoundIamInstanceProfileARNs explicitly prefix-matched
for i, arn := range roleEntry.BoundIamRoleARNs { 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 { for i, arn := range roleEntry.BoundIamInstanceProfileARNs {
roleEntry.BoundIamInstanceProfileARNs[i] = fmt.Sprintf("%s*", arn) roleEntry.BoundIamInstanceProfileARNs[i] = fmt.Sprintf("%s*", arn)
} }
roleEntry.Version = 2
fallthrough fallthrough
case 2:
roleID, err := uuid.GenerateUUID()
if err != nil {
return false, err
}
roleEntry.RoleID = roleID
fallthrough
case currentRoleStorageVersion: case currentRoleStorageVersion:
roleEntry.Version = currentRoleStorageVersion
default: default:
return false, fmt.Errorf("unrecognized role version: %q", roleEntry.Version) return false, fmt.Errorf("unrecognized role version: %q", roleEntry.Version)
} }
return upgraded, nil return upgraded, nil
} }
// nonLockedAWSRole returns the properties set on the given role. This method // 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 return nil, err
} }
if roleEntry == nil { if roleEntry == nil {
roleID, err := uuid.GenerateUUID()
if err != nil {
return nil, err
}
roleEntry = &awsRoleEntry{ roleEntry = &awsRoleEntry{
RoleID: roleID,
Version: currentRoleStorageVersion, Version: currentRoleStorageVersion,
} }
} else { } 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 // Struct to hold the information associated with a Vault role
type awsRoleEntry struct { 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"` BoundAmiIDs []string `json:"bound_ami_id_list"`
BoundAccountIDs []string `json:"bound_account_id_list"` BoundAccountIDs []string `json:"bound_account_id_list"`
BoundEc2InstanceIDs []string `json:"bound_ec2_instance_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_entity_type": r.InferredEntityType,
"inferred_aws_region": r.InferredAWSRegion, "inferred_aws_region": r.InferredAWSRegion,
"resolve_aws_unique_ids": r.ResolveAWSUniqueIDs, "resolve_aws_unique_ids": r.ResolveAWSUniqueIDs,
"role_id": r.RoleID,
"role_tag": r.RoleTag, "role_tag": r.RoleTag,
"allow_instance_migration": r.AllowInstanceMigration, "allow_instance_migration": r.AllowInstanceMigration,
"ttl": r.TTL / time.Second, "ttl": r.TTL / time.Second,

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/go-test/deep"
"github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical"
@ -620,8 +621,13 @@ func TestAwsEc2_RoleCrud(t *testing.T) {
"period": time.Duration(60), "period": time.Duration(60),
} }
if !reflect.DeepEqual(expected, resp.Data) { if resp.Data["role_id"] == nil {
t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data) 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" 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() config := logical.TestBackendConfig()
storage := &logical.InmemStorage{} storage := &logical.InmemStorage{}
config.StorageView = storage config.StorageView = storage
@ -743,8 +749,12 @@ func TestRoleEntryUpgradeV1(t *testing.T) {
if !upgraded { if !upgraded {
t.Fatalf("expected to upgrade role entry %#v but got no upgrade", roleEntryToUpgrade) t.Fatalf("expected to upgrade role entry %#v but got no upgrade", roleEntryToUpgrade)
} }
if !reflect.DeepEqual(*roleEntryToUpgrade, *expected) { if roleEntryToUpgrade.RoleID == "" {
t.Fatalf("bad: expected upgraded role of %#v, got %#v instead", expected, roleEntryToUpgrade) t.Fatal("expected role ID to be populated")
}
expected.RoleID = roleEntryToUpgrade.RoleID
if diff := deep.Equal(*roleEntryToUpgrade, *expected); diff != nil {
t.Fatal(diff)
} }
} }

View File

@ -135,7 +135,8 @@ $ curl \
## Configure Identity Integration ## Configure Identity Integration
This configures the way that Vault interacts with the 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 | | Method | Path | Produces |
| :------- | :--------------------------- | :--------------------- | | :------- | :--------------------------- | :--------------------- |
@ -144,8 +145,9 @@ This configures the way that Vault interacts with the
### Parameters ### Parameters
- `iam_alias` `(string: "unique_id")` - How to generate the identity alias when - `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`. using the `iam` auth method. Valid choices are `role_id`, `unique_id`, and
When `unique_id` is selected, the [IAM Unique `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) 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 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 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 Vault won't be aware and any identity aliases set up for the role name will
still be valid. still be valid.
- `ec2_alias (string: "instance_id")` - Configures how to generate the identity alias when - `ec2_alias (string: "instance_id")` - Configures how to generate the identity
using the `ec2` auth method. Valid choices are `instance_id` and `image_id`. alias when using the `ec2` auth method. Valid choices are `role_id`,
When `instance_id` is selected, the instance identifier is used as the `instance_id`, and `image_id`. When `role_id` is selected, the randomly
identity alias name. When `image_id` is selected, AMI ID of the instance is generated ID of the role is used. When `instance_id` is selected, the
used as the identity alias name. 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 ### Sample Payload
```json ```json
{ {
"iam_alias": "full_arn" "iam_alias": "unique_id"
} }
``` ```