mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Add WIF support for AWS Auth (#26507)
* Add wif support * update cli + add stubs * revert cli changes + add changelog * update with suggestions
This commit is contained in:
committed by
GitHub
parent
3150c321cb
commit
bdc16c396b
@@ -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"
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
@@ -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,
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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"`
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
changelog/26507.txt
Normal file
3
changelog/26507.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:feature
|
||||
**Plugin Identity Tokens**: Adds secret-less configuration of AWS auth engine using web identity federation.
|
||||
```
|
||||
Reference in New Issue
Block a user