Skip to content

Commit

Permalink
Add option to configure ec2_alias values (#5846)
Browse files Browse the repository at this point in the history
* Add option to configure ec2_alias values

* Doc updates

* Fix overwriting of previous config value

* s/configEntry/config

* Fix formatting

* Address review feedback

* Address review feedback
  • Loading branch information
vishalnayak authored Jan 9, 2019
1 parent ba2d6bd commit 7797805
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 56 deletions.
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": {
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
}

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

0 comments on commit 7797805

Please sign in to comment.