sftpd: autogenerate ecdsa key

With default configuration we now generate RSA and ECDSA server keys.
This commit is contained in:
Nicola Murino 2020-02-16 18:17:39 +01:00
parent 79c8b6cbc2
commit c8cc81cf4a
3 changed files with 106 additions and 44 deletions

View file

@ -125,7 +125,7 @@ The `serve` command supports the following flags:
- `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable. It is unused if `log-file-path` is empty.
- `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`).
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).
If you don't configure any private host keys, the daemon will use `id_rsa` and `id_ecdsa` in the configuration directory. If these files don't exist, the daemon will attempt to autogenerate them (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).
The `sftpgo` configuration file contains the following sections:
@ -141,7 +141,7 @@ The `sftpgo` 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.
- `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.
- `keys`, struct array. It contains the daemon's private keys. If empty or missing the daemon will search or try to generate `id_rsa` in the configuration directory.
- `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.
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
- `enable_scp`, boolean. Default disabled. Set to `true` to enable the experimental SCP support. This setting is deprecated and will be removed in future versions, please add `scp` to the `enabled_ssh_commands` list to enable it
- `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")
@ -253,7 +253,7 @@ Here is a full example showing the default config in JSON format:
}
```
If you want to use a private key that use an algorithm different from RSA or more than one private key then replace the empty `keys` array with something like this:
If you want to use a private key that use an algorithm different from RSA or ECDSA or more private keys then generate your own keys and replace the empty `keys` array with something like this:
```json
"keys": [
@ -262,10 +262,15 @@ If you want to use a private key that use an algorithm different from RSA or mor
},
{
"private_key": "id_ecdsa"
},
{
"private_key": "id_ed25519"
}
]
```
where `id_rsa`, `id_ecdsa` and `id_ed25519` are your generated keys. You can use absolute paths or paths relative to the configuration directory.
The configuration can be read from JSON, TOML, YAML, HCL, envfile and Java properties config files, if your `config-file` flag is set to `sftpgo` (default value) you need to create a configuration file called `sftpgo.json` or `sftpgo.yaml` and so on inside `config-dir`.
You can also override all the available configuration options using environment variables, sftpgo will check for environment variables with a name matching the key uppercased and prefixed with the `SFTPGO_`. You need to use `__` to traverse a struct.
@ -410,7 +415,7 @@ The program must write the questions on its standard output, in a single line, u
- `instruction`, string. A short description to show to the user that is trying to authenticate. Can be empty or omitted
- `questions`, list of questions to be asked to the user
- `echos` list of boolean flags corresponding to the questions (so the lengths of both lists must be the same) and indicating whether user's reply for a particular question should be echoed on the screen while they are typing: true if it should be echoed, or false if it should be hidden.
- `check_password` optional integer. Ask exactly one question and set this field to 1 if the expected answer is the user password and you want that SFTPGo check it for you. If the password is correct the returned response to the program is `OK`. If the password is wrong the program will be terminated and an authentication error will be returned to the user
- `check_password` optional integer. Ask exactly one question and set this field to 1 if the expected answer is the user password and you want that SFTPGo checks it for you. If the password is correct the returned response to the program is `OK`. If the password is wrong the program will be terminated and an authentication error will be returned to the user that is trying to authenticate
- `auth_result`, integer. Set this field to 1 to indicate successful authentication, 0 is ignored, any other value means authentication error. If this fields is found and it is different from 0 then SFTPGo does not read any other questions from the external program and finalize the authentication.
SFTPGo writes the user answers to the program standard input, one per line, in the same order of the questions.
@ -440,7 +445,7 @@ else
fi
```
and here is an example where SFTPGo check the user password for you:
and here is an example where SFTPGo checks the user password for you:
```
#!/bin/sh

View file

@ -1,12 +1,8 @@
package sftpd
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
@ -24,7 +20,10 @@ import (
"golang.org/x/crypto/ssh"
)
const defaultPrivateKeyName = "id_rsa"
const (
defaultPrivateRSAKeyName = "id_rsa"
defaultPrivateECDSAKeyName = "id_ecdsa"
)
var sftpExtensions = []string{"posix-rename@openssh.com"}
@ -445,18 +444,26 @@ func (c *Configuration) checkSSHCommands() {
// If no host keys are defined we try to use or generate the default one.
func (c *Configuration) checkHostKeys(configDir string) error {
var err error
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 %#v does not exist; creating new private key for server", autoFile)
logger.InfoToConsole("No host keys configured and %#v does not exist; creating new private key for server", autoFile)
err = c.generatePrivateKey(autoFile)
defaultKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName}
for _, k := range defaultKeys {
autoFile := filepath.Join(configDir, k)
if _, err := os.Stat(autoFile); os.IsNotExist(err) {
logger.Info(logSender, "", "No host keys configured and %#v does not exist; creating new key for server", autoFile)
logger.InfoToConsole("No host keys configured and %#v does not exist; creating new key for server", autoFile)
if k == defaultPrivateRSAKeyName {
err = utils.GenerateRSAKeys(autoFile)
} else {
err = utils.GenerateECDSAKeys(autoFile)
}
if err != nil {
return err
}
}
c.Keys = append(c.Keys, Key{PrivateKey: k})
}
c.Keys = append(c.Keys, Key{PrivateKey: defaultPrivateKeyName})
}
return err
return nil
}
func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey string) (*ssh.Permissions, error) {
@ -507,28 +514,3 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
metrics.AddLoginResult(method, err)
return sshPerm, err
}
// Generates a private key that will be used by the SFTP server.
func (c Configuration) generatePrivateKey(file string) error {
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer o.Close()
pkey := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
if err := pem.Encode(o, pkey); err != nil {
return err
}
return nil
}

View file

@ -4,14 +4,23 @@ package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
const logSender = "utils"
@ -173,3 +182,69 @@ func DecryptData(data string) (string, error) {
}
return string(plaintext), nil
}
// GenerateRSAKeys generate rsa private and public keys and write the
// private key to specified file and the public key to the specified
// file adding the .pub suffix
func GenerateRSAKeys(file string) error {
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer o.Close()
priv := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
if err := pem.Encode(o, priv); err != nil {
return err
}
pub, err := ssh.NewPublicKey(&key.PublicKey)
if err != nil {
return err
}
return ioutil.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600)
}
// GenerateECDSAKeys generate ecdsa private and public keys and write the
// private key to specified file and the public key to the specified
// file adding the .pub suffix
func GenerateECDSAKeys(file string) error {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer o.Close()
keyBytes, err := x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
priv := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
}
if err := pem.Encode(o, priv); err != nil {
return err
}
pub, err := ssh.NewPublicKey(&key.PublicKey)
if err != nil {
return err
}
return ioutil.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600)
}