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

WIF support for AWS secrets engine #24987

Merged
merged 10 commits into from
Jan 29, 2024
4 changes: 2 additions & 2 deletions builtin/logical/aws/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage) (iamiface.IA
return b.iamClient, nil
}

iamClient, err := nonCachedClientIAM(ctx, s, b.Logger())
iamClient, err := b.nonCachedClientIAM(ctx, s, b.Logger())
if err != nil {
return nil, err
}
Expand All @@ -168,7 +168,7 @@ func (b *backend) clientSTS(ctx context.Context, s logical.Storage) (stsiface.ST
return b.stsClient, nil
}

stsClient, err := nonCachedClientSTS(ctx, s, b.Logger())
stsClient, err := b.nonCachedClientSTS(ctx, s, b.Logger())
if err != nil {
return nil, err
}
Expand Down
71 changes: 65 additions & 6 deletions builtin/logical/aws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@
"context"
"fmt"
"os"
"strconv"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/awsutil"

"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)

// NOTE: The caller is required to ensure that b.clientMutex is at least read locked
func getRootConfig(ctx context.Context, s logical.Storage, clientType string, logger hclog.Logger) (*aws.Config, error) {
func (b *backend) getRootConfig(ctx context.Context, s logical.Storage, clientType string, logger hclog.Logger) (*aws.Config, error) {
credsConfig := &awsutil.CredentialsConfig{}
var endpoint string
var maxRetries int = aws.UseServiceDefaultRetries
Expand All @@ -44,6 +50,26 @@
case clientType == "sts" && config.STSEndpoint != "":
endpoint = *aws.String(config.STSEndpoint)
}

if config.IdentityTokenAudience != "" {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get namespace from context: %w", err)
}

fetcher := &PluginIdentityTokenFetcher{
sys: b.System(),
logger: b.Logger(),
ns: ns,
audience: config.IdentityTokenAudience,
ttl: config.IdentityTokenTTL,
}

sessionSuffix := strconv.FormatInt(time.Now().UnixNano(), 10)
credsConfig.RoleSessionName = fmt.Sprintf("vault-aws-secrets-%s", sessionSuffix)
credsConfig.WebIdentityTokenFetcher = fetcher

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (windows, 386) / Vault windows 386 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (freebsd, 386) / Vault freebsd 386 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Linux (linux, 386) / Vault linux 386 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Darwin (darwin, amd64) / Vault darwin amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (netbsd, arm) / Vault netbsd arm v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Run Go tests / test-go (1)

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (windows, amd64) / Vault windows amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Linux (linux, arm) / Vault linux arm v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Run Go tests tagged with testonly / test-go (1)

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Darwin (darwin, arm64) / Vault darwin arm64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Linux (linux, arm64) / Vault linux arm64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Run Go tests / test-go (0)

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (solaris, amd64) / Vault solaris amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Run Go tests tagged with testonly / test-go (0)

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Linux (linux, amd64) / Vault linux amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Run Go tests / test-go (12)

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (freebsd, amd64) / Vault freebsd amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (netbsd, amd64) / Vault netbsd amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (openbsd, 386) / Vault openbsd 386 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (freebsd, arm) / Vault freebsd arm v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (openbsd, amd64) / Vault openbsd amd64 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)

Check failure on line 70 in builtin/logical/aws/client.go

View workflow job for this annotation

GitHub Actions / Other (netbsd, 386) / Vault netbsd 386 v1.16.0-beta1

credsConfig.WebIdentityTokenFetcher undefined (type *"github.com/hashicorp/go-secure-stdlib/awsutil".CredentialsConfig has no field or method WebIdentityTokenFetcher)
credsConfig.RoleARN = config.RoleARN
}
}

if credsConfig.Region == "" {
Expand Down Expand Up @@ -74,8 +100,8 @@
}, nil
}

