secrets: pass key/value config data to plugins as env (#26455)

Co-authored-by: Michael Schurter <mschurter@hashicorp.com>
Co-authored-by: Tim Gross <tgross@hashicorp.com>
This commit is contained in:
Michael Smithhisler
2025-08-18 11:11:21 -04:00
parent e9e1631b8c
commit 10ed46cbd4
16 changed files with 231 additions and 54 deletions

View File

@@ -198,7 +198,7 @@ func (h *secretsHook) buildSecretProviders(secretDir string) ([]TemplateProvider
tmplProvider = append(tmplProvider, p)
}
default:
plug, err := commonplugins.NewExternalSecretsPlugin(h.clientConfig.CommonPluginDir, s.Provider)
plug, err := commonplugins.NewExternalSecretsPlugin(h.clientConfig.CommonPluginDir, s.Provider, s.Env)
if err != nil {
multierror.Append(mErr, err)
continue

View File

@@ -41,9 +41,16 @@ type externalSecretsPlugin struct {
// pluginPath is the path on the host to the plugin executable
pluginPath string
// env is optional envVars passed to the plugin process
env map[string]string
}
func NewExternalSecretsPlugin(commonPluginDir string, name string) (*externalSecretsPlugin, error) {
// NewExternalSecretsPlugin creates an instance of a secrets plugin by validating the plugin
// binary exists and is executable, and parsing any string key/value pairs out of the config
// which will be used as environment variables for Fetch.
func NewExternalSecretsPlugin(commonPluginDir string, name string, env map[string]string) (*externalSecretsPlugin, error) {
// validate plugin
executable := filepath.Join(commonPluginDir, SecretsPluginDir, name)
f, err := os.Stat(executable)
if err != nil {
@@ -58,6 +65,7 @@ func NewExternalSecretsPlugin(commonPluginDir string, name string) (*externalSec
return &externalSecretsPlugin{
pluginPath: executable,
env: env,
}, nil
}
@@ -96,6 +104,10 @@ func (e *externalSecretsPlugin) Fetch(ctx context.Context, path string) (*Secret
"CPI_OPERATION=fetch",
}
for env, val := range e.env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", env, val))
}
stdout, stderr, err := runPlugin(cmd, SecretsKillTimeout)
if err != nil {
return nil, err

View File

@@ -21,7 +21,7 @@ func TestExternalSecretsPlugin_Fingerprint(t *testing.T) {
t.Run("runs successfully", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `{"type": "secrets", "version": "1.0.0"}`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
res, err := plugin.Fingerprint(context.Background())
@@ -34,7 +34,7 @@ func TestExternalSecretsPlugin_Fingerprint(t *testing.T) {
t.Run("errors on non-zero exit code", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\nexit 1\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
res, err := plugin.Fingerprint(context.Background())
@@ -45,7 +45,7 @@ func TestExternalSecretsPlugin_Fingerprint(t *testing.T) {
t.Run("errors on timeout", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\nleep .5\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
@@ -58,7 +58,7 @@ func TestExternalSecretsPlugin_Fingerprint(t *testing.T) {
t.Run("errors on invalid json", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\ncat <<EOF\ninvalid\nEOF\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
res, err := plugin.Fingerprint(context.Background())
@@ -73,7 +73,7 @@ func TestExternalSecretsPlugin_Fetch(t *testing.T) {
t.Run("runs successfully", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `{"result": {"key": "value"}}`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
res, err := plugin.Fetch(context.Background(), "test-path")
@@ -86,7 +86,7 @@ func TestExternalSecretsPlugin_Fetch(t *testing.T) {
t.Run("errors on non-zero exit code", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\nexit 1\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
_, err = plugin.Fetch(context.Background(), "test-path")
@@ -96,7 +96,7 @@ func TestExternalSecretsPlugin_Fetch(t *testing.T) {
t.Run("errors on timeout", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\nsleep .5\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
@@ -109,12 +109,24 @@ func TestExternalSecretsPlugin_Fetch(t *testing.T) {
t.Run("errors on timeout", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `invalid`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, nil)
must.NoError(t, err)
_, err = plugin.Fetch(context.Background(), "dummy-path")
must.Error(t, err)
})
t.Run("can be passed environment variables via config", func(t *testing.T) {
// test the passed envVar is parsed and set correctly by printing it as part of the SecretResponse
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `{"result": {"foo": "$TEST_KEY"}}`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName, map[string]string{"TEST_KEY": "TEST_VALUE"})
must.NoError(t, err)
res, err := plugin.Fetch(context.Background(), "dummy-path")
must.NoError(t, err)
must.Eq(t, res.Result, map[string]string{"foo": "TEST_VALUE"})
})
}
func setupTestPlugin(t *testing.T, b []byte) (string, string) {

View File

@@ -53,7 +53,7 @@ func (s *SecretsPluginFingerprint) Fingerprint(request *FingerprintRequest, resp
// map of plugin names to fingerprinted versions
plugins := map[string]string{}
for name := range files {
plug, err := commonplugins.NewExternalSecretsPlugin(request.Config.CommonPluginDir, name)
plug, err := commonplugins.NewExternalSecretsPlugin(request.Config.CommonPluginDir, name, nil)
if err != nil {
return err
}