mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
Merge pull request #927 from urq/feature-sts
Adding STS to the aws backend
This commit is contained in:
commit
5de04e1810
8 changed files with 280 additions and 21 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ func Backend() *framework.Backend {
|
|||
pathConfigLease(&b),
|
||||
pathRoles(),
|
||||
pathUser(&b),
|
||||
pathSTS(&b),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
71
builtin/logical/aws/path_sts.go
Normal file
71
builtin/logical/aws/path_sts.go
Normal 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.
|
||||
`
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ lease_id aws/creds/deploy/7cb8df71-782f-3de1-79dd-251778e49f58
|
|||
lease_duration 3600
|
||||
access_key AKIAIOMYUTSLGJOGLHTQ
|
||||
secret_key BK9++oBABaBvRKcT5KEF69xQGcH7ZpPRF3oqVEv7
|
||||
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 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.
|
||||
|
||||
```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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue