From d206599b80aaa775a5f93aa1726fd6a779a279d3 Mon Sep 17 00:00:00 2001 From: Jack DeLoach Date: Mon, 7 Dec 2015 23:32:49 -0500 Subject: [PATCH 1/7] Add STS path to AWS backend. The new STS path allows for obtaining the same credentials that you would get from the AWS "creds" path, except it will also provide a security token, and will not have an annoyingly long propagation time before returning to the user. --- builtin/credential/cert/cli.go | 2 +- builtin/credential/userpass/cli.go | 4 +- builtin/logical/aws/backend.go | 1 + builtin/logical/aws/client.go | 15 ++++- builtin/logical/aws/path_sts.go | 62 ++++++++++++++++++ builtin/logical/aws/rollback.go | 1 + builtin/logical/aws/secret_access_keys.go | 79 ++++++++++++++++++++++- 7 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 builtin/logical/aws/path_sts.go 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/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..66cfbee8f2 --- /dev/null +++ b/builtin/logical/aws/path_sts.go @@ -0,0 +1,62 @@ +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", + }, + }, + + 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) + + // 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)) +} + +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/rollback.go b/builtin/logical/aws/rollback.go index 8f133396fe..1b79af70ba 100644 --- a/builtin/logical/aws/rollback.go +++ b/builtin/logical/aws/rollback.go @@ -9,6 +9,7 @@ import ( var rollbackMap = map[string]framework.RollbackFunc{ "user": pathUserRollback, + "sts": pathUserRollback, } func rollback(req *logical.Request, kind string, data interface{}) error { diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index 1aa17658e3..e5b39f7fb9 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,75 @@ func secretAccessKeys(b *backend) *framework.Secret { } } +func (b *backend) secretAccessKeysAndTokenCreate( + s logical.Storage, + displayName, policyName string, policy string) (*logical.Response, error) { + IAMClient, err := clientIAM(s) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + STSClient, err := clientSTS(s) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + // 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. + username := fmt.Sprintf("vault-%s-%d-%d", normalizeDisplayName(displayName), time.Now().Unix(), rand.Int31n(10000)) + + // 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 + // can fail, which would put us in an awkward position: we have a user + // we need to rollback but can't put the WAL entry to do the rollback. + walId, err := framework.PutWAL(s, "user", &walUser{ + UserName: username, + }) + if err != nil { + return nil, fmt.Errorf("Error writing WAL entry: %s", err) + } + + // Create the user + _, err = IAMClient.CreateUser(&iam.CreateUserInput{ + UserName: aws.String(username), + }) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Error creating IAM user: %s", err)), nil + } + + duration := int64(60*60) + + // Create the keys and token + resp, err := STSClient.GetFederationToken( + &sts.GetFederationTokenInput{ + Name: aws.String(username), + Policy: aws.String(policy), + DurationSeconds: &duration, //TODO make this configurable + }) + + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Error creating access keys: %s", err)), nil + } + + // Remove the WAL entry, we succeeded! If we fail, we don't return + // the secret because it'll get rolled back anyways, so we have to return + // an error here. + if err := framework.DeleteWAL(s, walId); err != nil { + return nil, fmt.Errorf("Failed to commit WAL entry: %s", err) + } + + // Return the info! + return b.Secret(SecretAccessKeyType).Response(map[string]interface{}{ + "access_key": *resp.Credentials.AccessKeyId, + "secret_key": *resp.Credentials.SecretAccessKey, + "security_token": *resp.Credentials.SessionToken, + }, map[string]interface{}{ + "username": username, + "policy": policy, + }), nil +} + func (b *backend) secretAccessKeysCreate( s logical.Storage, displayName, policyName string, policy string) (*logical.Response, error) { @@ -120,8 +194,9 @@ 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, From 522e8a345035c186447e10c01ad9d4dc6c671ba9 Mon Sep 17 00:00:00 2001 From: Dmitriy Gromov Date: Fri, 11 Dec 2015 01:00:54 -0500 Subject: [PATCH 2/7] Configurable sts duration --- builtin/logical/aws/path_sts.go | 11 ++++++++++- builtin/logical/aws/secret_access_keys.go | 12 ++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/builtin/logical/aws/path_sts.go b/builtin/logical/aws/path_sts.go index 66cfbee8f2..232bad444c 100644 --- a/builtin/logical/aws/path_sts.go +++ b/builtin/logical/aws/path_sts.go @@ -15,6 +15,11 @@ func pathSTS(b *backend) *framework.Path { Type: framework.TypeString, Description: "Name of the role", }, + "duration": &framework.FieldSchema{ + Type: framework.TypeInt, + Description: "Lifetime of the token in seconds", + Default: 3600, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -29,6 +34,7 @@ func pathSTS(b *backend) *framework.Path { func (b *backend) pathSTSRead( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { policyName := d.Get("name").(string) + duration := d.Get("duration").(int64) // Read the policy policy, err := req.Storage.Get("policy/" + policyName) @@ -42,7 +48,10 @@ func (b *backend) pathSTSRead( // Use the helper to create the secret return b.secretAccessKeysAndTokenCreate( - req.Storage, req.DisplayName, policyName, string(policy.Value)) + req.Storage, + req.DisplayName, policyName, string(policy.Value), + &duration, + ) } const pathSTSHelpSyn = ` diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index e5b39f7fb9..762bae6aea 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -43,9 +43,9 @@ func secretAccessKeys(b *backend) *framework.Secret { } } -func (b *backend) secretAccessKeysAndTokenCreate( - s logical.Storage, - displayName, policyName string, policy string) (*logical.Response, error) { +func (b *backend) secretAccessKeysAndTokenCreate(s logical.Storage, + displayName, policyName, policy string, + lifeTimeInSeconds *int64) (*logical.Response, error) { IAMClient, err := clientIAM(s) if err != nil { return logical.ErrorResponse(err.Error()), nil @@ -79,14 +79,14 @@ func (b *backend) secretAccessKeysAndTokenCreate( "Error creating IAM user: %s", err)), nil } - duration := int64(60*60) - // Create the keys and token + fmt.Println(username, policy) + resp, err := STSClient.GetFederationToken( &sts.GetFederationTokenInput{ Name: aws.String(username), Policy: aws.String(policy), - DurationSeconds: &duration, //TODO make this configurable + DurationSeconds: lifeTimeInSeconds, }) if err != nil { From 6f50cd943972f783b697d1763bccad85c0b9c3b6 Mon Sep 17 00:00:00 2001 From: Dmitriy Gromov Date: Fri, 8 Jan 2016 17:19:53 -0500 Subject: [PATCH 3/7] Fixed duration type and added acceptance test for sts --- builtin/logical/aws/backend_test.go | 48 +++++++++++++++++++++++++++++ builtin/logical/aws/path_sts.go | 2 +- builtin/logical/aws/rollback.go | 1 - 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index a61f5c2a35..517febd273 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, diff --git a/builtin/logical/aws/path_sts.go b/builtin/logical/aws/path_sts.go index 232bad444c..2a9b9a4812 100644 --- a/builtin/logical/aws/path_sts.go +++ b/builtin/logical/aws/path_sts.go @@ -34,7 +34,7 @@ func pathSTS(b *backend) *framework.Path { func (b *backend) pathSTSRead( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { policyName := d.Get("name").(string) - duration := d.Get("duration").(int64) + duration := int64(d.Get("duration").(int)) // Read the policy policy, err := req.Storage.Get("policy/" + policyName) diff --git a/builtin/logical/aws/rollback.go b/builtin/logical/aws/rollback.go index 1b79af70ba..8f133396fe 100644 --- a/builtin/logical/aws/rollback.go +++ b/builtin/logical/aws/rollback.go @@ -9,7 +9,6 @@ import ( var rollbackMap = map[string]framework.RollbackFunc{ "user": pathUserRollback, - "sts": pathUserRollback, } func rollback(req *logical.Request, kind string, data interface{}) error { From b37a963841d6877d3b6923613e811734bcab7c4e Mon Sep 17 00:00:00 2001 From: Dmitriy Gromov Date: Thu, 14 Jan 2016 13:22:26 -0500 Subject: [PATCH 4/7] Removing debug print statement from sts code --- builtin/logical/aws/secret_access_keys.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/builtin/logical/aws/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index 762bae6aea..55e531ac93 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -79,9 +79,6 @@ func (b *backend) secretAccessKeysAndTokenCreate(s logical.Storage, "Error creating IAM user: %s", err)), nil } - // Create the keys and token - fmt.Println(username, policy) - resp, err := STSClient.GetFederationToken( &sts.GetFederationTokenInput{ Name: aws.String(username), From e13f58713e99b55bd5d11563da8fb4819d015962 Mon Sep 17 00:00:00 2001 From: Dmitriy Gromov Date: Thu, 14 Jan 2016 14:20:02 -0500 Subject: [PATCH 5/7] documenting the new aws/sts endpoint --- website/source/docs/secrets/aws/index.html.md | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/website/source/docs/secrets/aws/index.html.md b/website/source/docs/secrets/aws/index.html.md index 4eb0c7f124..55625e2b3c 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,23 @@ 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.' + +```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 @@ -152,6 +168,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 +375,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": "..." + } + } + ``` +
+
From ea1e29fa33f1789f45b78f2cf47f6207d3382b9f Mon Sep 17 00:00:00 2001 From: Dmitriy Gromov Date: Thu, 21 Jan 2016 14:28:34 -0500 Subject: [PATCH 6/7] Renamed sts duration to ttl and added STS permissions note. --- builtin/logical/aws/path_sts.go | 8 ++++---- website/source/docs/secrets/aws/index.html.md | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/builtin/logical/aws/path_sts.go b/builtin/logical/aws/path_sts.go index 2a9b9a4812..c8fb95accf 100644 --- a/builtin/logical/aws/path_sts.go +++ b/builtin/logical/aws/path_sts.go @@ -15,8 +15,8 @@ func pathSTS(b *backend) *framework.Path { Type: framework.TypeString, Description: "Name of the role", }, - "duration": &framework.FieldSchema{ - Type: framework.TypeInt, + "ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, Description: "Lifetime of the token in seconds", Default: 3600, }, @@ -34,7 +34,7 @@ func pathSTS(b *backend) *framework.Path { func (b *backend) pathSTSRead( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { policyName := d.Get("name").(string) - duration := int64(d.Get("duration").(int)) + ttl := int64(d.Get("ttl").(int)) // Read the policy policy, err := req.Storage.Get("policy/" + policyName) @@ -50,7 +50,7 @@ func (b *backend) pathSTSRead( return b.secretAccessKeysAndTokenCreate( req.Storage, req.DisplayName, policyName, string(policy.Value), - &duration, + &ttl, ) } diff --git a/website/source/docs/secrets/aws/index.html.md b/website/source/docs/secrets/aws/index.html.md index 55625e2b3c..291f546c75 100644 --- a/website/source/docs/secrets/aws/index.html.md +++ b/website/source/docs/secrets/aws/index.html.md @@ -100,6 +100,7 @@ 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 @@ -161,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 From df65547eca94b4ef401ee907d1c5452b93f7c425 Mon Sep 17 00:00:00 2001 From: Dmitriy Gromov Date: Thu, 21 Jan 2016 15:04:16 -0500 Subject: [PATCH 7/7] STS now uses root vault user for keys The secretAccessKeysRevoke revoke function now asserts that it is not dealing with STS keys by checking a new internal data flag. Defaults to IAM when the flag is not found. Factored out genUsername into its own function to share between STS and IAM secret creation functions. Fixed bad call to "WriteOperation" instead of "UpdateOperation" in aws/backend_test --- builtin/logical/aws/backend_test.go | 2 +- builtin/logical/aws/secret_access_keys.go | 86 ++++++++++------------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/builtin/logical/aws/backend_test.go b/builtin/logical/aws/backend_test.go index 517febd273..e87e78f163 100644 --- a/builtin/logical/aws/backend_test.go +++ b/builtin/logical/aws/backend_test.go @@ -235,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/secret_access_keys.go b/builtin/logical/aws/secret_access_keys.go index 55e531ac93..84e99fd28b 100644 --- a/builtin/logical/aws/secret_access_keys.go +++ b/builtin/logical/aws/secret_access_keys.go @@ -43,43 +43,28 @@ 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) { - IAMClient, err := clientIAM(s) - if err != nil { - return logical.ErrorResponse(err.Error()), nil - } STSClient, err := clientSTS(s) if err != nil { return logical.ErrorResponse(err.Error()), nil } - // 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. - username := fmt.Sprintf("vault-%s-%d-%d", normalizeDisplayName(displayName), 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 - // can fail, which would put us in an awkward position: we have a user - // we need to rollback but can't put the WAL entry to do the rollback. - walId, err := framework.PutWAL(s, "user", &walUser{ - UserName: username, - }) - if err != nil { - return nil, fmt.Errorf("Error writing WAL entry: %s", err) - } - - // Create the user - _, err = IAMClient.CreateUser(&iam.CreateUserInput{ - UserName: aws.String(username), - }) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf( - "Error creating IAM user: %s", err)), nil - } - - resp, err := STSClient.GetFederationToken( + tokenResp, err := STSClient.GetFederationToken( &sts.GetFederationTokenInput{ Name: aws.String(username), Policy: aws.String(policy), @@ -88,24 +73,18 @@ func (b *backend) secretAccessKeysAndTokenCreate(s logical.Storage, if err != nil { return logical.ErrorResponse(fmt.Sprintf( - "Error creating access keys: %s", err)), nil - } - - // Remove the WAL entry, we succeeded! If we fail, we don't return - // the secret because it'll get rolled back anyways, so we have to return - // an error here. - if err := framework.DeleteWAL(s, walId); err != nil { - return nil, fmt.Errorf("Failed to commit WAL entry: %s", err) + "Error generating STS keys: %s", err)), nil } // Return the info! return b.Secret(SecretAccessKeyType).Response(map[string]interface{}{ - "access_key": *resp.Credentials.AccessKeyId, - "secret_key": *resp.Credentials.SecretAccessKey, - "security_token": *resp.Credentials.SessionToken, + "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 } @@ -117,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 @@ -197,6 +166,7 @@ func (b *backend) secretAccessKeysCreate( }, map[string]interface{}{ "username": username, "policy": policy, + "is_sts": false, }), nil } @@ -216,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 {