allow to restrict the env vars passed to plugins

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-10-29 15:19:30 +01:00
parent 9a7a3b00dc
commit 5c938e46b7
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
10 changed files with 90 additions and 13 deletions

View file

@ -477,7 +477,7 @@ The configuration file contains the following sections:
<details><summary><font size=4>Plugins</font></summary> <details><summary><font size=4>Plugins</font></summary>
- **plugins**, list of external plugins. :warning: Please note that the plugin system is experimental, the configuration parameters and interfaces may change in a backward incompatible way in future. Each plugin is configured using a struct with the following fields: - **plugins**, list of external plugins. :warning: Please note that the plugin system is experimental, the configuration parameters and interfaces may change in a backward incompatible way in future. Each plugin is configured using a struct with the following fields:
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`, `metadata`. - `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`, `metadata`, `eventsearcher`, `ipfilter`.
- `notifier_options`, struct. Defines the options for notifier plugins. - `notifier_options`, struct. Defines the options for notifier plugins.
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin. - `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
- `provider_events`, list of strings. Defines the provider events that will be notified to this plugin. - `provider_events`, list of strings. Defines the provider events that will be notified to this plugin.
@ -494,6 +494,8 @@ The configuration file contains the following sections:
- `args`, list of strings. Optional arguments to pass to the plugin executable. - `args`, list of strings. Optional arguments to pass to the plugin executable.
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable. - `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
- `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client. - `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mutual TLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
- `env_prefix`, string. Defines the prefix for env vars to pass from the SFTPGo process environment to the plugin. Set to `none` to not pass any environment variable, set to `*` to pass all environment variables. If empty, the prefix is returned as the plugin name in uppercase with `-` replaced with `_` and a trailing `_`. For example if the plugin name is `sftpgo-plugin-eventsearch` the prefix will be `SFTPGO_PLUGIN_EVENTSEARCH_`
- `env_vars`, list of strings. Additional environment variable names to pass from the SFTPGo process environment to the plugin.
</details> </details>

View file

@ -1046,6 +1046,18 @@ func getPluginsFromEnv(idx int) {
isSet = true isSet = true
} }
envPrefix, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__ENV_PREFIX", idx))
if ok {
pluginConfig.EnvPrefix = envPrefix
isSet = true
}
envVars, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__ENV_VARS", idx))
if ok {
pluginConfig.EnvVars = envVars
isSet = true
}
if isSet { if isSet {
if len(globalConf.PluginsConfig) > idx { if len(globalConf.PluginsConfig) > idx {
globalConf.PluginsConfig[idx] = pluginConfig globalConf.PluginsConfig[idx] = pluginConfig

View file

@ -693,6 +693,9 @@ func TestPluginsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeAWS) os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeAWS)
os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusAWS) os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusAWS)
os.Setenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE", "14") os.Setenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE", "14")
os.Setenv("SFTPGO_PLUGINS__0__ENV_PREFIX", "prefix_")
os.Setenv("SFTPGO_PLUGINS__0__ENV_VARS", "a, b")
t.Cleanup(func() { t.Cleanup(func() {
os.Unsetenv("SFTPGO_PLUGINS__0__TYPE") os.Unsetenv("SFTPGO_PLUGINS__0__TYPE")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS") os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS")
@ -708,6 +711,8 @@ func TestPluginsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME") os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME")
os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS") os.Unsetenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS")
os.Unsetenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE") os.Unsetenv("SFTPGO_PLUGINS__0__AUTH_OPTIONS__SCOPE")
os.Unsetenv("SFTPGO_PLUGINS__0__ENV_PREFIX")
os.Unsetenv("SFTPGO_PLUGINS__0__ENV_VARS")
}) })
err := config.LoadConfig(configDir, "") err := config.LoadConfig(configDir, "")
@ -739,6 +744,10 @@ func TestPluginsFromEnv(t *testing.T) {
require.Equal(t, kms.SchemeAWS, pluginConf.KMSOptions.Scheme) require.Equal(t, kms.SchemeAWS, pluginConf.KMSOptions.Scheme)
require.Equal(t, kms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus) require.Equal(t, kms.SecretStatusAWS, pluginConf.KMSOptions.EncryptedStatus)
require.Equal(t, 14, pluginConf.AuthOptions.Scope) require.Equal(t, 14, pluginConf.AuthOptions.Scope)
require.Equal(t, "prefix_", pluginConf.EnvPrefix)
require.Len(t, pluginConf.EnvVars, 2)
assert.Equal(t, "a", pluginConf.EnvVars[0])
assert.Equal(t, "b", pluginConf.EnvVars[1])
cfg := make(map[string]any) cfg := make(map[string]any)
cfg["plugins"] = pluginConf cfg["plugins"] = pluginConf
@ -754,6 +763,8 @@ func TestPluginsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "0") os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "0")
os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeVaultTransit) os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__SCHEME", kms.SchemeVaultTransit)
os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusVaultTransit) os.Setenv("SFTPGO_PLUGINS__0__KMS_OPTIONS__ENCRYPTED_STATUS", kms.SecretStatusVaultTransit)
os.Setenv("SFTPGO_PLUGINS__0__ENV_PREFIX", "")
os.Setenv("SFTPGO_PLUGINS__0__ENV_VARS", "")
err = config.LoadConfig(configDir, confName) err = config.LoadConfig(configDir, confName)
assert.NoError(t, err) assert.NoError(t, err)
pluginsConf = config.GetPluginsConfig() pluginsConf = config.GetPluginsConfig()
@ -778,6 +789,8 @@ func TestPluginsFromEnv(t *testing.T) {
require.Equal(t, kms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme) require.Equal(t, kms.SchemeVaultTransit, pluginConf.KMSOptions.Scheme)
require.Equal(t, kms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus) require.Equal(t, kms.SecretStatusVaultTransit, pluginConf.KMSOptions.EncryptedStatus)
require.Equal(t, 14, pluginConf.AuthOptions.Scope) require.Equal(t, 14, pluginConf.AuthOptions.Scope)
assert.Empty(t, pluginConf.EnvPrefix)
assert.Len(t, pluginConf.EnvVars, 0)
err = os.Remove(configFilePath) err = os.Remove(configFilePath)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -17,7 +17,6 @@ package plugin
import ( import (
"errors" "errors"
"fmt" "fmt"
"os/exec"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
@ -119,7 +118,8 @@ func (p *authPlugin) initialize() error {
client := plugin.NewClient(&plugin.ClientConfig{ client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: auth.Handshake, HandshakeConfig: auth.Handshake,
Plugins: auth.PluginMap, Plugins: auth.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...), Cmd: p.config.getCommand(),
SkipHostEnv: true,
AllowedProtocols: []plugin.Protocol{ AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC, plugin.ProtocolGRPC,
}, },

View file

@ -16,7 +16,6 @@ package plugin
import ( import (
"fmt" "fmt"
"os/exec"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
@ -60,7 +59,8 @@ func (p *ipFilterPlugin) initialize() error {
client := plugin.NewClient(&plugin.ClientConfig{ client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: ipfilter.Handshake, HandshakeConfig: ipfilter.Handshake,
Plugins: ipfilter.PluginMap, Plugins: ipfilter.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...), Cmd: p.config.getCommand(),
SkipHostEnv: true,
AllowedProtocols: []plugin.Protocol{ AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC, plugin.ProtocolGRPC,
}, },

View file

@ -16,7 +16,6 @@ package plugin
import ( import (
"fmt" "fmt"
"os/exec"
"path/filepath" "path/filepath"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
@ -81,7 +80,8 @@ func (p *kmsPlugin) initialize() error {
client := plugin.NewClient(&plugin.ClientConfig{ client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: kmsplugin.Handshake, HandshakeConfig: kmsplugin.Handshake,
Plugins: kmsplugin.PluginMap, Plugins: kmsplugin.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...), Cmd: p.config.getCommand(),
SkipHostEnv: true,
AllowedProtocols: []plugin.Protocol{ AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC, plugin.ProtocolGRPC,
}, },

View file

@ -16,7 +16,6 @@ package plugin
import ( import (
"fmt" "fmt"
"os/exec"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
@ -60,7 +59,8 @@ func (p *metadataPlugin) initialize() error {
client := plugin.NewClient(&plugin.ClientConfig{ client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: metadata.Handshake, HandshakeConfig: metadata.Handshake,
Plugins: metadata.PluginMap, Plugins: metadata.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...), Cmd: p.config.getCommand(),
SkipHostEnv: true,
AllowedProtocols: []plugin.Protocol{ AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC, plugin.ProtocolGRPC,
}, },

View file

@ -16,7 +16,6 @@ package plugin
import ( import (
"fmt" "fmt"
"os/exec"
"sync" "sync"
"time" "time"
@ -168,7 +167,8 @@ func (p *notifierPlugin) initialize() error {
client := plugin.NewClient(&plugin.ClientConfig{ client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: notifier.Handshake, HandshakeConfig: notifier.Handshake,
Plugins: notifier.PluginMap, Plugins: notifier.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...), Cmd: p.config.getCommand(),
SkipHostEnv: true,
AllowedProtocols: []plugin.Protocol{ AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC, plugin.ProtocolGRPC,
}, },

View file

@ -21,6 +21,10 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -81,6 +85,16 @@ type Config struct {
// rejected. The client will also refuse to connect to any server that isn't // rejected. The client will also refuse to connect to any server that isn't
// the original instance started by the client. // the original instance started by the client.
AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"` AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"`
// EnvPrefix defines the prefix for env vars to pass from the SFTPGo process
// environment to the plugin. Set to "none" to not pass any environment
// variable, set to "*" to pass all environment variables. If empty, the
// prefix is returned as the plugin name in uppercase with "-" replaced with
// "_" and a trailing "_". For example if the plugin name is
// sftpgo-plugin-eventsearch the prefix will be SFTPGO_PLUGIN_EVENTSEARCH_
EnvPrefix string `json:"env_prefix" mapstructure:"env_prefix"`
// Additional environment variable names to pass from the SFTPGo process
// environment to the plugin.
EnvVars []string `json:"env_vars" mapstructure:"env_vars"`
// unique identifier for kms plugins // unique identifier for kms plugins
kmsID int kmsID int
} }
@ -99,6 +113,42 @@ func (c *Config) getSecureConfig() (*plugin.SecureConfig, error) {
return nil, nil return nil, nil
} }
func (c *Config) getEnvVarPrefix() string {
if c.EnvPrefix == "none" {
return ""
}
if c.EnvPrefix != "" {
return c.EnvPrefix
}
prefix := strings.ToUpper(filepath.Base(c.Cmd)) + "_"
return strings.ReplaceAll(prefix, "-", "_")
}
func (c *Config) getCommand() *exec.Cmd {
cmd := exec.Command(c.Cmd, c.Args...)
cmd.Env = []string{}
if envVarPrefix := c.getEnvVarPrefix(); envVarPrefix != "" {
if envVarPrefix == "*" {
logger.Debug(logSender, "", "sharing all the environment variables with plugin %q", c.Cmd)
cmd.Env = append(cmd.Env, os.Environ()...)
return cmd
}
logger.Debug(logSender, "", "adding env vars with prefix %q for plugin %q", envVarPrefix, c.Cmd)
for _, val := range os.Environ() {
if strings.HasPrefix(val, envVarPrefix) {
cmd.Env = append(cmd.Env, val)
}
}
}
logger.Debug(logSender, "", "additional env vars for plugin %q: %+v", c.Cmd, c.EnvVars)
for _, key := range c.EnvVars {
cmd.Env = append(cmd.Env, os.Getenv(key))
}
return cmd
}
func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {
return &kmsPluginSecretProvider{ return &kmsPluginSecretProvider{
BaseSecret: base, BaseSecret: base,

View file

@ -16,7 +16,6 @@ package plugin
import ( import (
"fmt" "fmt"
"os/exec"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
@ -60,7 +59,8 @@ func (p *searcherPlugin) initialize() error {
client := plugin.NewClient(&plugin.ClientConfig{ client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: eventsearcher.Handshake, HandshakeConfig: eventsearcher.Handshake,
Plugins: eventsearcher.PluginMap, Plugins: eventsearcher.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...), Cmd: p.config.getCommand(),
SkipHostEnv: true,
AllowedProtocols: []plugin.Protocol{ AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC, plugin.ProtocolGRPC,
}, },