github.com/hashicorp/vault/sdk@v0.11.0/helper/pluginutil/run_config.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package pluginutil
     5  
     6  import (
     7  	"context"
     8  	"crypto/sha256"
     9  	"crypto/tls"
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"strconv"
    14  	"strings"
    15  
    16  	log "github.com/hashicorp/go-hclog"
    17  	"github.com/hashicorp/go-plugin"
    18  	"github.com/hashicorp/go-secure-stdlib/plugincontainer"
    19  	"github.com/hashicorp/vault/sdk/helper/consts"
    20  	"github.com/hashicorp/vault/sdk/helper/pluginruntimeutil"
    21  )
    22  
    23  const (
    24  	// Labels for plugin container ownership
    25  	labelVaultPID           = "com.hashicorp.vault.pid"
    26  	labelVaultClusterID     = "com.hashicorp.vault.cluster.id"
    27  	labelVaultPluginName    = "com.hashicorp.vault.plugin.name"
    28  	labelVaultPluginVersion = "com.hashicorp.vault.plugin.version"
    29  	labelVaultPluginType    = "com.hashicorp.vault.plugin.type"
    30  )
    31  
    32  type PluginClientConfig struct {
    33  	Name            string
    34  	PluginType      consts.PluginType
    35  	Version         string
    36  	PluginSets      map[int]plugin.PluginSet
    37  	HandshakeConfig plugin.HandshakeConfig
    38  	Logger          log.Logger
    39  	IsMetadataMode  bool
    40  	AutoMTLS        bool
    41  	MLock           bool
    42  	Wrapper         RunnerUtil
    43  }
    44  
    45  type runConfig struct {
    46  	// Provided by PluginRunner
    47  	command  string
    48  	image    string
    49  	imageTag string
    50  	args     []string
    51  	sha256   []byte
    52  
    53  	// Initialized with what's in PluginRunner.Env, but can be added to
    54  	env []string
    55  
    56  	runtimeConfig *pluginruntimeutil.PluginRuntimeConfig
    57  
    58  	PluginClientConfig
    59  	tmpdir string
    60  }
    61  
    62  func (rc runConfig) mlockEnabled() bool {
    63  	return rc.MLock || (rc.Wrapper != nil && rc.Wrapper.MlockEnabled())
    64  }
    65  
    66  func (rc runConfig) generateCmd(ctx context.Context) (cmd *exec.Cmd, clientTLSConfig *tls.Config, err error) {
    67  	cmd = exec.Command(rc.command, rc.args...)
    68  	env := rc.env
    69  
    70  	// Add the mlock setting to the ENV of the plugin
    71  	if rc.mlockEnabled() {
    72  		env = append(env, fmt.Sprintf("%s=%s", PluginMlockEnabled, "true"))
    73  	}
    74  	version, err := rc.Wrapper.VaultVersion(ctx)
    75  	if err != nil {
    76  		return nil, nil, err
    77  	}
    78  	env = append(env, fmt.Sprintf("%s=%s", PluginVaultVersionEnv, version))
    79  
    80  	if rc.IsMetadataMode {
    81  		rc.Logger = rc.Logger.With("metadata", "true")
    82  	}
    83  	metadataEnv := fmt.Sprintf("%s=%t", PluginMetadataModeEnv, rc.IsMetadataMode)
    84  	env = append(env, metadataEnv)
    85  
    86  	automtlsEnv := fmt.Sprintf("%s=%t", PluginAutoMTLSEnv, rc.AutoMTLS)
    87  	env = append(env, automtlsEnv)
    88  
    89  	if !rc.AutoMTLS && !rc.IsMetadataMode {
    90  		// Get a CA TLS Certificate
    91  		certBytes, key, err := generateCert()
    92  		if err != nil {
    93  			return nil, nil, err
    94  		}
    95  
    96  		// Use CA to sign a client cert and return a configured TLS config
    97  		clientTLSConfig, err = createClientTLSConfig(certBytes, key)
    98  		if err != nil {
    99  			return nil, nil, err
   100  		}
   101  
   102  		// Use CA to sign a server cert and wrap the values in a response wrapped
   103  		// token.
   104  		wrapToken, err := wrapServerConfig(ctx, rc.Wrapper, certBytes, key)
   105  		if err != nil {
   106  			return nil, nil, err
   107  		}
   108  
   109  		// Add the response wrap token to the ENV of the plugin
   110  		env = append(env, fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, wrapToken))
   111  	}
   112  
   113  	if rc.image == "" {
   114  		// go-plugin has always overridden user-provided env vars with the OS
   115  		// (Vault process) env vars, but we want plugins to be able to override
   116  		// the Vault process env. We don't want to make a breaking change in
   117  		// go-plugin so always set SkipHostEnv and replicate the legacy behavior
   118  		// ourselves if user opts in.
   119  		if legacy, _ := strconv.ParseBool(os.Getenv(PluginUseLegacyEnvLayering)); legacy {
   120  			// Env vars are layered as follows, with later entries overriding
   121  			// earlier entries if there are duplicate keys:
   122  			// 1. Env specified at plugin registration
   123  			// 2. Env from Vault SDK
   124  			// 3. Env from Vault process (OS)
   125  			// 4. Env from go-plugin
   126  			cmd.Env = append(env, os.Environ()...)
   127  		} else {
   128  			// Env vars are layered as follows, with later entries overriding
   129  			// earlier entries if there are duplicate keys:
   130  			// 1. Env from Vault process (OS)
   131  			// 2. Env specified at plugin registration
   132  			// 3. Env from Vault SDK
   133  			// 4. Env from go-plugin
   134  			cmd.Env = append(os.Environ(), env...)
   135  		}
   136  	} else {
   137  		// Containerized plugins do not inherit any env vars from Vault.
   138  		cmd.Env = env
   139  	}
   140  
   141  	return cmd, clientTLSConfig, nil
   142  }
   143  
   144  func (rc runConfig) makeConfig(ctx context.Context) (*plugin.ClientConfig, error) {
   145  	cmd, clientTLSConfig, err := rc.generateCmd(ctx)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	clientConfig := &plugin.ClientConfig{
   151  		HandshakeConfig:  rc.HandshakeConfig,
   152  		VersionedPlugins: rc.PluginSets,
   153  		TLSConfig:        clientTLSConfig,
   154  		Logger:           rc.Logger,
   155  		AllowedProtocols: []plugin.Protocol{
   156  			plugin.ProtocolNetRPC,
   157  			plugin.ProtocolGRPC,
   158  		},
   159  		AutoMTLS:    rc.AutoMTLS,
   160  		SkipHostEnv: true,
   161  	}
   162  	if rc.image == "" {
   163  		clientConfig.Cmd = cmd
   164  		clientConfig.SecureConfig = &plugin.SecureConfig{
   165  			Checksum: rc.sha256,
   166  			Hash:     sha256.New(),
   167  		}
   168  	} else {
   169  		containerCfg, err := rc.containerConfig(ctx, cmd.Env)
   170  		if err != nil {
   171  			return nil, err
   172  		}
   173  		clientConfig.RunnerFunc = containerCfg.NewContainerRunner
   174  		clientConfig.UnixSocketConfig = &plugin.UnixSocketConfig{
   175  			Group:   strconv.Itoa(containerCfg.GroupAdd),
   176  			TempDir: rc.tmpdir,
   177  		}
   178  		clientConfig.GRPCBrokerMultiplex = true
   179  	}
   180  	return clientConfig, nil
   181  }
   182  
   183  func (rc runConfig) containerConfig(ctx context.Context, env []string) (*plugincontainer.Config, error) {
   184  	clusterID, err := rc.Wrapper.ClusterID(ctx)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	cfg := &plugincontainer.Config{
   189  		Image:  rc.image,
   190  		Tag:    rc.imageTag,
   191  		SHA256: fmt.Sprintf("%x", rc.sha256),
   192  
   193  		Env:        env,
   194  		GroupAdd:   os.Getegid(),
   195  		Runtime:    consts.DefaultContainerPluginOCIRuntime,
   196  		CapIPCLock: rc.mlockEnabled(),
   197  		Labels: map[string]string{
   198  			labelVaultPID:           strconv.Itoa(os.Getpid()),
   199  			labelVaultClusterID:     clusterID,
   200  			labelVaultPluginName:    rc.PluginClientConfig.Name,
   201  			labelVaultPluginType:    rc.PluginClientConfig.PluginType.String(),
   202  			labelVaultPluginVersion: rc.PluginClientConfig.Version,
   203  		},
   204  	}
   205  
   206  	// Use rc.command and rc.args directly instead of cmd.Path and cmd.Args, as
   207  	// exec.Command may mutate the provided command.
   208  	if rc.command != "" {
   209  		cfg.Entrypoint = []string{rc.command}
   210  	}
   211  	if len(rc.args) > 0 {
   212  		cfg.Args = rc.args
   213  	}
   214  	if rc.runtimeConfig != nil {
   215  		cfg.CgroupParent = rc.runtimeConfig.CgroupParent
   216  		cfg.NanoCpus = rc.runtimeConfig.CPU
   217  		cfg.Memory = rc.runtimeConfig.Memory
   218  		if rc.runtimeConfig.OCIRuntime != "" {
   219  			cfg.Runtime = rc.runtimeConfig.OCIRuntime
   220  		}
   221  		if rc.runtimeConfig.Rootless {
   222  			cfg.Rootless = true
   223  		}
   224  	}
   225  
   226  	return cfg, nil
   227  }
   228  
   229  func (rc runConfig) run(ctx context.Context) (*plugin.Client, error) {
   230  	clientConfig, err := rc.makeConfig(ctx)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  
   235  	client := plugin.NewClient(clientConfig)
   236  	return client, nil
   237  }
   238  
   239  type RunOpt func(*runConfig)
   240  
   241  func Env(env ...string) RunOpt {
   242  	return func(rc *runConfig) {
   243  		rc.env = append(rc.env, env...)
   244  	}
   245  }
   246  
   247  func Runner(wrapper RunnerUtil) RunOpt {
   248  	return func(rc *runConfig) {
   249  		rc.Wrapper = wrapper
   250  	}
   251  }
   252  
   253  func PluginSets(pluginSets map[int]plugin.PluginSet) RunOpt {
   254  	return func(rc *runConfig) {
   255  		rc.PluginSets = pluginSets
   256  	}
   257  }
   258  
   259  func HandshakeConfig(hs plugin.HandshakeConfig) RunOpt {
   260  	return func(rc *runConfig) {
   261  		rc.HandshakeConfig = hs
   262  	}
   263  }
   264  
   265  func Logger(logger log.Logger) RunOpt {
   266  	return func(rc *runConfig) {
   267  		rc.Logger = logger
   268  	}
   269  }
   270  
   271  func MetadataMode(isMetadataMode bool) RunOpt {
   272  	return func(rc *runConfig) {
   273  		rc.IsMetadataMode = isMetadataMode
   274  	}
   275  }
   276  
   277  func AutoMTLS(autoMTLS bool) RunOpt {
   278  	return func(rc *runConfig) {
   279  		rc.AutoMTLS = autoMTLS
   280  	}
   281  }
   282  
   283  func MLock(mlock bool) RunOpt {
   284  	return func(rc *runConfig) {
   285  		rc.MLock = mlock
   286  	}
   287  }
   288  
   289  func (r *PluginRunner) RunConfig(ctx context.Context, opts ...RunOpt) (*plugin.Client, error) {
   290  	var image, imageTag string
   291  	if r.OCIImage != "" {
   292  		image = r.OCIImage
   293  		imageTag = strings.TrimPrefix(r.Version, "v")
   294  	}
   295  	rc := runConfig{
   296  		command:       r.Command,
   297  		image:         image,
   298  		imageTag:      imageTag,
   299  		args:          r.Args,
   300  		sha256:        r.Sha256,
   301  		env:           r.Env,
   302  		runtimeConfig: r.RuntimeConfig,
   303  		tmpdir:        r.Tmpdir,
   304  		PluginClientConfig: PluginClientConfig{
   305  			Name:       r.Name,
   306  			PluginType: r.Type,
   307  			Version:    r.Version,
   308  		},
   309  	}
   310  
   311  	for _, opt := range opts {
   312  		opt(&rc)
   313  	}
   314  
   315  	return rc.run(ctx)
   316  }