func nonCachedClientIAM(ctx context.Context, s logical.Storage, logger hclog.Logger) (*iam.IAM, error) {
awsConfig, err := getRootConfig(ctx, s, "iam", logger)
func (b *backend) nonCachedClientIAM(ctx context.Context, s logical.Storage, logger hclog.Logger) (*iam.IAM, error) {
awsConfig, err := b.getRootConfig(ctx, s, "iam", logger)
if err != nil {
return nil, err
}
Expand All @@ -90,8 +116,8 @@
return client, nil
}

func nonCachedClientSTS(ctx context.Context, s logical.Storage, logger hclog.Logger) (*sts.STS, error) {
awsConfig, err := getRootConfig(ctx, s, "sts", logger)
func (b *backend) nonCachedClientSTS(ctx context.Context, s logical.Storage, logger hclog.Logger) (*sts.STS, error) {
awsConfig, err := b.getRootConfig(ctx, s, "sts", logger)
if err != nil {
return nil, err
}
Expand All @@ -105,3 +131,36 @@
}
return client, nil
}

// PluginIdentityTokenFetcher fetches plugin identity tokens from Vault. It is provided
// to the AWS SDK client to keep assumed role credentials refreshed through expiration.
// When the client's STS credentials expire, it will use this interface to fetch a new
// plugin identity token and exchange it for new STS credentials.
type PluginIdentityTokenFetcher struct {
sys logical.SystemView
logger hclog.Logger
audience string
ns *namespace.Namespace
ttl time.Duration
}

var _ stscreds.TokenFetcher = (*PluginIdentityTokenFetcher)(nil)

func (f PluginIdentityTokenFetcher) FetchToken(ctx aws.Context) ([]byte, error) {
nsCtx := namespace.ContextWithNamespace(ctx, f.ns)
resp, err := f.sys.GenerateIdentityToken(nsCtx, &pluginutil.IdentityTokenRequest{
Audience: f.audience,
TTL: f.ttl,
})
if err != nil {
return nil, fmt.Errorf("failed to generate plugin identity token: %w", err)
}
f.logger.Info("fetched new plugin identity token")

if resp.TTL < f.ttl {
f.logger.Debug("generated plugin identity token has shorter TTL than requested",
"requested", f.ttl, "actual", resp.TTL)
}

return []byte(resp.Token.Token()), nil
}
33 changes: 30 additions & 3 deletions builtin/logical/aws/path_config_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import (
"context"

"github.com/aws/aws-sdk-go/aws"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"github.com/hashicorp/vault/sdk/logical"
)

// A single default template that supports both the different credential types (IAM/STS) that are capped at differing length limits (64 chars/32 chars respectively)
const defaultUserNameTemplate = `{{ if (eq .Type "STS") }}{{ printf "vault-%s-%s" (unix_time) (random 20) | truncate 32 }}{{ else }}{{ printf "vault-%s-%s-%s" (printf "%s-%s" (.DisplayName) (.PolicyName) | truncate 42) (unix_time) (random 20) | truncate 64 }}{{ end }}`

func pathConfigRoot(b *backend) *framework.Path {
return &framework.Path{
p := &framework.Path{
Pattern: "config/root",

DisplayAttrs: &framework.DisplayAttributes{
Expand Down Expand Up @@ -54,6 +56,10 @@ func pathConfigRoot(b *backend) *framework.Path {
Type: framework.TypeString,
Description: "Template to generate custom IAM usernames",
},
"role_arn": {
Type: framework.TypeString,
Description: "Role ARN to assume for plugin identity token federation",
},
},

Operations: map[logical.Operation]framework.OperationHandler{
Expand All @@ -75,6 +81,9 @@ func pathConfigRoot(b *backend) *framework.Path {
HelpSynopsis: pathConfigRootHelpSyn,
HelpDescription: pathConfigRootHelpDesc,
}
pluginidentityutil.AddPluginIdentityTokenFields(p.Fields)

return p
}

func (b *backend) pathConfigRootRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
Expand Down Expand Up @@ -102,7 +111,10 @@ func (b *backend) pathConfigRootRead(ctx context.Context, req *logical.Request,
"sts_endpoint": config.STSEndpoint,
"max_retries": config.MaxRetries,
"username_template": config.UsernameTemplate,
"role_arn": config.RoleARN,
}

config.PopulatePluginIdentityTokenData(configData)
return &logical.Response{
Data: configData,
}, nil
Expand All @@ -113,6 +125,7 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
iamendpoint := data.Get("iam_endpoint").(string)
stsendpoint := data.Get("sts_endpoint").(string)
maxretries := data.Get("max_retries").(int)
roleARN := data.Get("role_arn").(string)
usernameTemplate := data.Get("username_template").(string)
if usernameTemplate == "" {
usernameTemplate = defaultUserNameTemplate
Expand All @@ -121,15 +134,26 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
b.clientMutex.Lock()
defer b.clientMutex.Unlock()

entry, err := logical.StorageEntryJSON("config/root", rootConfig{
rc := rootConfig{
AccessKey: data.Get("access_key").(string),
SecretKey: data.Get("secret_key").(string),
IAMEndpoint: iamendpoint,
STSEndpoint: stsendpoint,
Region: region,
MaxRetries: maxretries,
UsernameTemplate: usernameTemplate,
})
RoleARN: roleARN,
}
if err := rc.ParsePluginIdentityTokenFields(data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}

// return error for mutually exclusive fields if provided
if rc.IdentityTokenAudience != "" && rc.AccessKey != "" {
return logical.ErrorResponse("must specify either 'access_key' or 'identity_token_audience', not both"), nil
}

entry, err := logical.StorageEntryJSON("config/root", rc)
if err != nil {
return nil, err
}
Expand All @@ -147,13 +171,16 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request,
}

type rootConfig struct {
pluginidentityutil.PluginIdentityTokenParams

AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
IAMEndpoint string `json:"iam_endpoint"`
STSEndpoint string `json:"sts_endpoint"`
Region string `json:"region"`
MaxRetries int `json:"max_retries"`
UsernameTemplate string `json:"username_template"`
RoleARN string `json:"role_arn"`
}

const pathConfigRootHelpSyn = `
Expand Down
74 changes: 74 additions & 0 deletions builtin/logical/aws/path_config_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package aws
import (
"context"
"reflect"
"strings"
"testing"

"github.com/hashicorp/vault/sdk/logical"
Expand Down Expand Up @@ -56,3 +57,76 @@ func TestBackend_PathConfigRoot(t *testing.T) {
t.Errorf("bad: expected to read config root as %#v, got %#v instead", configData, resp.Data)
}
}

func TestBackend_PathConfigRootIDToken(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}

b := Backend(config)
if err := b.Setup(context.Background(), config); err != nil {
t.Fatal(err)
}

// basic case
configData := map[string]interface{}{
// Vault can take different integers as input for ttl
// it returns an int64 value. Cast here for easier comparison below
"identity_token_ttl": int64(10),
"identity_token_audience": "test-aud",
"role_arn": "test-role-arn",
}

configReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: config.StorageView,
Path: "config/root",
Data: configData,
}

resp, err := b.HandleRequest(context.Background(), configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: config writing failed: resp:%#v\n err: %v", resp, err)
}

resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Storage: config.StorageView,
Path: "config/root",
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: config reading failed: resp:%#v\n err: %v", resp, err)
}

// Grab the subset of fields from the response we care to look at for this case
got := map[string]interface{}{
"identity_token_ttl": resp.Data["identity_token_ttl"],
"identity_token_audience": resp.Data["identity_token_audience"],
"role_arn": resp.Data["role_arn"],
}

if !reflect.DeepEqual(got, configData) {
t.Errorf("bad: expected to read config root as %#v, got %#v instead", configData, resp.Data)
}

// mutually exclusive fields provided
configData = map[string]interface{}{
"identity_token_audience": "test-aud",
"access_key": "ASIAIO10230XVB",
}

configReq = &logical.Request{
Operation: logical.UpdateOperation,
Storage: config.StorageView,
Path: "config/root",
Data: configData,
}

resp, err = b.HandleRequest(context.Background(), configReq)
if !resp.IsError() {
t.Fatalf("expected an error but got nil")
}
expectedError := "must specify either 'access_key' or 'identity_token_audience'"
if !strings.Contains(resp.Error().Error(), expectedError) {
t.Fatalf("expected errr %s, got %s", expectedError, err)
}
}
Loading