From 77978055fec9ae786ff08ae12261ee0c3ae1d9f2 Mon Sep 17 00:00:00 2001 From: Vishal Nayak Date: Wed, 9 Jan 2019 18:28:29 -0500 Subject: [PATCH] Add option to configure ec2_alias values (#5846) * 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 --- .../credential/aws/path_config_identity.go | 83 ++++++++++--- .../aws/path_config_identity_test.go | 110 +++++++++++++++--- builtin/credential/aws/path_login.go | 37 +++--- website/source/api/auth/aws/index.html.md | 20 ++-- 4 files changed, 193 insertions(+), 57 deletions(-) diff --git a/builtin/credential/aws/path_config_identity.go b/builtin/credential/aws/path_config_identity.go index ae4c1ad2b3..6e06b55b37 100644 --- a/builtin/credential/aws/path_config_identity.go +++ b/builtin/credential/aws/path_config_identity.go @@ -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{ @@ -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 { @@ -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 diff --git a/builtin/credential/aws/path_config_identity_test.go b/builtin/credential/aws/path_config_identity_test.go index 7e72e916a3..c08292a12d 100644 --- a/builtin/credential/aws/path_config_identity_test.go +++ b/builtin/credential/aws/path_config_identity_test.go @@ -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", } @@ -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", @@ -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"]) } } diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 32f0993f2f..d06c1b1203 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -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 @@ -814,7 +828,7 @@ func (b *backend) pathLoginUpdateEc2(ctx context.Context, req *logical.Request, MaxTTL: shortestMaxTTL, }, Alias: &logical.Alias{ - Name: identityDocParsed.InstanceID, + Name: identityAlias, }, }, } @@ -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 @@ -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] diff --git a/website/source/api/auth/aws/index.html.md b/website/source/api/auth/aws/index.html.md index 3871140f3d..2085c40fe3 100644 --- a/website/source/api/auth/aws/index.html.md +++ b/website/source/api/auth/aws/index.html.md @@ -135,8 +135,7 @@ $ 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 | | :------- | :--------------------------- | :--------------------- | @@ -144,18 +143,25 @@ configures how identity aliases are generated when using the `iam` auth method. ### 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:::user/` or `arn:aws:sts:::assumed-role//`. **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