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 option to configure ec2_alias values #5846

Merged
merged 13 commits into from
Jan 9, 2019
81 changes: 65 additions & 16 deletions builtin/credential/aws/path_config_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ func pathConfigIdentity(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/identity$",
Fields: map[string]*framework.FieldSchema{
"iam_alias": &framework.FieldSchema{
"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),
},
"ec2_alias": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the above parameter has the old style of declaring the parameter type (&framework.FieldSchema), could we run gofmt -s on this file since we're here anyway? That would clean up the above and any other places that need updating in the file.

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

Callbacks: map[logical.Operation]framework.OperationFunc{
Expand All @@ -30,27 +35,54 @@ func pathConfigIdentity(b *backend) *framework.Path {
}
}

func pathConfigIdentityRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
entry, err := req.Storage.Get(ctx, "config/identity")
func identityConfigEntry(ctx context.Context, s logical.Storage) (*identityConfig, error) {
entryRaw, err := s.Get(ctx, "config/identity")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil

var entry identityConfig
if entryRaw == nil {
entry.IAMAlias = identityAliasIAMUniqueID
entry.EC2Alias = identityAliasEC2InstanceID
return &entry, nil
}
var result identityConfig
if err := entry.DecodeJSON(&result); err != nil {

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

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

if entry.EC2Alias == "" {
entry.EC2Alias = identityAliasEC2InstanceID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should change this incase both end up being "" for some reason, probably break this into two if statements

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}

return &entry, nil
}

func pathConfigIdentityRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := identityConfigEntry(ctx, req.Storage)
if err != nil {
return nil, err
}

return &logical.Response{
Data: map[string]interface{}{
"iam_alias": result.IAMAlias,
"iam_alias": config.IAMAlias,
"ec2_alias": config.EC2Alias,
},
}, nil
}

func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var configEntry identityConfig
config, err := identityConfigEntry(ctx, req.Storage)
if err != nil {
return nil, err
}

iamAliasRaw, ok := data.GetOk("iam_alias")
if ok {
Expand All @@ -59,24 +91,41 @@ func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *f
if !strutil.StrListContains(allowedIAMAliasValues, iamAlias) {
return logical.ErrorResponse(fmt.Sprintf("iam_alias of %q not in set of allowed values: %v", iamAlias, allowedIAMAliasValues)), nil
}
configEntry.IAMAlias = iamAlias
entry, err := logical.StorageEntryJSON("config/identity", configEntry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
config.IAMAlias = iamAlias
}

ec2AliasRaw, ok := data.GetOk("ec2_alias")
if ok {
ec2Alias := ec2AliasRaw.(string)
allowedEC2AliasValues := []string{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
}
config.EC2Alias = ec2Alias
}

entry, err := logical.StorageEntryJSON("config/identity", config)
if err != nil {
return nil, err
}

err = req.Storage.Put(ctx, entry)
if err != nil {
return nil, err
}

return nil, nil
}

type identityConfig struct {
IAMAlias string `json:"iam_alias"`
EC2Alias string `json:"ec2_alias"`
}

const identityAliasIAMUniqueID = "unique_id"
const identityAliasIAMFullArn = "full_arn"
const identityAliasEC2InstanceID = "instance_id"
const identityAliasEC2ImageID = "image_id"

const pathConfigIdentityHelpSyn = `
Configure the way the AWS auth method interacts with the identity store
Expand Down
110 changes: 92 additions & 18 deletions builtin/credential/aws/path_config_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,23 @@ func TestBackend_pathConfigIdentity(t *testing.T) {
t.Fatal(err)
}

// Check if default values are returned before setting the configuration
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "config/identity",
Storage: storage,
})
if err != nil {
t.Fatal(err)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp != nil {
if resp.IsError() {
t.Fatalf("failed to read identity config entry")
} else if resp.Data["iam_alias"] != nil && resp.Data["iam_alias"] != "" {
t.Fatalf("returned alias is non-empty: %q", resp.Data["alias"])
}
if resp.Data["iam_alias"] == nil || resp.Data["iam_alias"] != identityAliasIAMUniqueID {
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 {
t.Fatalf("bad: ec2_alias; expected: %q, actual: %q", identityAliasIAMUniqueID, resp.Data["ec2_alias"])
}

// Invalid value for iam_alias
data := map[string]interface{}{
"iam_alias": "invalid",
}
Expand All @@ -58,7 +59,9 @@ func TestBackend_pathConfigIdentity(t *testing.T) {
t.Fatalf("received non-error response from invalid config/identity request: %#v", resp)
}

// Valid value for iam_alias but invalid value for ec2_alias
data["iam_alias"] = identityAliasIAMFullArn
data["ec2_alias"] = "invalid"
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/identity",
Expand All @@ -68,23 +71,94 @@ func TestBackend_pathConfigIdentity(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if resp != nil && resp.IsError() {
t.Fatalf("received error response from valid config/identity request: %#v", resp)
if resp == nil {
t.Fatalf("nil response from invalid config/identity request")
}
if !resp.IsError() {
t.Fatalf("received non-error response from invalid config/identity request: %#v", resp)
}

// Valid value for both iam_alias and ec2_alias
data["ec2_alias"] = identityAliasEC2ImageID
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/identity",
Data: data,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}

// Check if both values are stored properly
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "config/identity",
Storage: storage,
})
if err != nil {
t.Fatal(err)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp == nil {
t.Fatalf("nil response received from config/identity when data expected")
} else if resp.IsError() {
t.Fatalf("error response received from reading config/identity: %#v", resp)
} else if resp.Data["iam_alias"] != identityAliasIAMFullArn {
t.Fatalf("bad: expected response with iam_alias value of %q; got %#v", identityAliasIAMFullArn, resp)
if resp.Data["iam_alias"] != identityAliasIAMFullArn {
t.Fatalf("bad: expected response with iam_alias value of %q; got %#v", identityAliasIAMFullArn, resp.Data["iam_alias"])
}
if resp.Data["ec2_alias"] != identityAliasEC2ImageID {
t.Fatalf("bad: expected response with ec2_alias value of %q; got %#v", identityAliasEC2ImageID, resp.Data["ec2_alias"])
}

// Modify one field and ensure that the other one is unchanged
data["ec2_alias"] = identityAliasEC2InstanceID
delete(data, "iam_alias")
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/identity",
Data: data,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "config/identity",
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp.Data["iam_alias"] != identityAliasIAMFullArn {
t.Fatalf("bad: expected response with iam_alias value of %q; got %#v", identityAliasIAMFullArn, resp.Data["iam_alias"])
}
if resp.Data["ec2_alias"] != identityAliasEC2InstanceID {
t.Fatalf("bad: expected response with ec2_alias value of %q; got %#v", identityAliasEC2ImageID, resp.Data["ec2_alias"])
}

// Update both iam_alias and ec2_alias
data["iam_alias"] = identityAliasIAMUniqueID
data["ec2_alias"] = identityAliasEC2InstanceID
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/identity",
Data: data,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}

// Check if updates were stored properly
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "config/identity",
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if resp.Data["iam_alias"] != identityAliasIAMUniqueID {
t.Fatalf("bad: expected response with iam_alias value of %q; got %#v", identityAliasIAMFullArn, resp.Data["iam_alias"])
}
if resp.Data["ec2_alias"] != identityAliasEC2InstanceID {
t.Fatalf("bad: expected response with ec2_alias value of %q; got %#v", identityAliasEC2ImageID, resp.Data["ec2_alias"])
}
}
37 changes: 22 additions & 15 deletions builtin/credential/aws/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,12 +589,26 @@ 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So am I right in thinking that this is the default path if neither have been set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! The function identityConfigEntry above will return default values for both EC2Alias and IAMAlias.

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: identityDocParsed.InstanceID,
Name: identityAlias,
},
},
}, nil
Expand Down Expand Up @@ -814,7 +828,7 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request,
MaxTTL: shortestMaxTTL,
},
Alias: &logical.Alias{
Name: identityDocParsed.InstanceID,
Name: identityAlias,
},
},
}
Expand Down Expand Up @@ -1114,19 +1128,6 @@ func (b *backend) pathLoginRenewEc2(ctx context.Context, req *logical.Request, d
}

func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
identityConfigEntryRaw, err := req.Storage.Get(ctx, "config/identity")
if err != nil {
return nil, errwrap.Wrapf("failed to retrieve identity config: {{err}}", err)
}
var identityConfigEntry identityConfig
if identityConfigEntryRaw == nil {
identityConfigEntry.IAMAlias = identityAliasIAMUniqueID
} else {
if err = identityConfigEntryRaw.DecodeJSON(&identityConfigEntry); err != nil {
return nil, errwrap.Wrapf("failed to parse stored config/identity: {{err}}", err)
}
}

method := data.Get("iam_http_request_method").(string)
if method == "" {
return logical.ErrorResponse("missing iam_http_request_method"), nil
Expand Down Expand Up @@ -1191,6 +1192,12 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request,
if 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]
Expand Down
20 changes: 13 additions & 7 deletions website/source/api/auth/aws/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,27 +135,33 @@ $ curl \
## Configure Identity Integration

This configures the way that Vault interacts with the
[Identity](/docs/secrets/identity/index.html) store. This currently only
configures how identity aliases are generated when using the `iam` auth method.
[Identity](/docs/secrets/identity/index.html) store.

| Method | Path | Produces |
| :------- | :--------------------------- | :--------------------- |
| `POST` | `/auth/aws/config/identity` | `204 (empty body)` |

### 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`.
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.
When `full_arn` is selected, the ARN returned by the `sts:GetCallerIdentity`
call is used as the alias. This is either
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
`sts:GetCallerIdentity` call is used as the alias name. This is either
`arn:aws:iam::<account_id>:user/<optional_path/><user_name>` or
`arn:aws:sts::<account_id>:assumed-role/<role_name_without_path>/<role_session_name>`.
**Note**: if you select `full_arn` and then delete and recreate the IAM role,
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.

### Sample Payload

```json
Expand Down