Merge pull request #927 from urq/feature-sts

Adding STS to the aws backend
This commit is contained in:
Jeff Mitchell 2016-01-21 15:43:39 -05:00
commit 5de04e1810
8 changed files with 280 additions and 21 deletions

View file

@ -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

View file

@ -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{}

View file

@ -28,6 +28,7 @@ func Backend() *framework.Backend {
pathConfigLease(&b),
pathRoles(),
pathUser(&b),
pathSTS(&b),
},
Secrets: []*framework.Secret{

View file

@ -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,

View file

@ -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
}

View file

@ -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.
`

View file

@ -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 {

View file

@ -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 <nil>
```
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 <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.
```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
}
}
```
</dd>
</dl>
### /aws/sts/
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Generates a dynamic IAM credential with an STS token based on the named role.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/aws/sts/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"access_key": "...",
"secret_key": "...",
"secret_token": "..."
}
}
```
</dd>
</dl>