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:
* 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]

View File

@ -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 {
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

View File

@ -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"])
}

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)
// 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 {

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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"
}
```