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

auth/aws: Make identity alias configurable #5247

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions builtin/credential/aws/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
36 changes: 30 additions & 6 deletions builtin/credential/aws/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{
Expand All @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions builtin/credential/aws/path_config_identity.go
Original file line number Diff line number Diff line change
@@ -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 {
joelthompson marked this conversation as resolved.
Show resolved Hide resolved
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::<account_id>:assumed-role/<role_name>/<role_session_name>
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.
`
90 changes: 90 additions & 0 deletions builtin/credential/aws/path_config_identity_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
24 changes: 22 additions & 2 deletions builtin/credential/aws/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
joelthompson marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -1310,7 +1330,7 @@ func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request,
MaxTTL: roleEntry.MaxTTL,
},
Alias: &logical.Alias{
Name: callerUniqueId,
Name: identityAlias,
},
},
}
Expand Down
69 changes: 69 additions & 0 deletions website/source/api/auth/aws/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
joelthompson marked this conversation as resolved.
Show resolved Hide resolved
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::<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.

### 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
Expand Down