vault/sdk/helper/pluginutil/run_config.go
Tom Proctor d8f32855d2
Make plugin-specific env take precedence over sys env (#25128)
* Make plugin-specific env take precedence over sys env
* Expand the existing plugin env integration test

---------

Co-authored-by: Austin Gebauer <34121980+austingebauer@users.noreply.github.com>
2024-02-02 11:20:32 +00:00

317 lines
8.1 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pluginutil
import (
"context"
"crypto/sha256"
"crypto/tls"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/go-secure-stdlib/plugincontainer"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/pluginruntimeutil"
)
const (
// Labels for plugin container ownership
labelVaultPID = "com.hashicorp.vault.pid"
labelVaultClusterID = "com.hashicorp.vault.cluster.id"
labelVaultPluginName = "com.hashicorp.vault.plugin.name"
labelVaultPluginVersion = "com.hashicorp.vault.plugin.version"
labelVaultPluginType = "com.hashicorp.vault.plugin.type"
)
type PluginClientConfig struct {
Name string
PluginType consts.PluginType
Version string
PluginSets map[int]plugin.PluginSet
HandshakeConfig plugin.HandshakeConfig
Logger log.Logger
IsMetadataMode bool
AutoMTLS bool
MLock bool
Wrapper RunnerUtil
}
type runConfig struct {
// Provided by PluginRunner
command string
image string
imageTag string
args []string
sha256 []byte
// Initialized with what's in PluginRunner.Env, but can be added to
env []string
runtimeConfig *pluginruntimeutil.PluginRuntimeConfig
PluginClientConfig
tmpdir string
}
func (rc runConfig) mlockEnabled() bool {
return rc.MLock || (rc.Wrapper != nil && rc.Wrapper.MlockEnabled())
}
func (rc runConfig) generateCmd(ctx context.Context) (cmd *exec.Cmd, clientTLSConfig *tls.Config, err error) {
cmd = exec.Command(rc.command, rc.args...)
env := rc.env
// Add the mlock setting to the ENV of the plugin
if rc.mlockEnabled() {
env = append(env, fmt.Sprintf("%s=%s", PluginMlockEnabled, "true"))
}
version, err := rc.Wrapper.VaultVersion(ctx)
if err != nil {
return nil, nil, err
}
env = append(env, fmt.Sprintf("%s=%s", PluginVaultVersionEnv, version))
if rc.IsMetadataMode {
rc.Logger = rc.Logger.With("metadata", "true")
}
metadataEnv := fmt.Sprintf("%s=%t", PluginMetadataModeEnv, rc.IsMetadataMode)
env = append(env, metadataEnv)
automtlsEnv := fmt.Sprintf("%s=%t", PluginAutoMTLSEnv, rc.AutoMTLS)
env = append(env, automtlsEnv)
if !rc.AutoMTLS && !rc.IsMetadataMode {
// Get a CA TLS Certificate
certBytes, key, err := generateCert()
if err != nil {
return nil, nil, err
}
// Use CA to sign a client cert and return a configured TLS config
clientTLSConfig, err = createClientTLSConfig(certBytes, key)
if err != nil {
return nil, nil, err
}
// Use CA to sign a server cert and wrap the values in a response wrapped
// token.
wrapToken, err := wrapServerConfig(ctx, rc.Wrapper, certBytes, key)
if err != nil {
return nil, nil, err
}
// Add the response wrap token to the ENV of the plugin
env = append(env, fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, wrapToken))
}
if rc.image == "" {
// go-plugin has always overridden user-provided env vars with the OS
// (Vault process) env vars, but we want plugins to be able to override
// the Vault process env. We don't want to make a breaking change in
// go-plugin so always set SkipHostEnv and replicate the legacy behavior
// ourselves if user opts in.
if legacy, _ := strconv.ParseBool(os.Getenv(PluginUseLegacyEnvLayering)); legacy {
// Env vars are layered as follows, with later entries overriding
// earlier entries if there are duplicate keys:
// 1. Env specified at plugin registration
// 2. Env from Vault SDK
// 3. Env from Vault process (OS)
// 4. Env from go-plugin
cmd.Env = append(env, os.Environ()...)
} else {
// Env vars are layered as follows, with later entries overriding
// earlier entries if there are duplicate keys:
// 1. Env from Vault process (OS)
// 2. Env specified at plugin registration
// 3. Env from Vault SDK
// 4. Env from go-plugin
cmd.Env = append(os.Environ(), env...)
}
} else {
// Containerized plugins do not inherit any env vars from Vault.
cmd.Env = env
}
return cmd, clientTLSConfig, nil
}
func (rc runConfig) makeConfig(ctx context.Context) (*plugin.ClientConfig, error) {
cmd, clientTLSConfig, err := rc.generateCmd(ctx)
if err != nil {
return nil, err
}
clientConfig := &plugin.ClientConfig{
HandshakeConfig: rc.HandshakeConfig,
VersionedPlugins: rc.PluginSets,
TLSConfig: clientTLSConfig,
Logger: rc.Logger,
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC,
plugin.ProtocolGRPC,
},
AutoMTLS: rc.AutoMTLS,
SkipHostEnv: true,
}
if rc.image == "" {
clientConfig.Cmd = cmd
clientConfig.SecureConfig = &plugin.SecureConfig{
Checksum: rc.sha256,
Hash: sha256.New(),
}
} else {
containerCfg, err := rc.containerConfig(ctx, cmd.Env)
if err != nil {
return nil, err
}
clientConfig.RunnerFunc = containerCfg.NewContainerRunner
clientConfig.UnixSocketConfig = &plugin.UnixSocketConfig{
Group: strconv.Itoa(containerCfg.GroupAdd),
TempDir: rc.tmpdir,
}
clientConfig.GRPCBrokerMultiplex = true
}
return clientConfig, nil
}
func (rc runConfig) containerConfig(ctx context.Context, env []string) (*plugincontainer.Config, error) {
clusterID, err := rc.Wrapper.ClusterID(ctx)
if err != nil {
return nil, err
}
cfg := &plugincontainer.Config{
Image: rc.image,
Tag: rc.imageTag,
SHA256: fmt.Sprintf("%x", rc.sha256),
Env: env,
GroupAdd: os.Getegid(),
Runtime: consts.DefaultContainerPluginOCIRuntime,
CapIPCLock: rc.mlockEnabled(),
Labels: map[string]string{
labelVaultPID: strconv.Itoa(os.Getpid()),
labelVaultClusterID: clusterID,
labelVaultPluginName: rc.PluginClientConfig.Name,
labelVaultPluginType: rc.PluginClientConfig.PluginType.String(),
labelVaultPluginVersion: rc.PluginClientConfig.Version,
},
}
// Use rc.command and rc.args directly instead of cmd.Path and cmd.Args, as
// exec.Command may mutate the provided command.
if rc.command != "" {
cfg.Entrypoint = []string{rc.command}
}
if len(rc.args) > 0 {
cfg.Args = rc.args
}
if rc.runtimeConfig != nil {
cfg.CgroupParent = rc.runtimeConfig.CgroupParent
cfg.NanoCpus = rc.runtimeConfig.CPU
cfg.Memory = rc.runtimeConfig.Memory
if rc.runtimeConfig.OCIRuntime != "" {
cfg.Runtime = rc.runtimeConfig.OCIRuntime
}
if rc.runtimeConfig.Rootless {
cfg.Rootless = true
}
}
return cfg, nil
}
func (rc runConfig) run(ctx context.Context) (*plugin.Client, error) {
clientConfig, err := rc.makeConfig(ctx)
if err != nil {
return nil, err
}
client := plugin.NewClient(clientConfig)
return client, nil
}
type RunOpt func(*runConfig)
func Env(env ...string) RunOpt {
return func(rc *runConfig) {
rc.env = append(rc.env, env...)
}
}
func Runner(wrapper RunnerUtil) RunOpt {
return func(rc *runConfig) {
rc.Wrapper = wrapper
}
}
func PluginSets(pluginSets map[int]plugin.PluginSet) RunOpt {
return func(rc *runConfig) {
rc.PluginSets = pluginSets
}
}
func HandshakeConfig(hs plugin.HandshakeConfig) RunOpt {
return func(rc *runConfig) {
rc.HandshakeConfig = hs
}
}
func Logger(logger log.Logger) RunOpt {
return func(rc *runConfig) {
rc.Logger = logger
}
}
func MetadataMode(isMetadataMode bool) RunOpt {
return func(rc *runConfig) {
rc.IsMetadataMode = isMetadataMode
}
}
func AutoMTLS(autoMTLS bool) RunOpt {
return func(rc *runConfig) {
rc.AutoMTLS = autoMTLS
}
}
func MLock(mlock bool) RunOpt {
return func(rc *runConfig) {
rc.MLock = mlock
}
}
func (r *PluginRunner) RunConfig(ctx context.Context, opts ...RunOpt) (*plugin.Client, error) {
var image, imageTag string
if r.OCIImage != "" {
image = r.OCIImage
imageTag = strings.TrimPrefix(r.Version, "v")
}
rc := runConfig{
command: r.Command,
image: image,
imageTag: imageTag,
args: r.Args,
sha256: r.Sha256,
env: r.Env,
runtimeConfig: r.RuntimeConfig,
tmpdir: r.Tmpdir,
PluginClientConfig: PluginClientConfig{
Name: r.Name,
PluginType: r.Type,
Version: r.Version,
},
}
for _, opt := range opts {
opt(&rc)
}
return rc.run(ctx)
}