Support multiple private (host) keys

With this patch, one can configure one or more private (host) keys in
the configuration file.

I made it a nested struct, so we can add more options later. Eg. host
certificates might be a useful addition if I can figure out how this is
done in golang's crypto/ssh...

Signed-off-by: Jo Vandeginste <Jo.Vandeginste@kuleuven.be>
This commit is contained in:
Jo Vandeginste
2019-08-01 09:42:15 +02:00
committed by drakkan
parent 1566e43cd7
commit bd7b6a785e
4 changed files with 125 additions and 91 deletions

View File

@@ -49,7 +49,7 @@ Alternately you can use distro packages:
The `sftpgo` executable supports the following command line flags: The `sftpgo` executable supports the following command line flags:
- `-config-dir` string. Location of the config dir. This directory should contain the `sftpgo.conf` configuration file, the private key for the SFTP server (`id_rsa` file) and the SQLite database if you use SQLite as data provider. The server private key will be autogenerated if the user that executes SFTPGo has write access to the config-dir. The default value is "." - `-config-dir` string. Location of the config dir. This directory should contain the `sftpgo.conf` configuration file and is used as the base for files with a relative path (eg. the private keys for the SFTP server, the SQLite database if you use SQLite as data provider). The default value is "."
- `-log-file-path` string. Location for the log file, default "sftpgo.log" - `-log-file-path` string. Location for the log file, default "sftpgo.log"
- `-log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 - `-log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10
- `-log-max-backups` int. Maximum number of old log files to retain. Default 5 - `-log-max-backups` int. Maximum number of old log files to retain. Default 5
@@ -57,6 +57,8 @@ The `sftpgo` executable supports the following command line flags:
- `-log-compress` boolean. Determine if the rotated log files should be compressed using gzip - `-log-compress` boolean. Determine if the rotated log files should be compressed using gzip
- `-log-verbose` boolean. Enable verbose logs. Default `true` - `-log-verbose` boolean. Enable verbose logs. Default `true`
If you don't configure any private host keys, the daemon will use `id_rsa` in the configuration directory. If that file doesn't exist, the daemon will attempt to autogenerate it (if the user that executes SFTPGo has write access to the config-dir). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L32).
Before starting `sftpgo` a dataprovider must be configured. Before starting `sftpgo` a dataprovider must be configured.
Sample SQL scripts to create the required database structure can be found insite the source tree [sql](https://github.com/drakkan/sftpgo/tree/master/sql "sql") directory. The SQL scripts filename's is, by convention, the date as `YYYYMMDD` and the suffix `.sql`. You need to apply all the SQL scripts for your database ordered by name, for example `20190706.sql` must be applied before `20190728.sql` and so on. Sample SQL scripts to create the required database structure can be found insite the source tree [sql](https://github.com/drakkan/sftpgo/tree/master/sql "sql") directory. The SQL scripts filename's is, by convention, the date as `YYYYMMDD` and the suffix `.sql`. You need to apply all the SQL scripts for your database ordered by name, for example `20190706.sql` must be applied before `20190728.sql` and so on.
@@ -82,6 +84,8 @@ The `sftpgo.conf` configuration file contains the following sections:
- `username` - `username`
- `path` - `path`
- `target_path`, added for `rename` action only - `target_path`, added for `rename` action only
- `keys`, struct array. It contains the daemon's private keys
- `private_key`, path to the private key file
- **"data_provider"**, the configuration for the data provider - **"data_provider"**, the configuration for the data provider
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql` - `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database. - `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
@@ -116,7 +120,15 @@ Here is a full example showing the default config:
"execute_on": [], "execute_on": [],
"command": "", "command": "",
"http_notification_url": "" "http_notification_url": ""
},
"keys": [
{
"private_key": "id_rsa"
},
{
"private_key": "id_ecdsa"
} }
]
}, },
"data_provider": { "data_provider": {
"driver": "sqlite", "driver": "sqlite",

View File

@@ -36,9 +36,8 @@ func main() {
logCompress bool logCompress bool
logVerbose bool logVerbose bool
) )
flag.StringVar(&configDir, "config-dir", ".", "Location for SFTPGo config dir. It must contain sftpgo.conf, "+ flag.StringVar(&configDir, "config-dir", ".", "Location for SFTPGo config dir. It must contain sftpgo.conf "+
"the private key for the SFTP server (id_rsa file) and the SQLite database if you use SQLite as data provider. "+ "and is used as the base for files with a relative path (eg. the private keys for the SFTP server, the SQLite database if you use SQLite as data provider).")
"The server private key will be autogenerated if the user that executes SFTPGo has write access to the config-dir")
flag.StringVar(&logFilePath, "log-file-path", "sftpgo.log", "Location for the log file") flag.StringVar(&logFilePath, "log-file-path", "sftpgo.log", "Location for the log file")
flag.IntVar(&logMaxSize, "log-max-size", 10, "Maximum size in megabytes of the log file before it gets rotated.") flag.IntVar(&logMaxSize, "log-max-size", 10, "Maximum size in megabytes of the log file before it gets rotated.")
flag.IntVar(&logMaxBackups, "log-max-backups", 5, "Maximum number of old log files to retain") flag.IntVar(&logMaxBackups, "log-max-backups", 5, "Maximum number of old log files to retain")

View File

@@ -25,6 +25,8 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
const defaultPrivateKeyName = "id_rsa"
// Configuration for the SFTP server // Configuration for the SFTP server
type Configuration struct { type Configuration struct {
// Identification string used by the server // Identification string used by the server
@@ -43,6 +45,14 @@ type Configuration struct {
Umask string `json:"umask"` Umask string `json:"umask"`
// Actions to execute on SFTP create, download, delete and rename // Actions to execute on SFTP create, download, delete and rename
Actions Actions `json:"actions"` Actions Actions `json:"actions"`
// Keys are a list of host keys
Keys []Key `json:"keys"`
}
// Struct containing information about host keys
type Key struct {
// The private key
PrivateKey string `json:"private_key"`
} }
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections. // Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
@@ -76,17 +86,29 @@ func (c Configuration) Initialize(configDir string) error {
ServerVersion: "SSH-2.0-" + c.Banner, ServerVersion: "SSH-2.0-" + c.Banner,
} }
if _, err := os.Stat(filepath.Join(configDir, "id_rsa")); os.IsNotExist(err) { if len(c.Keys) == 0 {
logger.Info(logSender, "creating new private key for server") autoFile := filepath.Join(configDir, defaultPrivateKeyName)
logger.InfoToConsole("id_rsa does not exist, creating new private key for server") if _, err := os.Stat(autoFile); os.IsNotExist(err) {
if err := c.generatePrivateKey(configDir); err != nil { logger.Info(logSender, "No host keys configured and %s does not exist; creating new private key for server", autoFile)
logger.InfoToConsole("No host keys configured and %s does not exist; creating new private key for server", autoFile)
if err := c.generatePrivateKey(autoFile); err != nil {
return err return err
} }
} else if err != nil { } else if err != nil {
return err return err
} }
privateBytes, err := ioutil.ReadFile(filepath.Join(configDir, "id_rsa")) c.Keys = append(c.Keys, Key{PrivateKey: defaultPrivateKeyName})
}
for _, k := range c.Keys {
privateFile := k.PrivateKey
if !filepath.IsAbs(privateFile) {
privateFile = filepath.Join(configDir, privateFile)
}
logger.Info(logSender, "Loading private key: %s", privateFile)
privateBytes, err := ioutil.ReadFile(privateFile)
if err != nil { if err != nil {
return err return err
} }
@@ -98,6 +120,7 @@ func (c Configuration) Initialize(configDir string) error {
// Add our private key to the server configuration. // Add our private key to the server configuration.
serverConfig.AddHostKey(private) serverConfig.AddHostKey(private)
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort)) listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort))
if err != nil { if err != nil {
@@ -273,13 +296,13 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
} }
// Generates a private key that will be used by the SFTP server. // Generates a private key that will be used by the SFTP server.
func (c Configuration) generatePrivateKey(configDir string) error { func (c Configuration) generatePrivateKey(file string) error {
key, err := rsa.GenerateKey(rand.Reader, 4096) key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil { if err != nil {
return err return err
} }
o, err := os.OpenFile(filepath.Join(configDir, "id_rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return err return err
} }