mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 17:10:28 +00:00
sshd: add support for host key certificates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
a7b159aebb
commit
55f8171dd1
5 changed files with 91 additions and 2 deletions
|
@ -195,6 +195,7 @@ func Init() {
|
|||
MaxAuthTries: 0,
|
||||
Banner: defaultSFTPDBanner,
|
||||
HostKeys: []string{},
|
||||
HostCertificates: []string{},
|
||||
KexAlgorithms: []string{},
|
||||
Ciphers: []string{},
|
||||
MACs: []string{},
|
||||
|
@ -1539,6 +1540,7 @@ func setViperDefaults() {
|
|||
viper.SetDefault("sftpd.max_auth_tries", globalConf.SFTPD.MaxAuthTries)
|
||||
viper.SetDefault("sftpd.banner", globalConf.SFTPD.Banner)
|
||||
viper.SetDefault("sftpd.host_keys", globalConf.SFTPD.HostKeys)
|
||||
viper.SetDefault("sftpd.host_certificates", globalConf.SFTPD.HostCertificates)
|
||||
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
|
||||
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
|
||||
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
|
||||
|
|
|
@ -108,6 +108,7 @@ The configuration file contains the following sections:
|
|||
- `max_auth_tries` integer. Maximum number of authentication attempts permitted per connection. If set to a negative number, the number of attempts is unlimited. If set to zero, the number of attempts is limited to 6.
|
||||
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
|
||||
- `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, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
|
||||
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
|
||||
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. Sha512 based KEXs are disabled by default because they are slow.
|
||||
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
|
||||
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
|
||||
|
|
|
@ -102,6 +102,10 @@ type Configuration struct {
|
|||
// 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"`
|
||||
// HostCertificates defines public host certificates.
|
||||
// Each certificate can be defined as a path relative to the configuration directory or an absolute one.
|
||||
// Certificate's public key must match a private host key otherwise it will be silently ignored.
|
||||
HostCertificates []string `json:"host_certificates" mapstructure:"host_certificates"`
|
||||
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
|
||||
// preference order.
|
||||
KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"`
|
||||
|
@ -790,6 +794,10 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
|||
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
|
||||
return err
|
||||
}
|
||||
hostCertificates, err := c.loadHostCertificates(configDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceStatus.HostKeys = nil
|
||||
for _, hostKey := range c.HostKeys {
|
||||
if !util.IsFileInputValid(hostKey) {
|
||||
|
@ -821,6 +829,14 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
|||
|
||||
// Add private key to the server configuration.
|
||||
serverConfig.AddHostKey(private)
|
||||
for _, cert := range hostCertificates {
|
||||
signer, err := ssh.NewCertSigner(cert, private)
|
||||
if err == nil {
|
||||
serverConfig.AddHostKey(signer)
|
||||
logger.Info(logSender, "", "Host certificate loaded for host key %#v, fingerprint %#v",
|
||||
hostKey, ssh.FingerprintSHA256(signer.PublicKey()))
|
||||
}
|
||||
}
|
||||
}
|
||||
var fp []string
|
||||
for idx := range serviceStatus.HostKeys {
|
||||
|
@ -831,11 +847,42 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certificate, error) {
|
||||
var certs []*ssh.Certificate
|
||||
for _, certPath := range c.HostCertificates {
|
||||
if !util.IsFileInputValid(certPath) {
|
||||
logger.Warn(logSender, "", "unable to load invalid host certificate %#v", certPath)
|
||||
logger.WarnToConsole("unable to load invalid host certificate %#v", certPath)
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(certPath) {
|
||||
certPath = filepath.Join(configDir, certPath)
|
||||
}
|
||||
certBytes, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return certs, fmt.Errorf("unable to load host certificate %#v: %w", certPath, err)
|
||||
}
|
||||
parsed, _, _, _, err := ssh.ParseAuthorizedKey(certBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse host certificate %#v: %w", certPath, err)
|
||||
}
|
||||
cert, ok := parsed.(*ssh.Certificate)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the file %#v is not an SSH certificate", certPath)
|
||||
}
|
||||
if cert.CertType != ssh.HostCert {
|
||||
return nil, fmt.Errorf("the file %#v is not an host certificate", certPath)
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
func (c *Configuration) initializeCertChecker(configDir string) error {
|
||||
for _, keyPath := range c.TrustedUserCAKeys {
|
||||
if !util.IsFileInputValid(keyPath) {
|
||||
logger.Warn(logSender, "", "unable to load invalid trusted user CA key: %#v", keyPath)
|
||||
logger.WarnToConsole("unable to load invalid trusted user CA key: %#v", keyPath)
|
||||
logger.Warn(logSender, "", "unable to load invalid trusted user CA key %#v", keyPath)
|
||||
logger.WarnToConsole("unable to load invalid trusted user CA key %#v", keyPath)
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(keyPath) {
|
||||
|
|
|
@ -396,6 +396,44 @@ func TestInitialization(t *testing.T) {
|
|||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "unsupported key-exchange algorithm")
|
||||
}
|
||||
sftpdConf.HostCertificates = []string{"missing file"}
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "unable to load host certificate")
|
||||
}
|
||||
sftpdConf.HostCertificates = []string{"."}
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
hostCertPath := filepath.Join(os.TempDir(), "host_cert.pub")
|
||||
err = os.WriteFile(hostCertPath, []byte(testCertValid), 0600)
|
||||
assert.NoError(t, err)
|
||||
sftpdConf.HostKeys = []string{privateKeyPath}
|
||||
sftpdConf.HostCertificates = []string{hostCertPath}
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "is not an host certificate")
|
||||
}
|
||||
err = os.WriteFile(hostCertPath, []byte(testPubKey), 0600)
|
||||
assert.NoError(t, err)
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "is not an SSH certificate")
|
||||
}
|
||||
err = os.WriteFile(hostCertPath, []byte("abc"), 0600)
|
||||
assert.NoError(t, err)
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "unable to parse host certificate")
|
||||
}
|
||||
err = os.WriteFile(hostCertPath, []byte(testHostCert), 0600)
|
||||
assert.NoError(t, err)
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = os.Remove(hostCertPath)
|
||||
assert.NoError(t, err)
|
||||
sftpdConf.HostKeys = nil
|
||||
sftpdConf.HostCertificates = nil
|
||||
sftpdConf.RevokedUserCertsFile = "."
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
"max_auth_tries": 0,
|
||||
"banner": "",
|
||||
"host_keys": [],
|
||||
"host_certificates": [],
|
||||
"kex_algorithms": [],
|
||||
"ciphers": [],
|
||||
"macs": [],
|
||||
|
|
Loading…
Reference in a new issue