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:
- `-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-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
@ -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-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.
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`
- `path`
- `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
- `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.
@ -116,7 +120,15 @@ Here is a full example showing the default config:
"execute_on": [],
"command": "",
"http_notification_url": ""
},
"keys": [
{
"private_key": "id_rsa"
},
{
"private_key": "id_ecdsa"
}
]
},
"data_provider": {
"driver": "sqlite",

View file

@ -36,9 +36,8 @@ func main() {
logCompress bool
logVerbose bool
)
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. "+
"The server private key will be autogenerated if the user that executes SFTPGo has write access to the config-dir")
flag.StringVar(&configDir, "config-dir", ".", "Location for SFTPGo config dir. It must contain sftpgo.conf "+
"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).")
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(&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"
)
const defaultPrivateKeyName = "id_rsa"
// Configuration for the SFTP server
type Configuration struct {
// Identification string used by the server
@ -43,6 +45,14 @@ type Configuration struct {
Umask string `json:"umask"`
// Actions to execute on SFTP create, download, delete and rename
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.
@ -76,17 +86,29 @@ func (c Configuration) Initialize(configDir string) error {
ServerVersion: "SSH-2.0-" + c.Banner,
}
if _, err := os.Stat(filepath.Join(configDir, "id_rsa")); os.IsNotExist(err) {
logger.Info(logSender, "creating new private key for server")
logger.InfoToConsole("id_rsa does not exist, creating new private key for server")
if err := c.generatePrivateKey(configDir); err != nil {
if len(c.Keys) == 0 {
autoFile := filepath.Join(configDir, defaultPrivateKeyName)
if _, err := os.Stat(autoFile); os.IsNotExist(err) {
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
}
} else if err != nil {
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 {
return err
}
@ -98,6 +120,7 @@ func (c Configuration) Initialize(configDir string) error {
// Add our private key to the server configuration.
serverConfig.AddHostKey(private)
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort))
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.
func (c Configuration) generatePrivateKey(configDir string) error {
func (c Configuration) generatePrivateKey(file string) error {
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
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 {
return err
}