diff --git a/builtin/credential/aws/backend.go b/builtin/credential/aws/backend.go index 42a66652c809..35cd49a72c79 100644 --- a/builtin/credential/aws/backend.go +++ b/builtin/credential/aws/backend.go @@ -116,6 +116,7 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { pathRoleTag(b), pathConfigClient(b), pathConfigCertificate(b), + pathConfigIdentity(b), pathConfigSts(b), pathListSts(b), pathConfigTidyRoletagBlacklist(b), diff --git a/builtin/credential/aws/backend_test.go b/builtin/credential/aws/backend_test.go index 5d494506ace0..5f05594c03c3 100644 --- a/builtin/credential/aws/backend_test.go +++ b/builtin/credential/aws/backend_test.go @@ -1503,16 +1503,17 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { // Test setup largely done // At this point, we're going to: // 1. Configure the client to require our test header value - // 2. Configure two different roles: + // 2. Configure identity to use the ARN for the alias + // 3. Configure two different roles: // a. One bound to our test user // b. One bound to a garbage ARN - // 3. Pass in a request that doesn't have the signed header, ensure + // 4. Pass in a request that doesn't have the signed header, ensure // we're not allowed to login - // 4. Passin a request that has a validly signed header, but the wrong + // 5. Passin a request that has a validly signed header, but the wrong // value, ensure it doesn't allow login - // 5. Pass in a request that has a validly signed request, ensure + // 6. Pass in a request that has a validly signed request, ensure // it allows us to login to our role - // 6. Pass in a request that has a validly signed request, asking for + // 7. Pass in a request that has a validly signed request, asking for // the other role, ensure it fails clientConfigData := map[string]interface{}{ @@ -1529,6 +1530,23 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { t.Fatal(err) } + configIdentityData := map[string]interface{}{ + "iam_alias": identityAliasIAMFullArn, + } + configIdentityRequest := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/identity", + Storage: storage, + Data: configIdentityData, + } + resp, err := b.HandleRequest(context.Background(), configIdentityRequest) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf("received error response when configuring identity: %#v", resp) + } + // configuring the valid role we'll be able to login to roleData := map[string]interface{}{ "bound_iam_principal_arn": []string{entity.canonicalArn(), "arn:aws:iam::123456789012:role/FakeRoleArn1*"}, // Fake ARN MUST be wildcard terminated because we're resolving unique IDs, and the wildcard termination prevents unique ID resolution @@ -1541,7 +1559,7 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { Storage: storage, Data: roleData, } - resp, err := b.HandleRequest(context.Background(), roleRequest) + resp, err = b.HandleRequest(context.Background(), roleRequest) if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: failed to create role: resp:%#v\nerr:%v", resp, err) } @@ -1659,6 +1677,12 @@ func TestBackendAcc_LoginWithCallerIdentity(t *testing.T) { if resp == nil || resp.Auth == nil || resp.IsError() { t.Fatalf("bad: expected valid login: resp:%#v", resp) } + if resp.Auth.Alias == nil { + t.Fatalf("bad: nil auth Alias") + } + if resp.Auth.Alias.Name != *testIdentity.Arn { + t.Fatalf("bad: expected identity alias of %q, got %q instead", *testIdentity.Arn, resp.Auth.Alias.Name) + } renewReq := generateRenewRequest(storage, resp.Auth) // dump a fake ARN into the metadata to ensure that we ONLY look diff --git a/builtin/credential/aws/path_config_identity.go b/builtin/credential/aws/path_config_identity.go new file mode 100644 index 000000000000..ae4c1ad2b39a --- /dev/null +++ b/builtin/credential/aws/path_config_identity.go @@ -0,0 +1,98 @@ +package awsauth + +import ( + "context" + "fmt" + + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfigIdentity(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/identity$", + Fields: map[string]*framework.FieldSchema{ + "iam_alias": &framework.FieldSchema{ + 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), + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: pathConfigIdentityRead, + logical.UpdateOperation: pathConfigIdentityUpdate, + }, + + HelpSynopsis: pathConfigIdentityHelpSyn, + HelpDescription: pathConfigIdentityHelpDesc, + } +} + +func pathConfigIdentityRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + entry, err := req.Storage.Get(ctx, "config/identity") + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + var result identityConfig + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &logical.Response{ + Data: map[string]interface{}{ + "iam_alias": result.IAMAlias, + }, + }, nil +} + +func pathConfigIdentityUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + var configEntry identityConfig + + iamAliasRaw, ok := data.GetOk("iam_alias") + if ok { + iamAlias := iamAliasRaw.(string) + allowedIAMAliasValues := []string{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 + } + 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 + } + } + return nil, nil +} + +type identityConfig struct { + IAMAlias string `json:"iam_alias"` +} + +const identityAliasIAMUniqueID = "unique_id" +const identityAliasIAMFullArn = "full_arn" + +const pathConfigIdentityHelpSyn = ` +Configure the way the AWS auth method interacts with the identity store +` + +const pathConfigIdentityHelpDesc = ` +The AWS auth backend defaults to aliasing an IAM principal's unique ID to the +identity store. This path allows users to change how Vault configures the +mapping to Identity aliases for more flexibility. + +You can set the iam_alias parameter to one of the following values: + +* 'unique_id': This retains Vault's default behavior +* 'full_arn': This maps the full authenticated ARN to the identity alias, e.g., + "arn:aws:sts:::assumed-role// + This is useful where you have an identity provder that sets role_session_name + to a known value of a person, such as a username or email address, and allows + you to map those roles back to entries in your identity store. +` diff --git a/builtin/credential/aws/path_config_identity_test.go b/builtin/credential/aws/path_config_identity_test.go new file mode 100644 index 000000000000..7e72e916a3c8 --- /dev/null +++ b/builtin/credential/aws/path_config_identity_test.go @@ -0,0 +1,90 @@ +package awsauth + +import ( + "context" + "testing" + + "github.com/hashicorp/vault/logical" +) + +func TestBackend_pathConfigIdentity(t *testing.T) { + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b, err := Backend(config) + if err != nil { + t.Fatal(err) + } + + err = b.Setup(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "config/identity", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + 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"]) + } + } + + data := map[string]interface{}{ + "iam_alias": "invalid", + } + + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/identity", + Data: data, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + 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) + } + + data["iam_alias"] = identityAliasIAMFullArn + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/identity", + Data: data, + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.IsError() { + t.Fatalf("received error response from valid config/identity request: %#v", resp) + } + + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "config/identity", + Storage: storage, + }) + if err != nil { + t.Fatal(err) + } + 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) + } +} diff --git a/builtin/credential/aws/path_login.go b/builtin/credential/aws/path_login.go index 0a473809a145..0819a7b273b8 100644 --- a/builtin/credential/aws/path_login.go +++ b/builtin/credential/aws/path_login.go @@ -1114,6 +1114,19 @@ 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 @@ -1181,13 +1194,20 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, // 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: callerUniqueId, + Name: identityAlias, }, }, }, nil @@ -1310,7 +1330,7 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, MaxTTL: roleEntry.MaxTTL, }, Alias: &logical.Alias{ - Name: callerUniqueId, + Name: identityAlias, }, }, } diff --git a/website/source/api/auth/aws/index.html.md b/website/source/api/auth/aws/index.html.md index 0634487caf7d..a44e5b288ee7 100644 --- a/website/source/api/auth/aws/index.html.md +++ b/website/source/api/auth/aws/index.html.md @@ -131,6 +131,75 @@ $ curl \ http://127.0.0.1:8200/v1/auth/aws/config/client ``` +## Configure Identity Integration + +This configures the way that Vault interacts with the +[Identity](/docs/secrets/identity/index.html.md) store. This currently only +configures how identity aliases are generated when using the `iam` auth method. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `POST` | `/auth/aws/config/identity` | `204 (empty body)` | + +### 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 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 + `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. + +### Sample Payload + +```json +{ + "iam_alias": "full_arn" +} +``` + +### Sample Request + +``` +$ curl \ + -- header "X-Vault-Token:..." \ + --request POST + --data @payload.json \ + http://127.0.0.1:8200/v1/auth/aws/config/identity +``` + +## Read Identity Integration Configuration + +Returns the previously configured Identity integration configuration + + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `GET` | `/auth/aws/config/identity` | `200 application/json` | + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token:..." \ + http://127.0.0.1:8200/v1/auth/aws/config/identity +``` + +### Sample Response + +```json +{ + "data": { + "iam_alias": "full_arn" + } +} +``` + ## Create Certificate Configuration Registers an AWS public key to be used to verify the instance identity