sftpd: deprecate keys and add a new host_keys config param

host_key defines the private host keys as plain list of strings.

Remove the other deprecated config params from the default config too.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2020-05-16 23:26:44 +02:00
parent 469d36d979
commit a08dd85efd
7 changed files with 70 additions and 37 deletions

View File

@@ -57,7 +57,7 @@ func init() {
Command: "", Command: "",
HTTPNotificationURL: "", HTTPNotificationURL: "",
}, },
Keys: []sftpd.Key{}, HostKeys: []string{},
KexAlgorithms: []string{}, KexAlgorithms: []string{},
Ciphers: []string{}, Ciphers: []string{},
MACs: []string{}, MACs: []string{},
@@ -218,6 +218,7 @@ func LoadConfig(configDir, configName string) error {
logger.WarnToConsole("Configuration error: %v", err) logger.WarnToConsole("Configuration error: %v", err)
} }
checkHooksCompatibility() checkHooksCompatibility()
checkHostKeyCompatibility()
logger.Debug(logSender, "", "config file used: '%#v', config loaded: %+v", viper.ConfigFileUsed(), getRedactedGlobalConf()) logger.Debug(logSender, "", "config file used: '%#v', config loaded: %+v", viper.ConfigFileUsed(), getRedactedGlobalConf())
return err return err
} }
@@ -240,3 +241,14 @@ func checkHooksCompatibility() {
globalConf.SFTPD.KeyboardInteractiveHook = globalConf.SFTPD.KeyboardInteractiveProgram //nolint:staticcheck globalConf.SFTPD.KeyboardInteractiveHook = globalConf.SFTPD.KeyboardInteractiveProgram //nolint:staticcheck
} }
} }
func checkHostKeyCompatibility() {
// we copy deprecated fields to new ones to keep backward compatibility so lint is disabled
if len(globalConf.SFTPD.Keys) > 0 && len(globalConf.SFTPD.HostKeys) == 0 { //nolint:staticcheck
logger.Warn(logSender, "", "keys is deprecated, please use host_keys")
logger.WarnToConsole("keys is deprecated, please use host_keys")
for _, k := range globalConf.SFTPD.Keys { //nolint:staticcheck
globalConf.SFTPD.HostKeys = append(globalConf.SFTPD.HostKeys, k.PrivateKey)
}
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/drakkan/sftpgo/httpclient" "github.com/drakkan/sftpgo/httpclient"
"github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
) )
const ( const (
@@ -205,6 +206,37 @@ func TestHookCompatibity(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestHostKeyCompatibility(t *testing.T) {
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, configName)
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
sftpdConf.Keys = []sftpd.Key{ //nolint:staticcheck
{
PrivateKey: "rsa",
},
{
PrivateKey: "ecdsa",
},
}
c := make(map[string]sftpd.Configuration)
c["sftpd"] = sftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, 0666)
assert.NoError(t, err)
err = config.LoadConfig(configDir, tempConfigName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
assert.Equal(t, 2, len(sftpdConf.HostKeys))
assert.True(t, utils.IsStringInSlice("rsa", sftpdConf.HostKeys))
assert.True(t, utils.IsStringInSlice("ecdsa", sftpdConf.HostKeys))
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestSetGetConfig(t *testing.T) { func TestSetGetConfig(t *testing.T) {
sftpdConf := config.GetSFTPDConfig() sftpdConf := config.GetSFTPDConfig()
sftpdConf.IdleTimeout = 3 sftpdConf.IdleTimeout = 3

View File

@@ -50,8 +50,9 @@ The configuration file contains the following sections:
- `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions. - `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
- `command`, string. Absolute path to the command to execute. Leave empty to disable. - `command`, string. Absolute path to the command to execute. Leave empty to disable.
- `http_notification_url`, a valid URL. An HTTP GET request will be executed to this URL. Leave empty to disable. - `http_notification_url`, a valid URL. An HTTP GET request will be executed to this URL. Leave empty to disable.
- `keys`, struct array. It contains the daemon's private keys. If empty or missing, the daemon will search or try to generate `id_rsa` and `id_ecdsa` keys in the configuration directory. - `keys`, struct array. Deprecated, please use `host_keys`.
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one. - `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty or missing, the daemon will search or try to generate `id_rsa` and `id_ecdsa` keys inside the configuration directory.
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L46 "Supported kex algos") - `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L46 "Supported kex algos")
- `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers") - `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers")
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs") - `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs")

View File

@@ -1738,24 +1738,13 @@ func TestProxyProtocolVersion(t *testing.T) {
func TestLoadHostKeys(t *testing.T) { func TestLoadHostKeys(t *testing.T) {
c := Configuration{} c := Configuration{}
c.Keys = []Key{ c.HostKeys = []string{".", "missing file"}
{
PrivateKey: ".",
},
{
PrivateKey: "missing file",
},
}
err := c.checkAndLoadHostKeys("..", &ssh.ServerConfig{}) err := c.checkAndLoadHostKeys("..", &ssh.ServerConfig{})
assert.Error(t, err) assert.Error(t, err)
testfile := filepath.Join(os.TempDir(), "invalidkey") testfile := filepath.Join(os.TempDir(), "invalidkey")
err = ioutil.WriteFile(testfile, []byte("some bytes"), 0666) err = ioutil.WriteFile(testfile, []byte("some bytes"), 0666)
assert.NoError(t, err) assert.NoError(t, err)
c.Keys = []Key{ c.HostKeys = []string{testfile}
{
PrivateKey: testfile,
},
}
err = c.checkAndLoadHostKeys("..", &ssh.ServerConfig{}) err = c.checkAndLoadHostKeys("..", &ssh.ServerConfig{})
assert.Error(t, err) assert.Error(t, err)
err = os.Remove(testfile) err = os.Remove(testfile)

View File

@@ -63,8 +63,13 @@ type Configuration struct {
UploadMode int `json:"upload_mode" mapstructure:"upload_mode"` UploadMode int `json:"upload_mode" mapstructure:"upload_mode"`
// Actions to execute on SFTP create, download, delete and rename // Actions to execute on SFTP create, download, delete and rename
Actions Actions `json:"actions" mapstructure:"actions"` Actions Actions `json:"actions" mapstructure:"actions"`
// Keys are a list of host keys // Deprecated: please use HostKeys
Keys []Key `json:"keys" mapstructure:"keys"` Keys []Key `json:"keys" mapstructure:"keys"`
// HostKeys define the daemon's private host keys.
// Each host key can be defined as a path relative to the configuration directory or an absolute one.
// If empty or missing, the daemon will search or try to generate "id_rsa" and "id_ecdsa" host keys
// inside the configuration directory.
HostKeys []string `json:"host_keys" mapstructure:"host_keys"`
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in // KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
// preference order. // preference order.
KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"` KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"`
@@ -131,6 +136,7 @@ type Configuration struct {
} }
// Key contains information about host keys // Key contains information about host keys
// Deprecated: please use HostKeys
type Key struct { type Key struct {
// The private key path as absolute path or relative to the configuration directory // The private key path as absolute path or relative to the configuration directory
PrivateKey string `json:"private_key" mapstructure:"private_key"` PrivateKey string `json:"private_key" mapstructure:"private_key"`
@@ -509,7 +515,7 @@ func (c *Configuration) checkSSHCommands() {
// If no host keys are defined we try to use or generate the default ones. // If no host keys are defined we try to use or generate the default ones.
func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error { func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error {
if len(c.Keys) == 0 { if len(c.HostKeys) == 0 {
defaultKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName} defaultKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName}
for _, k := range defaultKeys { for _, k := range defaultKeys {
autoFile := filepath.Join(configDir, k) autoFile := filepath.Join(configDir, k)
@@ -525,22 +531,22 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
return err return err
} }
} }
c.Keys = append(c.Keys, Key{PrivateKey: k}) c.HostKeys = append(c.HostKeys, k)
} }
} }
for _, k := range c.Keys { for _, k := range c.HostKeys {
privateFile := k.PrivateKey hostKey := k
if !utils.IsFileInputValid(privateFile) { if !utils.IsFileInputValid(hostKey) {
logger.Warn(logSender, "", "unable to load invalid host key: %#v", privateFile) logger.Warn(logSender, "", "unable to load invalid host key: %#v", hostKey)
logger.WarnToConsole("unable to load invalid host key: %#v", privateFile) logger.WarnToConsole("unable to load invalid host key: %#v", hostKey)
continue continue
} }
if !filepath.IsAbs(privateFile) { if !filepath.IsAbs(hostKey) {
privateFile = filepath.Join(configDir, privateFile) hostKey = filepath.Join(configDir, hostKey)
} }
logger.Info(logSender, "", "Loading private key: %s", privateFile) logger.Info(logSender, "", "Loading private host key: %s", hostKey)
privateBytes, err := ioutil.ReadFile(privateFile) privateBytes, err := ioutil.ReadFile(hostKey)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -293,11 +293,7 @@ func TestInitialization(t *testing.T) {
sftpdConf.ProxyAllowed = []string{"1270.0.0.1"} sftpdConf.ProxyAllowed = []string{"1270.0.0.1"}
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
assert.Error(t, err) assert.Error(t, err)
sftpdConf.Keys = []sftpd.Key{ sftpdConf.HostKeys = []string{"missing file"}
{
PrivateKey: "missing file",
},
}
err = sftpdConf.Initialize(configDir) err = sftpdConf.Initialize(configDir)
assert.Error(t, err) assert.Error(t, err)
sftpdConf.Keys = nil sftpdConf.Keys = nil

View File

@@ -12,7 +12,7 @@
"command": "", "command": "",
"http_notification_url": "" "http_notification_url": ""
}, },
"keys": [], "host_keys": [],
"kex_algorithms": [], "kex_algorithms": [],
"ciphers": [], "ciphers": [],
"macs": [], "macs": [],
@@ -26,7 +26,6 @@
"pwd", "pwd",
"scp" "scp"
], ],
"keyboard_interactive_auth_program": "",
"keyboard_interactive_auth_hook": "", "keyboard_interactive_auth_hook": "",
"proxy_protocol": 0, "proxy_protocol": 0,
"proxy_allowed": [] "proxy_allowed": []
@@ -50,12 +49,10 @@
"command": "", "command": "",
"http_notification_url": "" "http_notification_url": ""
}, },
"external_auth_program": "",
"external_auth_hook": "", "external_auth_hook": "",
"external_auth_scope": 0, "external_auth_scope": 0,
"credentials_path": "credentials", "credentials_path": "credentials",
"pre_login_hook": "", "pre_login_hook": ""
"pre_login_program": ""
}, },
"httpd": { "httpd": {
"bind_port": 8080, "bind_port": 8080,