diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index 2d98c46b2e..4afc1eadc5 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -12,7 +12,7 @@ type CLIHandler struct{} func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { var data struct { - Mount string `mapstructure:"mount"` + Mount string `mapstructure:"mount"` } if err := mapstructure.WeakDecode(m, &data); err != nil { return "", err diff --git a/builtin/credential/userpass/cli.go b/builtin/credential/userpass/cli.go index c5d5cc27af..5bac2f43ee 100644 --- a/builtin/credential/userpass/cli.go +++ b/builtin/credential/userpass/cli.go @@ -2,12 +2,12 @@ package userpass import ( "fmt" - "strings" "os" + "strings" "github.com/hashicorp/vault/api" - "github.com/mitchellh/mapstructure" pwd "github.com/hashicorp/vault/helper/password" + "github.com/mitchellh/mapstructure" ) type CLIHandler struct{} diff --git a/builtin/logical/aws/backend.go b/builtin/logical/aws/backend.go index d92aa8f0b7..8d9614d609 100644 --- a/builtin/logical/aws/backend.go +++ b/builtin/logical/aws/backend.go @@ -28,6 +28,7 @@ func Backend() *framework.Backend { pathConfigLease(&b), pathRoles(), pathUser(&b), + pathSTS(&b), }, Secrets: []*framework.Secret{ diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index a61f5c2a35..e87e78f163 100644 --- a/builtin/logical/aws/backend_test.go +++ b/builtin/logical/aws/backend_test.go @@ -36,6 +36,18 @@ func TestBackend_basic(t *testing.T) { }) } +func TestBackend_basicSTS(t *testing.T) { + logicaltest.Test(t, logicaltest.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Backend: getBackend(t), + Steps: []logicaltest.TestStep{ + testAccStepConfig(t), + testAccStepWritePolicy(t, "test", testPolicy), + testAccStepReadSTS(t, "test"), + }, + }) +} + func TestBackend_policyCrud(t *testing.T) { var compacted bytes.Buffer if err := json.Compact(&compacted, []byte(testPolicy)); err != nil { @@ -119,6 +131,42 @@ func testAccStepReadUser(t *testing.T, name string) logicaltest.TestStep { } } +func testAccStepReadSTS(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "sts/" + name, + Check: func(resp *logical.Response) error { + var d struct { + AccessKey string `mapstructure:"access_key"` + SecretKey string `mapstructure:"secret_key"` + STSToken string `mapstructure:"security_token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[WARN] Generated credentials: %v", d) + + // Build a client and verify that the credentials work + creds := credentials.NewStaticCredentials(d.AccessKey, d.SecretKey, d.STSToken) + awsConfig := &aws.Config{ + Credentials: creds, + Region: aws.String("us-east-1"), + HTTPClient: cleanhttp.DefaultClient(), + } + client := ec2.New(session.New(awsConfig)) + + log.Printf("[WARN] Verifying that the generated credentials work...") + _, err := client.DescribeInstances(&ec2.DescribeInstancesInput{}) + if err != nil { + return err + } + + return nil + }, + } +} + + func testAccStepWritePolicy(t *testing.T, name string, policy string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, @@ -187,7 +235,7 @@ const testPolicyArn = "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" func testAccStepWriteArnPolicyRef(t *testing.T, name string, arn string) logicaltest.TestStep { return logicaltest.TestStep{ - Operation: logical.WriteOperation, + Operation: logical.UpdateOperation, Path: "roles/" + name, Data: map[string]interface{}{ "arn": testPolicyArn, diff --git a/builtin/logical/aws/client.go b/builtin/logical/aws/client.go index 27a0f8995f..b0df673d5a 100644 --- a/builtin/logical/aws/client.go +++ b/builtin/logical/aws/client.go @@ -7,11 +7,12 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/logical" ) -func clientIAM(s logical.Storage) (*iam.IAM, error) { +func getRootConfig(s logical.Storage) (*aws.Config, error) { entry, err := s.Get("config/root") if err != nil { return nil, err @@ -28,11 +29,19 @@ func clientIAM(s logical.Storage) (*iam.IAM, error) { } creds := credentials.NewStaticCredentials(config.AccessKey, config.SecretKey, "") - awsConfig := &aws.Config{ + return &aws.Config{ Credentials: creds, Region: aws.String(config.Region), HTTPClient: cleanhttp.DefaultClient(), - } + }, nil +} +func clientIAM(s logical.Storage) (*iam.IAM, error) { + awsConfig, _ := getRootConfig(s) return iam.New(session.New(awsConfig)), nil } + +func clientSTS(s logical.Storage) (*sts.STS, error) { + awsConfig, _ := getRootConfig(s) + return sts.New(session.New(awsConfig)), nil +} diff --git a/builtin/logical/aws/path_sts.go b/builtin/logical/aws/path_sts.go new file mode 100644 index 0000000000..c8fb95accf --- /dev/null +++ b/builtin/logical/aws/path_sts.go @@ -0,0 +1,71 @@ +package aws + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathSTS(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "sts/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role", + }, + "ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: "Lifetime of the token in seconds", + Default: 3600, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathSTSRead, + }, + + HelpSynopsis: pathSTSHelpSyn, + HelpDescription: pathSTSHelpDesc, + } +} + +func (b *backend) pathSTSRead( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + policyName := d.Get("name").(string) + ttl := int64(d.Get("ttl").(int)) + + // Read the policy + policy, err := req.Storage.Get("policy/" + policyName) + if err != nil { + return nil, fmt.Errorf("error retrieving role: %s", err) + } + if policy == nil { + return logical.ErrorResponse(fmt.Sprintf( + "Role '%s' not found", policyName)), nil + } + + // Use the helper to create the secret + return b.secretAccessKeysAndTokenCreate( + req.Storage, + req.DisplayName, policyName, string(policy.Value), + &ttl, + ) +} + +const pathSTSHelpSyn = ` +Generate an access key pair + security token for a specific role. +` + +const pathSTSHelpDesc = ` +This path will generate a new, never before used key pair + security token for +accessing AWS. The IAM policy used to back this key pair will be +the "name" parameter. For example, if this backend is mounted at "aws", +then "aws/sts/deploy" would generate access keys for the "deploy" role. + +Note, these credentials are instantiated using the AWS STS backend. + +The access keys will have a lease associated with them. The access keys +can be revoked by using the lease ID. +` diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index 1aa17658e3..84e99fd28b 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" "strings" @@ -28,6 +29,10 @@ func secretAccessKeys(b *backend) *framework.Secret { Type: framework.TypeString, Description: "Secret Key", }, + "security_token": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Security Token", + }, }, DefaultDuration: 1 * time.Hour, @@ -38,6 +43,51 @@ func secretAccessKeys(b *backend) *framework.Secret { } } +func genUsername(displayName, policyName string) string { + // Generate a random username. We don't put the policy names in the + // username because the AWS console makes it pretty easy to see that. + return fmt.Sprintf( + "vault-%s-%s-%d-%d", + normalizeDisplayName(displayName), + normalizeDisplayName(policyName), + time.Now().Unix(), + rand.Int31n(10000)) +} + +func (b *backend) secretAccessKeysAndTokenCreate(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 := genUsername(displayName, policyName) + + tokenResp, err := STSClient.GetFederationToken( + &sts.GetFederationTokenInput{ + Name: aws.String(username), + Policy: aws.String(policy), + DurationSeconds: lifeTimeInSeconds, + }) + + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Error generating STS keys: %s", err)), nil + } + + // Return the info! + return 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, + }), nil +} + func (b *backend) secretAccessKeysCreate( s logical.Storage, displayName, policyName string, policy string) (*logical.Response, error) { @@ -46,17 +96,7 @@ func (b *backend) secretAccessKeysCreate( return logical.ErrorResponse(err.Error()), nil } - // Generate a random username. Originally when only dealing with user supplied - // inline polices, the policy name was not added into the generated username - // as the AWS console made it pretty easy to see this, however with the introduction - // of policy (arn) references having it form part of the name makes it easier to - // track down - username := fmt.Sprintf( - "vault-%s-%s-%d-%d", - normalizeDisplayName(displayName), - normalizeDisplayName(policyName), - time.Now().Unix(), - rand.Int31n(10000)) + username := genUsername(displayName, policyName) // Write to the WAL that this user will be created. We do this before // the user is created because if switch the order then the WAL put @@ -120,11 +160,13 @@ func (b *backend) secretAccessKeysCreate( // Return the info! return b.Secret(SecretAccessKeyType).Response(map[string]interface{}{ - "access_key": *keyResp.AccessKey.AccessKeyId, - "secret_key": *keyResp.AccessKey.SecretAccessKey, + "access_key": *keyResp.AccessKey.AccessKeyId, + "secret_key": *keyResp.AccessKey.SecretAccessKey, + "security_token": nil, }, map[string]interface{}{ "username": username, "policy": policy, + "is_sts": false, }), nil } @@ -144,6 +186,22 @@ func (b *backend) secretAccessKeysRenew( func secretAccessKeysRevoke( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + + // STS cleans up after itself so we can skip this if is_sts internal data + // element set to true. If is_sts is not set, assumes old version + // and defaults to the IAM approach. + isSTSRaw, ok := req.Secret.InternalData["is_sts"] + if ok { + isSTS, ok := isSTSRaw.(bool) + if ok { + if isSTS { + return nil, nil + } + } else { + return nil, fmt.Errorf("secret has is_sts but value could not be understood") + } + } + // Get the username from the internal data usernameRaw, ok := req.Secret.InternalData["username"] if !ok { diff --git a/website/source/docs/secrets/aws/index.html.md b/website/source/docs/secrets/aws/index.html.md index 4eb0c7f124..291f546c75 100644 --- a/website/source/docs/secrets/aws/index.html.md +++ b/website/source/docs/secrets/aws/index.html.md @@ -84,6 +84,7 @@ lease_id aws/creds/deploy/7cb8df71-782f-3de1-79dd-251778e49f58 lease_duration 3600 access_key AKIAIOMYUTSLGJOGLHTQ secret_key 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 +security_token ``` If you run the command again, you will get a new set of credentials: @@ -95,8 +96,24 @@ lease_id aws/creds/deploy/82d89562-ff19-382e-6be9-cb45c8f6a42d lease_duration 3600 access_key AKIAJZ5YRPHFH3QHRRRQ secret_key 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 +security_token ``` +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. + +```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 +``` + + 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 @@ -145,6 +162,20 @@ Note that this policy example is unrelated to the policy you wrote to `aws/roles 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 + +Vault generates STS tokens using the IAM credentials passed to aws/config. + +Those credentials must have two properties: + +- They must have permissions to call sts:GetFederatedToken. +- The capabilities of those credentials have to be at least as permissive as those requested +by policies attached to the STS creds. + +If either of those conditions are not met, a "403 not-authorized" error will be returned. + +See http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html for more details. + ## A Note on Consistency Unfortunately, IAM credentials are eventually consistent with respect to other @@ -152,6 +183,10 @@ 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. + ## API ### /aws/config/root @@ -355,10 +390,47 @@ credentials before they can be used successfully. { "data": { "access_key": "...", - "secret_key": "..." + "secret_key": "...", + "secret_token": null } } ``` + + +### /aws/sts/ +#### GET + +
+
Description
+
+ Generates a dynamic IAM credential with an STS token based on the named role. +
+ +
Method
+
GET
+ +
URL
+
`/aws/sts/`
+ +
Parameters
+
+ None +
+ +
Returns
+
+ + ```javascript + { + "data": { + "access_key": "...", + "secret_key": "...", + "secret_token": "..." + } + } + ``` +
+