Adds sts:AssumeRole support to the AWS secret backend

Support use cases where you want to provision STS tokens
using Vault, but, you need to call AWS APIs that are blocked
for federated tokens.  For example, STS federated tokens cannot
invoke IAM APIs, such as  Terraform scripts containing
`aws_iam_*` resources.
This commit is contained in:
Steve Jansen 2016-04-11 22:30:30 -04:00
parent 902b2c4c72
commit 69740e57e0
4 changed files with 386 additions and 51 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt"
"log"
"os"
"strings"
"testing"
"time"
@ -13,6 +14,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/logical"
logicaltest "github.com/hashicorp/vault/logical/testing"
@ -40,15 +42,21 @@ func TestBackend_basic(t *testing.T) {
func TestBackend_basicSTS(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() { testAccPreCheck(t) },
Backend: getBackend(t),
PreCheck: func() {
testAccPreCheck(t)
createRole(t)
},
Backend: getBackend(t),
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepWritePolicy(t, "test", testPolicy),
testAccStepReadSTS(t, "test"),
testAccStepWriteArnPolicyRef(t, "test", testPolicyArn),
testAccStepReadSTSWithArnPolicy(t, "test"),
testAccStepWriteArnRoleRef(t, testRoleName),
testAccStepReadSTS(t, testRoleName),
},
Teardown: teardown,
})
}
@ -84,6 +92,123 @@ func testAccPreCheck(t *testing.T) {
log.Println("[INFO] Test: Using us-west-2 as test region")
os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
}
if v := os.Getenv("AWS_ACCOUNT_ID"); v == "" {
accountId, err := getAccountId()
if err != nil {
t.Fatal("AWS_ACCOUNT_ID could not be read from iam:GetUser for acceptance tests")
}
log.Printf("[INFO] Test: Used %s as AWS_ACCOUNT_ID", accountId)
os.Setenv("AWS_ACCOUNT_ID", accountId)
}
}
func getAccountId() (string, error) {
creds := credentials.NewStaticCredentials(os.Getenv("AWS_ACCESS_KEY_ID"),
os.Getenv("AWS_SECRET_ACCESS_KEY"),
"")
awsConfig := &aws.Config{
Credentials: creds,
Region: aws.String("us-east-1"),
HTTPClient: cleanhttp.DefaultClient(),
}
svc := iam.New(session.New(awsConfig))
params := &iam.GetUserInput{}
res, err := svc.GetUser(params)
if err != nil {
return "", err
}
// split "arn:aws:iam::012345678912:user/username"
accountId := strings.Split(*res.User.Arn, ":")[4]
return accountId, nil
}
const testRoleName = "Vault-Acceptance-Test-AWS-Assume-Role"
func createRole(t *testing.T) {
const testRoleAssumePolicy = `{
"Version": "2012-10-17",
"Statement": [
{
"Effect":"Allow",
"Principal": {
"AWS": "arn:aws:iam::%s:root"
},
"Action": "sts:AssumeRole"
}
]
}
`
creds := credentials.NewStaticCredentials(os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY"), "")
awsConfig := &aws.Config{
Credentials: creds,
Region: aws.String("us-east-1"),
HTTPClient: cleanhttp.DefaultClient(),
}
svc := iam.New(session.New(awsConfig))
trustPolicy := fmt.Sprintf(testRoleAssumePolicy, os.Getenv("AWS_ACCOUNT_ID"))
params := &iam.CreateRoleInput{
AssumeRolePolicyDocument: aws.String(trustPolicy),
RoleName: aws.String(testRoleName),
Path: aws.String("/"),
}
log.Printf("[INFO] AWS CreateRole: %s", testRoleName)
_, err := svc.CreateRole(params)
if err != nil {
t.Fatal("AWS CreateRole failed: %v", err)
}
attachment := &iam.AttachRolePolicyInput{
PolicyArn: aws.String(testPolicyArn),
RoleName: aws.String(testRoleName), // Required
}
_, err = svc.AttachRolePolicy(attachment)
if err != nil {
t.Fatal("AWS CreateRole failed: %v", err)
}
// Sleep sometime because AWS is eventually consistent
log.Println("[WARN] Sleeping for 10 seconds waiting for AWS...")
time.Sleep(10 * time.Second)
}
func teardown() error {
creds := credentials.NewStaticCredentials(os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY"), "")
awsConfig := &aws.Config{
Credentials: creds,
Region: aws.String("us-east-1"),
HTTPClient: cleanhttp.DefaultClient(),
}
svc := iam.New(session.New(awsConfig))
attachment := &iam.DetachRolePolicyInput{
PolicyArn: aws.String(testPolicyArn),
RoleName: aws.String(testRoleName), // Required
}
_, err := svc.DetachRolePolicy(attachment)
params := &iam.DeleteRoleInput{
RoleName: aws.String(testRoleName),
}
log.Printf("[INFO] AWS DeleteRole: %s", testRoleName)
_, err = svc.DeleteRole(params)
if err != nil {
log.Printf("[WARN] AWS DeleteRole failed: %v", err)
}
return err
}
func testAccStepConfig(t *testing.T) logicaltest.TestStep {
@ -178,7 +303,7 @@ func testAccStepReadSTSWithArnPolicy(t *testing.T, name string) logicaltest.Test
ErrorOk: true,
Check: func(resp *logical.Response) error {
if resp.Data["error"] !=
"Can't generate STS credentials for a managed policy; use an inline policy instead" {
"Can't generate STS credentials for a managed policy; use a role to assume or an inline policy instead" {
t.Fatalf("bad: %v", resp)
}
return nil
@ -317,3 +442,13 @@ func testAccStepReadArnPolicy(t *testing.T, name string, value string) logicalte
},
}
}
func testAccStepWriteArnRoleRef(t *testing.T, roleName string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "roles/" + roleName,
Data: map[string]interface{}{
"arn": fmt.Sprintf("arn:aws:iam::%s:role/%s", os.Getenv("AWS_ACCOUNT_ID"), roleName),
},
}
}

View File

@ -48,9 +48,17 @@ func (b *backend) pathSTSRead(
}
policyValue := string(policy.Value)
if strings.HasPrefix(policyValue, "arn:") {
return logical.ErrorResponse(
"Can't generate STS credentials for a managed policy; use an inline policy instead"),
logical.ErrInvalidRequest
if strings.Contains(policyValue, ":role/") {
return b.assumeRole(
req.Storage,
req.DisplayName, policyName, policyValue,
&ttl,
)
} else {
return logical.ErrorResponse(
"Can't generate STS credentials for a managed policy; use a role to assume or an inline policy instead"),
logical.ErrInvalidRequest
}
}
// Use the helper to create the secret
return b.secretTokenCreate(

View File

@ -111,6 +111,48 @@ func (b *backend) secretTokenCreate(s logical.Storage,
return resp, nil
}
func (b *backend) assumeRole(s logical.Storage,
displayName, policyName, policy string,
lifeTimeInSeconds *int64) (*logical.Response, error) {
STSClient, err := clientSTS(s)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
username, usernameWarning := genUsername(displayName, policyName, "iam_user")
tokenResp, err := STSClient.AssumeRole(
&sts.AssumeRoleInput{
RoleSessionName: aws.String(username),
RoleArn: aws.String(policy),
DurationSeconds: lifeTimeInSeconds,
})
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error assuming role: %s", err)), nil
}
resp := b.Secret(SecretAccessKeyType).Response(map[string]interface{}{
"access_key": *tokenResp.Credentials.AccessKeyId,
"secret_key": *tokenResp.Credentials.SecretAccessKey,
"security_token": *tokenResp.Credentials.SessionToken,
}, map[string]interface{}{
"username": username,
"policy": policy,
"is_sts": true,
})
// Set the secret TTL to appropriately match the expiration of the token
resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now())
if usernameWarning != "" {
resp.AddWarning(usernameWarning)
}
return resp, nil
}
func (b *backend) secretAccessKeysCreate(
s logical.Storage,
displayName, policyName string, policy string) (*logical.Response, error) {

View File

@ -46,7 +46,7 @@ The following parameters are required:
- `region` the AWS region for API calls.
The next step is to configure a role. A role is a logical name that maps
to a policy used to generated those credentials.
to a policy used to generated those credentials.
You can either supply a user inline policy (via the policy argument), or
provide a reference to an existing AWS policy by supplying the full ARN
reference (via the arn argument).
@ -114,38 +114,17 @@ secret_key vS61xxXgwwX/V4qZMUv8O8wd2RLqngXz6WmN04uW
security_token <nil>
```
If you want keys with an STS token use the 'sts' endpoint instead of 'creds.'
The aws/sts endpoint will always fetch STS credentials with a 1hr ttl. Note that STS credentials can only be generated
for user inline policies.
## Dynamic IAM users
```text
$vault read aws/sts/deploy
Key Value
lease_id aws/sts/deploy/31d771a6-fb39-f46b-fdc5-945109106422
lease_duration 3600
lease_renewable true
access_key ASIAJYYYY2AA5K4WIXXX
secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX
security_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX
```
The `aws/creds` endpoint will dynamically create a new IAM user and respond
with an IAM access key for the newly created user.
The [Quick Start](#quick-start) describes how to setup the `aws/creds` endpoint.
If you get an error message similar to either of the following, the root credentials that you wrote to `aws/config/root` have insufficient privilege:
## Root Credentials for Dynamic IAM users
```text
$ vault read aws/creds/deploy
* Error creating IAM user: User: arn:aws:iam::000000000000:user/hashicorp is not authorized to perform: iam:CreateUser on resource: arn:aws:iam::000000000000:user/vault-root-1432735386-4059
$ vault revoke aws/creds/deploy/774cfb27-c22d-6e78-0077-254879d1af3c
Revoke error: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/sys/revoke/aws/creds/deploy/774cfb27-c22d-6e78-0077-254879d1af3c
Code: 400. Errors:
* invalid request
```
The root credentials need permission to perform various IAM actions. These are the actions that the AWS secret backend uses to manage IAM credentials. Here is an example IAM policy that would grant these permissions:
The `aws/config/root` credentials need permission to manage dynamic IAM users.
Here is an example IAM policy that would grant these permissions:
```javascript
{
@ -176,14 +155,196 @@ The root credentials need permission to perform various IAM actions. These are t
}
```
Note that this policy example is unrelated to the policy you wrote to `aws/roles/deploy`. This policy example should be applied to the IAM user (or role) associated with the root credentials that you wrote to `aws/config/root`. You have to apply it yourself in IAM. The policy you wrote to `aws/roles/deploy` is the policy you want the AWS secret backend to apply to the temporary credentials it returns from `aws/creds/deploy`.
Note that this policy example is unrelated to the policy you wrote to `aws/roles/deploy`.
This policy example should be applied to the IAM user (or role) associated with
the root credentials that you wrote to `aws/config/root`. You have to apply it
yourself in IAM. The policy you wrote to `aws/roles/deploy` is the policy you
want the AWS secret backend to apply to the temporary credentials it returns
from `aws/creds/deploy`.
Unfortunately, IAM credentials are eventually consistent with respect to other
Amazon services. If you are planning on using these credential in a pipeline,
you may need to add a delay of 5-10 seconds (or more) after fetching
credentials before they can be used successfully.
If you want to be able to use credentials without the wait, consider using the STS
method of fetching keys. IAM credentials supported by an STS token are available for use
as soon as they are generated.
## STS credentials
Vault also supports an STS credentials instead of creating a new IAM user.
The `aws/sts` endpoint will always fetch credentials with a 1hr ttl.
Unlike the `aws/creds` enpoint, the ttl is enforced by STS.
Vault supports two of the [STS APIs](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_request.html),
[STS federation tokens](http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html) and
[STS AssumeRole](http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html).
### STS Federation Tokens
An STS federation token inherits a set of permissions that are the combination
(intersection) of three sets of permissions:
1. The permissions granted to the `aws/config/root` credentials
2. The user inline policy configured for the `aws/role`
3. An implicit deny policy on IAM or STS operations.
STS federation token credentials can only be generated for user inline
policies; the AWS GetFederationToken API does not support managed policies.
The `aws/config/root` credentials require IAM permissions for
`sts:GetFederationToken` and the permissions to delegate to the STS
federation token. For example, this policy on the `aws/config/root` credentials
would allow creation of an STS federated token with delegated `ec2:*` permissions:
```javascript
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": [
"ec2:*",
"sts:GetFederationToken"
],
"Resource": "*"
}
}
```
Our "deploy" role would then assign an inline user policy with the same `ec2:*`
permissions.
```text
$ vault write aws/roles/deploy \
policy=@policy.json
```
The policy.json file would contain an inline policy with similar permissions,
less the `sts:GetFederationToken` permission. (We could grant `sts` permissions,
but STS would attach an implict deny on `sts` that overides the allow.)
```javascript
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "ec2:*",
"Resource": "*"
}
}
```
To generate a new set of STS federation token credentials, we simply read from
the role using the aws/sts endpoint:
```text
$vault read aws/sts/deploy
Key Value
lease_id aws/sts/deploy/31d771a6-fb39-f46b-fdc5-945109106422
lease_duration 3600
lease_renewable true
access_key ASIAJYYYY2AA5K4WIXXX
secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX
security_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX
```
### STS AssumeRole
STS AssumeRole is typically used for cross-account authentication or single sign-on (SSO)
scenarios. AssumeRole has additional complexity compared STS federation tokens:
1. The ARN of a IAM role to assume
2. IAM inline policies and/or managed policies attached to the IAM role
3. IAM trust policy attached to the IAM role to grant privileges for one identity
to assume the role.
AssumeRole adds a few benefits over federation tokens:
1. Assumed roles can invoke IAM and STS operations, if granted by the role's
IAM policies.
2. Assumed roles support cross-account authenication
The `aws/config/root` credentials must have an IAM policy that allows `sts:AssumeRole`
against the target role:
```javascript
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:role/RoleNameToAssume"
}
}
```
You must attach a trust policy to the target IAM role to assume, allowing
the aws/root/config credentials to assume the role.
```javascript
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/VAULT-AWS-ROOT-CONFIG-USER-NAME"
},
"Action": "sts:AssumeRole"
}
]
}
```
Finally, let's create a "deploy" policy using the arn of our role to assume:
```text
$ vault write aws/roles/deploy \
policy=arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:role/RoleNameToAssume
```
To generate a new set of STS assumed role credentials, we again read from
the role using the aws/sts endpoint:
```text
$vault read aws/sts/deploy
Key Value
lease_id aws/sts/deploy/31d771a6-fb39-f46b-fdc5-945109106422
lease_duration 3600
lease_renewable true
access_key ASIAJYYYY2AA5K4WIXXX
secret_key HSs0DYYYYYY9W81DXtI0K7X84H+OVZXK5BXXXX
security_token AQoDYXdzEEwasAKwQyZUtZaCjVNDiXXXXXXXXgUgBBVUUbSyujLjsw6jYzboOQ89vUVIehUw/9MreAifXFmfdbjTr3g6zc0me9M+dB95DyhetFItX5QThw0lEsVQWSiIeIotGmg7mjT1//e7CJc4LpxbW707loFX1TYD1ilNnblEsIBKGlRNXZ+QJdguY4VkzXxv2urxIH0Sl14xtqsRPboV7eYruSEZlAuP3FLmqFbmA0AFPCT37cLf/vUHinSbvw49C4c9WQLH7CeFPhDub7/rub/QU/lCjjJ43IqIRo9jYgcEvvdRkQSt70zO8moGCc7pFvmL7XGhISegQpEzudErTE/PdhjlGpAKGR3d5qKrHpPYK/k480wk1Ai/t1dTa/8/3jUYTUeIkaJpNBnupQt7qoaXXXXXXXXXX
```
## Troubleshooting
### Dynamic IAM user errors
If you get an error message similar to either of the following, the root credentials that you wrote to `aws/config/root` have insufficient privilege:
```text
$ vault read aws/creds/deploy
* Error creating IAM user: User: arn:aws:iam::000000000000:user/hashicorp is not authorized to perform: iam:CreateUser on resource: arn:aws:iam::000000000000:user/vault-root-1432735386-4059
$ vault revoke aws/creds/deploy/774cfb27-c22d-6e78-0077-254879d1af3c
Revoke error: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/sys/revoke/aws/creds/deploy/774cfb27-c22d-6e78-0077-254879d1af3c
Code: 400. Errors:
* invalid request
```
If you get stuck at any time, simply run `vault path-help aws` or with a subpath for
interactive help output.
## A Note on STS Permissions
### STS federated token errors
Vault generates STS tokens using the IAM credentials passed to aws/config.
Vault generates STS tokens using the IAM credentials passed to `aws/config`.
Those credentials must have two properties:
@ -195,19 +356,8 @@ If either of those conditions are not met, a "403 not-authorized" error will be
See http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html for more details.
Vault 0.5.1 or later is recommended when using STS tokens to avoid validation errors for exceeding
the AWS limit of 32 characters on STS token names.
## A Note on Consistency
Unfortunately, IAM credentials are eventually consistent with respect to other
Amazon services. If you are planning on using these credential in a pipeline,
you may need to add a delay of 5-10 seconds (or more) after fetching
credentials before they can be used successfully.
If you want to be able to use credentials without the wait, consider using the STS
method of fetching keys. IAM credentials supported by an STS token are available for use
as soon as they are generated.
Vault 0.5.1 or later is recommended when using STS tokens to avoid validation
errors for exceeding the AWS limit of 32 characters on STS token names.
## API