Skip to content

Commit

Permalink
plugin workload identity (#188)
Browse files Browse the repository at this point in the history
* plugin workload identity

go get latest vault sdk for plugin wif changes

pass context to assertion func

* fix test build; handle mutual exclisivity

* fix failing tests

* add mutual exclusion test case

* go mod tidy and add wif happy path test

* revert to previous test equal implementation

* add check for ErrPluginWorkloadIdentityUnsupported

* enable WIF for test sysview

* ensure token TTL is set to clientSettings

* changelog

---------

Co-authored-by: Austin Gebauer <[email protected]>
Co-authored-by: Vinay Gopalan <[email protected]>
  • Loading branch information
3 people authored May 8, 2024
1 parent 149383c commit 5a25f04
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 88 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

FEATURES:
* Adds secret-less configuration of Azure secret engine using plugin Workload Identity Federation (https://github.com/hashicorp/vault-plugin-secrets-azure/pull/188)

## v0.18.1

BUG FIXES:
Expand Down
7 changes: 4 additions & 3 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sync"
"time"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/locksutil"
Expand All @@ -26,7 +27,7 @@ const (
type azureSecretBackend struct {
*framework.Backend

getProvider func(*clientSettings) (AzureProvider, error)
getProvider func(context.Context, hclog.Logger, logical.SystemView, *clientSettings) (AzureProvider, error)
client *client
settings *clientSettings
lock sync.RWMutex
Expand All @@ -46,7 +47,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
}

func backend() *azureSecretBackend {
var b = azureSecretBackend{
b := azureSecretBackend{
updatePassword: true,
}

Expand Down Expand Up @@ -228,7 +229,7 @@ func (b *azureSecretBackend) getClient(ctx context.Context, s logical.Storage) (
return nil, fmt.Errorf("config is nil")
}

p, err := b.getProvider(b.settings)
p, err := b.getProvider(ctx, b.Logger(), b.System(), b.settings)
if err != nil {
return nil, err
}
Expand Down
27 changes: 18 additions & 9 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"testing"
"time"

"github.com/hashicorp/go-hclog"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)

Expand All @@ -25,15 +27,23 @@ var (
testClientSecret = "testClientSecret"
)

type testSystemViewEnt struct {
logical.StaticSystemView
}

func (d testSystemViewEnt) GenerateIdentityToken(_ context.Context, _ *pluginutil.IdentityTokenRequest) (*pluginutil.IdentityTokenResponse, error) {
return &pluginutil.IdentityTokenResponse{}, nil
}

func getTestBackendMocked(t *testing.T, initConfig bool) (*azureSecretBackend, logical.Storage) {
b := backend()
sysView := testSystemViewEnt{}
sysView.DefaultLeaseTTLVal = defaultLeaseTTLHr
sysView.MaxLeaseTTLVal = maxLeaseTTLHr

config := &logical.BackendConfig{
Logger: logging.NewVaultLogger(log.Trace),
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLHr,
MaxLeaseTTLVal: maxLeaseTTLHr,
},
Logger: logging.NewVaultLogger(log.Trace),
System: &sysView,
StorageView: &logical.InmemStorage{},
}
err := b.Setup(context.Background(), config)
Expand All @@ -43,7 +53,7 @@ func getTestBackendMocked(t *testing.T, initConfig bool) (*azureSecretBackend, l

b.settings = new(clientSettings)
mockProvider := newMockProvider()
b.getProvider = func(s *clientSettings) (AzureProvider, error) {
b.getProvider = func(context.Context, hclog.Logger, logical.SystemView, *clientSettings) (AzureProvider, error) {
return mockProvider, nil
}

Expand All @@ -58,7 +68,7 @@ func getTestBackendMocked(t *testing.T, initConfig bool) (*azureSecretBackend, l
"max_ttl": defaultTestMaxTTL,
}

testConfigCreate(t, b, config.StorageView, cfg)
testConfigCreate(t, b, config.StorageView, cfg, false)
}

return b, config.StorageView
Expand Down Expand Up @@ -101,14 +111,13 @@ func TestPeriodicFuncNilConfig(t *testing.T) {

b.settings = new(clientSettings)
mockProvider := newMockProvider()
b.getProvider = func(s *clientSettings) (AzureProvider, error) {
b.getProvider = func(context.Context, hclog.Logger, logical.SystemView, *clientSettings) (AzureProvider, error) {
return mockProvider, nil
}

err = b.periodicFunc(context.Background(), &logical.Request{
Storage: config.StorageView,
})

if err != nil {
t.Fatalf("periodicFunc error not nil, it should have been: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ vault write "${PLUGIN_PATH}"/roles/dev-role ttl="5m" azure_roles=-<<EOF
[
{
"role_name": "Storage Blob Data Owner",
"scope": "/subscriptions/${SUBSCRIPTION_ID}"
"scope": "/subscriptions/${AZURE_SUBSCRIPTION_ID}"
}
]
EOF
Expand Down
5 changes: 5 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"github.com/hashicorp/vault/sdk/logical"

"github.com/hashicorp/vault-plugin-secrets-azure/api"
Expand Down Expand Up @@ -296,6 +297,8 @@ func (c *client) findGroups(ctx context.Context, groupName string) ([]api.Group,
// clientSettings is used by a client to configure the connections to Azure.
// It is created from a combination of Vault config settings and environment variables.
type clientSettings struct {
pluginidentityutil.PluginIdentityTokenParams

SubscriptionID string
TenantID string
ClientID string
Expand All @@ -321,6 +324,8 @@ func (b *azureSecretBackend) getClientSettings(ctx context.Context, config *azur

settings.ClientID = firstAvailable(os.Getenv("AZURE_CLIENT_ID"), config.ClientID)
settings.ClientSecret = firstAvailable(os.Getenv("AZURE_CLIENT_SECRET"), config.ClientSecret)
settings.IdentityTokenAudience = config.IdentityTokenAudience
settings.IdentityTokenTTL = config.IdentityTokenTTL

settings.SubscriptionID = firstAvailable(os.Getenv("AZURE_SUBSCRIPTION_ID"), config.SubscriptionID)
if settings.SubscriptionID == "" {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/microsoftgraph/msgraph-sdk-go v1.40.0
github.com/microsoftgraph/msgraph-sdk-go-core v1.1.0
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.9.0
)

require (
Expand Down Expand Up @@ -85,7 +86,6 @@ require (
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sasha-s/go-deadlock v0.2.0 // indirect
github.com/std-uritemplate/std-uritemplate/go v0.0.55 // indirect
github.com/stretchr/testify v1.9.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
Expand Down
39 changes: 34 additions & 5 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/hashicorp/go-multierror"
"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"
)

Expand All @@ -25,6 +27,8 @@ const (
// defaults for roles. The zero value is useful and results in
// environments variable and system defaults being used.
type azureConfig struct {
pluginidentityutil.PluginIdentityTokenParams

SubscriptionID string `json:"subscription_id"`
TenantID string `json:"tenant_id"`
ClientID string `json:"client_id"`
Expand All @@ -40,7 +44,7 @@ type azureConfig struct {
}

func pathConfig(b *azureSecretBackend) *framework.Path {
return &framework.Path{
p := &framework.Path{
Pattern: "config",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixAzure,
Expand Down Expand Up @@ -108,6 +112,9 @@ func pathConfig(b *azureSecretBackend) *framework.Path {
HelpSynopsis: confHelpSyn,
HelpDescription: confHelpDesc,
}
pluginidentityutil.AddPluginIdentityTokenFields(p.Fields)

return p
}

func (b *azureSecretBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
Expand Down Expand Up @@ -156,6 +163,27 @@ func (b *azureSecretBackend) pathConfigWrite(ctx context.Context, req *logical.R
config.RootPasswordTTL = defaultRootPasswordTTL
}

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

if config.IdentityTokenAudience != "" && config.ClientSecret != "" {
return logical.ErrorResponse("only one of 'client_secret' or 'identity_token_audience' can be set"), nil
}

// generate token to check if WIF is enabled on this edition of Vault
if config.IdentityTokenAudience != "" {
_, err := b.System().GenerateIdentityToken(ctx, &pluginutil.IdentityTokenRequest{
Audience: config.IdentityTokenAudience,
})
if err != nil {
if errors.Is(err, pluginidentityutil.ErrPluginWorkloadIdentityUnsupported) {
return logical.ErrorResponse(err.Error()), nil
}
return nil, err
}
}

if merr.ErrorOrNil() != nil {
return logical.ErrorResponse(merr.Error()), nil
}
Expand All @@ -170,7 +198,6 @@ func (b *azureSecretBackend) pathConfigWrite(ctx context.Context, req *logical.R

func (b *azureSecretBackend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.getConfig(ctx, req.Storage)

if err != nil {
return nil, err
}
Expand All @@ -188,6 +215,7 @@ func (b *azureSecretBackend) pathConfigRead(ctx context.Context, req *logical.Re
"root_password_ttl": int(config.RootPasswordTTL.Seconds()),
},
}
config.PopulatePluginIdentityTokenData(resp.Data)

if !config.RootPasswordExpirationDate.IsZero() {
resp.Data["root_password_expiration_date"] = config.RootPasswordExpirationDate
Expand Down Expand Up @@ -235,7 +263,6 @@ func (b *azureSecretBackend) getConfig(ctx context.Context, s logical.Storage) (

func (b *azureSecretBackend) saveConfig(ctx context.Context, config *azureConfig, s logical.Storage) error {
entry, err := logical.StorageEntryJSON(configStoragePath, config)

if err != nil {
return err
}
Expand All @@ -252,9 +279,11 @@ func (b *azureSecretBackend) saveConfig(ctx context.Context, config *azureConfig
return nil
}

const confHelpSyn = `Configure the Azure Secret backend.`
const confHelpDesc = `
const (
confHelpSyn = `Configure the Azure Secret backend.`
confHelpDesc = `
The Azure secret backend requires credentials for managing applications and
service principals. This endpoint is used to configure those credentials as
well as default values for the backend in general.
`
)
Loading

0 comments on commit 5a25f04

Please sign in to comment.