Skip to content

Commit

Permalink
Add WIF support for AWS Auth (#26507)
Browse files Browse the repository at this point in the history
* Add wif support

* update cli + add stubs

* revert cli changes + add changelog

* update with suggestions
  • Loading branch information
Zlaticanin authored May 9, 2024
1 parent 3150c32 commit bdc16c3
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 12 deletions.
58 changes: 58 additions & 0 deletions builtin/credential/aws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package awsauth
import (
"context"
"fmt"
"strconv"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
Expand All @@ -14,7 +16,10 @@ import (
"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-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"
)

Expand Down Expand Up @@ -58,6 +63,26 @@ func (b *backend) getRawClientConfig(ctx context.Context, s logical.Storage, reg
credsConfig.AccessKey = config.AccessKey
credsConfig.SecretKey = config.SecretKey
maxRetries = config.MaxRetries

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-auth-%s", sessionSuffix)
credsConfig.WebIdentityTokenFetcher = fetcher
credsConfig.RoleARN = config.RoleARN
}
}

credsConfig.HTTPClient = cleanhttp.DefaultClient()
Expand Down Expand Up @@ -302,3 +327,36 @@ func (b *backend) clientIAM(ctx context.Context, s logical.Storage, region, acco
}
return b.IAMClientsMap[region][stsRole], 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
}
77 changes: 65 additions & 12 deletions builtin/credential/aws/path_config_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)

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

DisplayAttrs: &framework.DisplayAttributes{
Expand Down Expand Up @@ -85,6 +87,12 @@ func (b *backend) pathConfigClient() *framework.Path {
Default: aws.UseServiceDefaultRetries,
Description: "Maximum number of retries for recoverable exceptions of AWS APIs",
},

"role_arn": {
Type: framework.TypeString,
Default: "",
Description: "Role ARN to assume for plugin identity token federation",
},
},

ExistenceCheck: b.pathConfigClientExistenceCheck,
Expand Down Expand Up @@ -121,6 +129,9 @@ func (b *backend) pathConfigClient() *framework.Path {
HelpSynopsis: pathConfigClientHelpSyn,
HelpDescription: pathConfigClientHelpDesc,
}
pluginidentityutil.AddPluginIdentityTokenFields(p.Fields)

return p
}

// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
Expand Down Expand Up @@ -168,18 +179,22 @@ func (b *backend) pathConfigClientRead(ctx context.Context, req *logical.Request
return nil, nil
}

configData := map[string]interface{}{
"access_key": clientConfig.AccessKey,
"endpoint": clientConfig.Endpoint,
"iam_endpoint": clientConfig.IAMEndpoint,
"sts_endpoint": clientConfig.STSEndpoint,
"sts_region": clientConfig.STSRegion,
"use_sts_region_from_client": clientConfig.UseSTSRegionFromClient,
"iam_server_id_header_value": clientConfig.IAMServerIdHeaderValue,
"max_retries": clientConfig.MaxRetries,
"allowed_sts_header_values": clientConfig.AllowedSTSHeaderValues,
"role_arn": clientConfig.RoleARN,
}

clientConfig.PopulatePluginIdentityTokenData(configData)
return &logical.Response{
Data: map[string]interface{}{
"access_key": clientConfig.AccessKey,
"endpoint": clientConfig.Endpoint,
"iam_endpoint": clientConfig.IAMEndpoint,
"sts_endpoint": clientConfig.STSEndpoint,
"sts_region": clientConfig.STSRegion,
"use_sts_region_from_client": clientConfig.UseSTSRegionFromClient,
"iam_server_id_header_value": clientConfig.IAMServerIdHeaderValue,
"max_retries": clientConfig.MaxRetries,
"allowed_sts_header_values": clientConfig.AllowedSTSHeaderValues,
},
Data: configData,
}, nil
}

Expand Down Expand Up @@ -334,6 +349,41 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical
configEntry.MaxRetries = data.Get("max_retries").(int)
}

roleArnStr, ok := data.GetOk("role_arn")
if ok {
if configEntry.RoleARN != roleArnStr.(string) {
changedCreds = true
configEntry.RoleARN = roleArnStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.RoleARN = data.Get("role_arn").(string)
}

if err := configEntry.ParsePluginIdentityTokenFields(data); err != nil {
return logical.ErrorResponse(err.Error()), nil
}

// handle mutual exclusivity
if configEntry.IdentityTokenAudience != "" && configEntry.AccessKey != "" {
return logical.ErrorResponse("only one of 'access_key' or 'identity_token_audience' can be set"), nil
}

if configEntry.IdentityTokenAudience != "" && configEntry.RoleARN == "" {
return logical.ErrorResponse("role_arn must be set when identity_token_audience is set"), nil
}

if configEntry.IdentityTokenAudience != "" {
_, err := b.System().GenerateIdentityToken(ctx, &pluginutil.IdentityTokenRequest{
Audience: configEntry.IdentityTokenAudience,
})
if err != nil {
if errors.Is(err, pluginidentityutil.ErrPluginWorkloadIdentityUnsupported) {
return logical.ErrorResponse(err.Error()), nil
}
return nil, err
}
}

// Since this endpoint supports both create operation and update operation,
// the error checks for access_key and secret_key not being set are not present.
// This allows calling this endpoint multiple times to provide the values.
Expand Down Expand Up @@ -373,6 +423,8 @@ func (b *backend) configClientToEntry(conf *clientConfig) (*logical.StorageEntry
// Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to
// interact with the AWS EC2 API.
type clientConfig struct {
pluginidentityutil.PluginIdentityTokenParams

AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Endpoint string `json:"endpoint"`
Expand All @@ -383,6 +435,7 @@ type clientConfig struct {
IAMServerIdHeaderValue string `json:"iam_server_id_header_value"`
AllowedSTSHeaderValues []string `json:"allowed_sts_header_values"`
MaxRetries int `json:"max_retries"`
RoleARN string `json:"role_arn"`
}

func (c *clientConfig) validateAllowedSTSHeaderValues(headers http.Header) error {
Expand Down
47 changes: 47 additions & 0 deletions builtin/credential/aws/path_config_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (
"context"
"testing"

"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/assert"
)

func TestBackend_pathConfigClient(t *testing.T) {
Expand Down Expand Up @@ -129,3 +132,47 @@ func TestBackend_pathConfigClient(t *testing.T) {
data["sts_region"], resp.Data["sts_region"])
}
}

// TestBackend_PathConfigClient_PluginIdentityToken tests that configuration
// of plugin WIF returns an immediate error.
func TestBackend_PathConfigClient_PluginIdentityToken(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = &testSystemView{}

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

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

configData := map[string]interface{}{
"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/client",
Data: configData,
}

resp, err := b.HandleRequest(context.Background(), configReq)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.ErrorContains(t, resp.Error(), pluginidentityutil.ErrPluginWorkloadIdentityUnsupported.Error())
}

type testSystemView struct {
logical.StaticSystemView
}

func (d testSystemView) GenerateIdentityToken(_ context.Context, _ *pluginutil.IdentityTokenRequest) (*pluginutil.IdentityTokenResponse, error) {
return nil, pluginidentityutil.ErrPluginWorkloadIdentityUnsupported
}
3 changes: 3 additions & 0 deletions changelog/26507.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**Plugin Identity Tokens**: Adds secret-less configuration of AWS auth engine using web identity federation.
```

0 comments on commit bdc16c3

Please sign in to comment.