Skip to content

Commit

Permalink
auth/aws: Make identity alias configurable (#5247)
Browse files Browse the repository at this point in the history
* auth/aws: Make identity alias configurable

This is inspired by #4178, though not quite exactly what is requested
there. Rather than just use RoleSessionName as the Identity alias, the
full ARN is uses as the Alias. This mitigates against concerns that an
AWS role with an insufficiently secured trust policy could allow an
attacker to generate arbitrary RoleSessionNames in AssumeRole calls to
impersonate anybody in the Identity store that had an alias set up.
By using the full ARN, the owner of the identity store has to explicitly
trust specific AWS roles in specific AWS accounts to generate an
appropriate RoleSessionName to map back to an identity.

Fixes #4178

* Respond to PR feedback

* Remove CreateOperation

Response to PR feedback
  • Loading branch information
joelthompson authored and vishalnayak committed Sep 26, 2018
1 parent 9add4f0 commit d12547c
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 8 deletions.
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 {
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
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`.
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

0 comments on commit d12547c

Please sign in to comment.