Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VAULT-12226: Add Static Roles to the AWS plugin #20536

Merged
merged 51 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d0641a9
* AWS static roles POC
maxcoulombe Feb 6, 2023
6b0c9fb
Merge branch 'main' into VAULT-12226-StaticAccountPOC
kpcraig Apr 7, 2023
bfd94c4
Merge branch 'main' into VAULT-12226-StaticAccountPOC
kpcraig Apr 19, 2023
cb9fb25
Fix re-queuing mechanism for clearing out stale static role credentia…
vinay-gopalan Apr 28, 2023
924da54
validate static role values, add a test, some placeholder description…
kpcraig Apr 28, 2023
1a71e9b
Test coverage for static roles (#20487)
kpcraig May 5, 2023
51223d3
add changelog
kpcraig May 9, 2023
6fb865a
add comments to tests
kpcraig May 9, 2023
ed5ffff
gomod update
kpcraig May 9, 2023
33f1646
tidy gomod
kpcraig May 9, 2023
e62eafd
Merge branch 'main' into VAULT-12226/aws-static-roles
kpcraig May 10, 2023
4927e77
add descriptions for the routes
kpcraig May 10, 2023
aa30783
website docs
kpcraig May 16, 2023
9b74c9e
remove now unnecessary placeholder init
kpcraig May 16, 2023
d3073f6
named returns and some comments on the rotation functions
kpcraig May 16, 2023
e47846f
separate queue empty vs other errors
kpcraig May 16, 2023
4fb9769
validate no cred rotation case
kpcraig May 16, 2023
72f4ed2
description editing
kpcraig May 16, 2023
251bf7a
more double quotes %q fixes
kpcraig May 16, 2023
4008ef9
missed an extra quote
kpcraig May 17, 2023
1e00c7c
change struct name to be analogous
kpcraig May 17, 2023
25f75fa
iterate over access keys instead of assuming 2
kpcraig May 17, 2023
fd9be36
very basic locking on storage access
kpcraig May 19, 2023
b9b43dd
Apply Doc suggestions
kpcraig May 19, 2023
2e339cd
fix mistake in key age loop
kpcraig May 19, 2023
f7d77f9
somewhat existent test for credential creation
kpcraig May 19, 2023
75158a4
fix formatting
kpcraig May 19, 2023
8aab96e
add tests to ensure we're deleting the oldest key
kpcraig May 22, 2023
b6a88b8
fix format
kpcraig May 22, 2023
6a3ff89
lock on both role and cred deletion
kpcraig May 22, 2023
da84326
Merge branch 'main' into VAULT-12226/aws-static-roles
kpcraig May 22, 2023
b7a646b
test polish
kpcraig May 22, 2023
b36c837
add queue verification to deletion test
kpcraig May 22, 2023
0df02de
update deletion logic and tests
kpcraig May 22, 2023
8fbf3ad
remove extraneous quotes
kpcraig May 23, 2023
1d1af14
add update user test case
kpcraig May 23, 2023
083e2a3
add separate update validation logic
kpcraig May 23, 2023
bfd0854
remove extra print
kpcraig May 23, 2023
c20a7ef
replace quote-percent-s with percent-q
kpcraig May 23, 2023
64ebc69
track ID since username can change
kpcraig May 23, 2023
198fa74
doc updates
kpcraig May 23, 2023
2c52b94
update backend to accept config; update periodic func to only run on …
kpcraig May 24, 2023
995188b
elevate mockIAM out of loop in rotation test
kpcraig May 24, 2023
c3703c1
fix local/leader check
kpcraig May 24, 2023
43f51b3
add static role information to the non-api docs
kpcraig May 24, 2023
e77d7f3
add commentary for why we're storing the user id
kpcraig May 24, 2023
64e699d
link directly to static role section of api
kpcraig May 24, 2023
e612ece
return client error instead of actual error for validation problems
kpcraig May 24, 2023
2b2938d
syntactic sugar for error list
kpcraig May 24, 2023
dd849d1
fix last minute bug with json decoding
kpcraig May 24, 2023
50ecb27
add iam unique user id verification to credential creation
kpcraig May 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion builtin/logical/aws/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/aws/aws-sdk-go/service/sts/stsiface"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)

const (
Expand All @@ -32,6 +33,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,

func Backend() *backend {
var b backend
b.credRotationQueue = queue.New()
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),

Expand All @@ -40,7 +42,8 @@ func Backend() *backend {
framework.WALPrefix,
},
SealWrapStorage: []string{
"config/root",
rootConfigPath,
pathStaticCreds + "/",
},
},

Expand All @@ -50,6 +53,8 @@ func Backend() *backend {
pathConfigLease(&b),
pathRoles(&b),
pathListRoles(&b),
pathStaticRoles(&b),
pathStaticCredentials(&b),
pathUser(&b),
},

Expand All @@ -60,6 +65,7 @@ func Backend() *backend {
Invalidate: b.invalidate,
WALRollback: b.walRollback,
WALRollbackMinAge: minAwsUserRollbackAge,
PeriodicFunc: b.rotateExpiredStaticCreds,
kpcraig marked this conversation as resolved.
Show resolved Hide resolved
BackendType: logical.TypeLogical,
}

Expand All @@ -79,6 +85,10 @@ type backend struct {
// to enable mocking with AWS iface for tests
iamClient iamiface.IAMAPI
stsClient stsiface.STSAPI

// the age of a static role's credential is tracked by a priority queue and handled
// by the PeriodicFunc
credRotationQueue *queue.PriorityQueue
}

const backendHelp = `
Expand Down
99 changes: 99 additions & 0 deletions builtin/logical/aws/path_static_creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package aws

import (
"context"
"fmt"
"net/http"

"github.com/fatih/structs"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)

const (
pathStaticCreds = "static-creds"

paramAccessKeyID = "access_key_id"
paramSecretsAccessKey = "secret_access_key"
)

type awsCredentials struct {
AccessKeyID string `json:"access_key_id" structs:"access_key_id" mapstructure:"access_key_id"`
SecretAccessKey string `json:"secret_access_key" structs:"secret_access_key" mapstructure:"secret_access_key"`
kpcraig marked this conversation as resolved.
Show resolved Hide resolved
}

func pathStaticCredentials(b *backend) *framework.Path {
return &framework.Path{
Pattern: fmt.Sprintf("%s/%s", pathStaticCreds, framework.GenericNameWithAtRegex(paramRoleName)),
Fields: map[string]*framework.FieldSchema{
paramRoleName: {
Type: framework.TypeString,
Description: descRoleName,
},
},

Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathStaticCredsRead,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: http.StatusText(http.StatusOK),
Fields: map[string]*framework.FieldSchema{
paramAccessKeyID: {
Type: framework.TypeString,
Description: descAccessKeyID,
},
paramSecretsAccessKey: {
Type: framework.TypeString,
Description: descSecretAccessKey,
},
},
}},
},
},
},

HelpSynopsis: pathStaticCredsHelpSyn,
HelpDescription: pathStaticCredsHelpDesc,
}
}

func (b *backend) pathStaticCredsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName, ok := data.GetOk(paramRoleName)
if !ok {
return nil, fmt.Errorf("missing %q parameter", paramRoleName)
}

entry, err := req.Storage.Get(ctx, formatCredsStoragePath(roleName.(string)))
if err != nil {
return nil, fmt.Errorf("failed to read credentials for role %q: %w", roleName, err)
}
if entry == nil {
return nil, nil
}

var credentials awsCredentials
if err := entry.DecodeJSON(&credentials); err != nil {
return nil, fmt.Errorf("failed to decode credentials: %w", err)
}

return &logical.Response{
Data: structs.New(credentials).Map(),
}, nil
}

func formatCredsStoragePath(roleName string) string {
return fmt.Sprintf("%s/%s", pathStaticCreds, roleName)
}

const pathStaticCredsHelpSyn = `Retrieve static credentials from the named role.`

const pathStaticCredsHelpDesc = `
This path reads AWS credentials for a certain static role. The keys are rotated
periodically according to their configuration, and will return the same password
until they are rotated.`

const (
descAccessKeyID = "The access key of the AWS Credential"
descSecretAccessKey = "The secret key of the AWS Credential"
)
92 changes: 92 additions & 0 deletions builtin/logical/aws/path_static_creds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package aws

import (
"context"
"reflect"
"testing"

"github.com/fatih/structs"

"github.com/hashicorp/vault/sdk/framework"

"github.com/hashicorp/vault/sdk/logical"
)

// TestStaticCredsRead verifies that we can correctly read a cred that exists, and correctly _not read_
// a cred that does not exist.
func TestStaticCredsRead(t *testing.T) {
// setup
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
bgCTX := context.Background() // for brevity later

// insert a cred to get
creds := &awsCredentials{
AccessKeyID: "foo",
SecretAccessKey: "bar",
}
entry, err := logical.StorageEntryJSON(formatCredsStoragePath("test"), creds)
if err != nil {
t.Fatal(err)
}
err = config.StorageView.Put(bgCTX, entry)
if err != nil {
t.Fatal(err)
}

// cases
cases := []struct {
name string
roleName string
expectedError error
expectedResponse *logical.Response
}{
{
name: "get existing creds",
roleName: "test",
expectedResponse: &logical.Response{
Data: structs.New(creds).Map(),
},
},
{
name: "get non-existent creds",
roleName: "this-doesnt-exist",
// returns nil, nil
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
b := Backend()

req := &logical.Request{
Storage: config.StorageView,
Data: map[string]interface{}{
"name": c.roleName,
},
}
resp, err := b.pathStaticCredsRead(bgCTX, req, staticCredsFieldData(req.Data))

if err != c.expectedError {
t.Fatalf("got error %q, but expected %q", err, c.expectedError)
}
if !reflect.DeepEqual(resp, c.expectedResponse) {
t.Fatalf("got response %v, but expected %v", resp, c.expectedResponse)
}
})
}
}

func staticCredsFieldData(data map[string]interface{}) *framework.FieldData {
schema := map[string]*framework.FieldSchema{
paramRoleName: {
Type: framework.TypeString,
Description: descRoleName,
},
}

return &framework.FieldData{
Raw: data,
Schema: schema,
}
}
Loading