Browse Source

ssh: allow to configure public key auth algorithms

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 1 year ago
parent
commit
c5c5860012

+ 1 - 0
docs/full-configuration.md

@@ -147,6 +147,7 @@ The configuration file contains the following sections:
   - `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-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. If you set one or more moduli files,  `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
   - `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`.
+  - `public_key_algorithms`, list of strings. Public key algorithms that the server will accept for client authentication. The supported values are: `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`, `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`. Default values: `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`, `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`.
   - `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
   - `revoked_user_certs_file`, path to a file containing the revoked user certificates. The path can be absolute or relative to the configuration directory. It must contain a JSON list with the public key fingerprints of the revoked certificates. Example content: `["SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es","SHA256:119+8cL/HH+NLMawRsJx6CzPF1I3xC+jpM60bQHXGE8"]`. The revocation list can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. Default: "".
   - `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to disable login banner.

+ 1 - 1
go.mod

@@ -177,5 +177,5 @@ replace (
 	github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20230820193955-e7243edeb89b
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
 	github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
-	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20231109082937-60ac5813bca0
+	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20231109180513-aa0daef37eeb
 )

+ 2 - 2
go.sum

@@ -153,8 +153,8 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
 github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
 github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
 github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/drakkan/crypto v0.0.0-20231109082937-60ac5813bca0 h1:5UwG68raSmbWbZdCDOxxaQpzfRS/2/4XLjP+o5lOvt0=
-github.com/drakkan/crypto v0.0.0-20231109082937-60ac5813bca0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+github.com/drakkan/crypto v0.0.0-20231109180513-aa0daef37eeb h1:2CkHnBtgdS29SoGR4SI9wkE711HRkC9983PNYi+vtKQ=
+github.com/drakkan/crypto v0.0.0-20231109180513-aa0daef37eeb/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
 github.com/drakkan/ftpserverlib v0.0.0-20230820193955-e7243edeb89b h1:sCtiYerLxfOQrSludkwGwwXLlSVHxpvfmyOxjCOf0ec=

+ 2 - 0
internal/config/config.go

@@ -263,6 +263,7 @@ func Init() {
 			KexAlgorithms:                     []string{},
 			Ciphers:                           []string{},
 			MACs:                              []string{},
+			PublicKeyAlgorithms:               []string{},
 			TrustedUserCAKeys:                 []string{},
 			RevokedUserCertsFile:              "",
 			LoginBannerFile:                   "",
@@ -2020,6 +2021,7 @@ func setViperDefaults() {
 	viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
 	viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
 	viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
+	viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
 	viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
 	viper.SetDefault("sftpd.revoked_user_certs_file", globalConf.SFTPD.RevokedUserCertsFile)
 	viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)

+ 30 - 12
internal/dataprovider/configs.go

@@ -28,8 +28,9 @@ import (
 
 // Supported values for host keys, KEXs, ciphers, MACs
 var (
-	supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
-	supportedKexAlgos     = []string{
+	supportedHostKeyAlgos   = []string{ssh.KeyAlgoRSA}
+	supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.KeyAlgoDSA}
+	supportedKexAlgos       = []string{
 		"diffie-hellman-group16-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
 		"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
 	}
@@ -45,17 +46,21 @@ var (
 
 // SFTPDConfigs defines configurations for SFTPD
 type SFTPDConfigs struct {
-	HostKeyAlgos  []string `json:"host_key_algos,omitempty"`
-	Moduli        []string `json:"moduli,omitempty"`
-	KexAlgorithms []string `json:"kex_algorithms,omitempty"`
-	Ciphers       []string `json:"ciphers,omitempty"`
-	MACs          []string `json:"macs,omitempty"`
+	HostKeyAlgos   []string `json:"host_key_algos,omitempty"`
+	PublicKeyAlgos []string `json:"public_key_algos,omitempty"`
+	Moduli         []string `json:"moduli,omitempty"`
+	KexAlgorithms  []string `json:"kex_algorithms,omitempty"`
+	Ciphers        []string `json:"ciphers,omitempty"`
+	MACs           []string `json:"macs,omitempty"`
 }
 
 func (c *SFTPDConfigs) isEmpty() bool {
 	if len(c.HostKeyAlgos) > 0 {
 		return false
 	}
+	if len(c.PublicKeyAlgos) > 0 {
+		return false
+	}
 	if len(c.Moduli) > 0 {
 		return false
 	}
@@ -76,6 +81,11 @@ func (*SFTPDConfigs) GetSupportedHostKeyAlgos() []string {
 	return supportedHostKeyAlgos
 }
 
+// GetSupportedPublicKeyAlgos returns the supported legacy public key algos
+func (*SFTPDConfigs) GetSupportedPublicKeyAlgos() []string {
+	return supportedPublicKeyAlgos
+}
+
 // GetSupportedKEXAlgos returns the supported KEX algos
 func (*SFTPDConfigs) GetSupportedKEXAlgos() []string {
 	return supportedKexAlgos
@@ -129,12 +139,19 @@ func (c *SFTPDConfigs) validate() error {
 			return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
 		}
 	}
+	for _, algo := range c.PublicKeyAlgos {
+		if !util.Contains(supportedPublicKeyAlgos, algo) {
+			return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
+		}
+	}
 	return nil
 }
 
 func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
 	hostKeys := make([]string, len(c.HostKeyAlgos))
 	copy(hostKeys, c.HostKeyAlgos)
+	publicKeys := make([]string, len(c.PublicKeyAlgos))
+	copy(publicKeys, c.PublicKeyAlgos)
 	moduli := make([]string, len(c.Moduli))
 	copy(moduli, c.Moduli)
 	kexs := make([]string, len(c.KexAlgorithms))
@@ -145,11 +162,12 @@ func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
 	copy(macs, c.MACs)
 
 	return &SFTPDConfigs{
-		HostKeyAlgos:  hostKeys,
-		Moduli:        moduli,
-		KexAlgorithms: kexs,
-		Ciphers:       ciphers,
-		MACs:          macs,
+		HostKeyAlgos:   hostKeys,
+		PublicKeyAlgos: publicKeys,
+		Moduli:         moduli,
+		KexAlgorithms:  kexs,
+		Ciphers:        ciphers,
+		MACs:           macs,
 	}
 }
 

+ 12 - 1
internal/httpd/httpd_test.go

@@ -7823,7 +7823,8 @@ func TestLoaddata(t *testing.T) {
 	}
 	configs := dataprovider.Configs{
 		SFTPD: &dataprovider.SFTPDConfigs{
-			HostKeyAlgos: []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},
+			HostKeyAlgos:   []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},
+			PublicKeyAlgos: []string{ssh.KeyAlgoDSA},
 		},
 		SMTP: &dataprovider.SMTPConfigs{
 			Host: "mail.example.com",
@@ -7890,6 +7891,7 @@ func TestLoaddata(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, configs.SMTP, configsGet.SMTP)
 	assert.Equal(t, []string{ssh.KeyAlgoRSA}, configsGet.SFTPD.HostKeyAlgos)
+	assert.Equal(t, []string{ssh.KeyAlgoDSA}, configsGet.SFTPD.PublicKeyAlgos)
 	assert.Len(t, configsGet.SFTPD.Moduli, 0)
 	assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
 	assert.Len(t, configsGet.SFTPD.Ciphers, 0)
@@ -12722,6 +12724,7 @@ func TestWebConfigsMock(t *testing.T) {
 	// save SFTP configs
 	form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
 	form.Add("sftp_host_key_algos", ssh.CertAlgoDSAv01)
+	form.Set("sftp_pub_key_algos", ssh.KeyAlgoDSA)
 	form.Set("sftp_moduli", "path 1 , path 2")
 	form.Set("form_action", "sftp_submit")
 	req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
@@ -12733,6 +12736,7 @@ func TestWebConfigsMock(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
 	form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
 	form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
+	form.Set("sftp_pub_key_algos", ssh.KeyAlgoDSA)
 	form.Set("sftp_kex_algos", "diffie-hellman-group18-sha512")
 	form.Add("sftp_kex_algos", "diffie-hellman-group16-sha512")
 	req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
@@ -12747,6 +12751,8 @@ func TestWebConfigsMock(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
 	assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
+	assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
+	assert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.KeyAlgoDSA)
 	assert.Len(t, configs.SFTPD.Moduli, 2)
 	assert.Contains(t, configs.SFTPD.Moduli, "path 1")
 	assert.Contains(t, configs.SFTPD.Moduli, "path 2")
@@ -12795,6 +12801,8 @@ func TestWebConfigsMock(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
 	assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
+	assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
+	assert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.KeyAlgoDSA)
 	assert.Len(t, configs.SFTPD.Moduli, 2)
 	assert.Equal(t, "mail.example.net", configs.SMTP.Host)
 	assert.Equal(t, 587, configs.SMTP.Port)
@@ -12865,6 +12873,8 @@ func TestWebConfigsMock(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
 	assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
+	assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
+	assert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.KeyAlgoDSA)
 	assert.Len(t, configs.SFTPD.Moduli, 2)
 	assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)
 	assert.Equal(t, 7, configs.ACME.Protocols)
@@ -12896,6 +12906,7 @@ func TestWebConfigsMock(t *testing.T) {
 	configs, err = dataprovider.GetConfigs()
 	assert.NoError(t, err)
 	assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
+	assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
 	assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)
 	assert.Equal(t, 1, configs.ACME.Protocols)
 	assert.Equal(t, domain, configs.ACME.Domain)

+ 6 - 5
internal/httpd/webadmin.go

@@ -2553,11 +2553,12 @@ func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListT
 
 func getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {
 	return &dataprovider.SFTPDConfigs{
-		HostKeyAlgos:  r.Form["sftp_host_key_algos"],
-		Moduli:        getSliceFromDelimitedValues(r.Form.Get("sftp_moduli"), ","),
-		KexAlgorithms: r.Form["sftp_kex_algos"],
-		Ciphers:       r.Form["sftp_ciphers"],
-		MACs:          r.Form["sftp_macs"],
+		HostKeyAlgos:   r.Form["sftp_host_key_algos"],
+		PublicKeyAlgos: r.Form["sftp_pub_key_algos"],
+		Moduli:         getSliceFromDelimitedValues(r.Form.Get("sftp_moduli"), ","),
+		KexAlgorithms:  r.Form["sftp_kex_algos"],
+		Ciphers:        r.Form["sftp_ciphers"],
+		MACs:           r.Form["sftp_macs"],
 	}
 }
 

+ 9 - 5
internal/sftpd/internal_test.go

@@ -1879,13 +1879,15 @@ func TestConfigsFromProvider(t *testing.T) {
 	assert.Len(t, c.KexAlgorithms, 0)
 	assert.Len(t, c.Ciphers, 0)
 	assert.Len(t, c.MACs, 0)
+	assert.Len(t, c.PublicKeyAlgorithms, 0)
 	configs := dataprovider.Configs{
 		SFTPD: &dataprovider.SFTPDConfigs{
-			HostKeyAlgos:  []string{ssh.KeyAlgoRSA},
-			Moduli:        []string{"/etc/ssh/moduli"},
-			KexAlgorithms: []string{kexDHGroupExchangeSHA256},
-			Ciphers:       []string{"aes128-cbc", "aes192-cbc", "aes256-cbc"},
-			MACs:          []string{"hmac-sha2-512-etm@openssh.com"},
+			HostKeyAlgos:   []string{ssh.KeyAlgoRSA},
+			Moduli:         []string{"/etc/ssh/moduli"},
+			KexAlgorithms:  []string{kexDHGroupExchangeSHA256},
+			Ciphers:        []string{"aes128-cbc", "aes192-cbc", "aes256-cbc"},
+			MACs:           []string{"hmac-sha2-512-etm@openssh.com"},
+			PublicKeyAlgos: []string{ssh.KeyAlgoDSA},
 		},
 	}
 	err = dataprovider.UpdateConfigs(&configs, "", "", "")
@@ -1896,11 +1898,13 @@ func TestConfigsFromProvider(t *testing.T) {
 	expectedKEXs := append(preferredKexAlgos, configs.SFTPD.KexAlgorithms...)
 	expectedCiphers := append(preferredCiphers, configs.SFTPD.Ciphers...)
 	expectedMACs := append(preferredMACs, configs.SFTPD.MACs...)
+	expectedPublicKeyAlgos := append(preferredPublicKeyAlgos, configs.SFTPD.PublicKeyAlgos...)
 	assert.Equal(t, expectedHostKeyAlgos, c.HostKeyAlgorithms)
 	assert.Equal(t, expectedKEXs, c.KexAlgorithms)
 	assert.Equal(t, expectedCiphers, c.Ciphers)
 	assert.Equal(t, expectedMACs, c.MACs)
 	assert.Equal(t, configs.SFTPD.Moduli, c.Moduli)
+	assert.Equal(t, expectedPublicKeyAlgos, c.PublicKeyAlgorithms)
 
 	err = dataprovider.UpdateConfigs(nil, "", "", "")
 	assert.NoError(t, err)

+ 55 - 10
internal/sftpd/server.go

@@ -69,6 +69,19 @@ var (
 		ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
 		ssh.KeyAlgoED25519,
 	}
+	supportedPublicKeyAlgos = []string{
+		ssh.KeyAlgoED25519,
+		ssh.KeyAlgoSKED25519, ssh.KeyAlgoSKECDSA256,
+		ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
+		ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA,
+		ssh.KeyAlgoDSA,
+	}
+	preferredPublicKeyAlgos = []string{
+		ssh.KeyAlgoED25519,
+		ssh.KeyAlgoSKED25519, ssh.KeyAlgoSKECDSA256,
+		ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
+		ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512,
+	}
 	supportedKexAlgos = []string{
 		"curve25519-sha256", "curve25519-sha256@libssh.org",
 		"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
@@ -171,6 +184,8 @@ type Configuration struct {
 	// MACs Specifies the available MAC (message authentication code) algorithms
 	// in preference order
 	MACs []string `json:"macs" mapstructure:"macs"`
+	// PublicKeyAlgorithms lists the supported public key algorithms for client authentication.
+	PublicKeyAlgorithms []string `json:"public_key_algorithms" mapstructure:"public_key_algorithms"`
 	// TrustedUserCAKeys specifies a list of public keys paths of certificate authorities
 	// that are trusted to sign user certificates for authentication.
 	// The paths can be absolute or relative to the configuration directory
@@ -318,6 +333,12 @@ func (c *Configuration) loadFromProvider() error {
 		}
 		c.HostKeyAlgorithms = append(c.HostKeyAlgorithms, configs.SFTPD.HostKeyAlgos...)
 	}
+	if len(configs.SFTPD.PublicKeyAlgos) > 0 {
+		if len(c.PublicKeyAlgorithms) == 0 {
+			c.PublicKeyAlgorithms = preferredPublicKeyAlgos
+		}
+		c.PublicKeyAlgorithms = append(c.PublicKeyAlgorithms, configs.SFTPD.PublicKeyAlgos...)
+	}
 	c.Moduli = append(c.Moduli, configs.SFTPD.Moduli...)
 	if len(configs.SFTPD.KexAlgorithms) > 0 {
 		if len(c.KexAlgorithms) == 0 {
@@ -441,7 +462,7 @@ func (c *Configuration) serve(listener net.Listener, serverConfig *ssh.ServerCon
 	}
 }
 
-func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) error {
+func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error {
 	if len(c.HostKeyAlgorithms) == 0 {
 		c.HostKeyAlgorithms = preferredHostKeyAlgos
 	} else {
@@ -453,6 +474,27 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 		}
 	}
 
+	if len(c.PublicKeyAlgorithms) > 0 {
+		c.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)
+		for _, algo := range c.PublicKeyAlgorithms {
+			if !util.Contains(supportedPublicKeyAlgos, algo) {
+				return fmt.Errorf("unsupported public key authentication algorithm %q", algo)
+			}
+		}
+	} else {
+		c.PublicKeyAlgorithms = preferredPublicKeyAlgos
+	}
+	serverConfig.PublicKeyAuthAlgorithms = c.PublicKeyAlgorithms
+	serviceStatus.PublicKeyAlgorithms = c.PublicKeyAlgorithms
+
+	return nil
+}
+
+func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) error {
+	if err := c.configureKeyAlgos(serverConfig); err != nil {
+		return err
+	}
+
 	if len(c.KexAlgorithms) > 0 {
 		hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256)
 		if !hasDHGroupKEX {
@@ -468,11 +510,12 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 				return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
 			}
 		}
-		serverConfig.KeyExchanges = c.KexAlgorithms
-		serviceStatus.KexAlgorithms = c.KexAlgorithms
 	} else {
-		serviceStatus.KexAlgorithms = preferredKexAlgos
+		c.KexAlgorithms = preferredKexAlgos
 	}
+	serverConfig.KeyExchanges = c.KexAlgorithms
+	serviceStatus.KexAlgorithms = c.KexAlgorithms
+
 	if len(c.Ciphers) > 0 {
 		c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
 		for _, cipher := range c.Ciphers {
@@ -480,11 +523,12 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 				return fmt.Errorf("unsupported cipher %q", cipher)
 			}
 		}
-		serverConfig.Ciphers = c.Ciphers
-		serviceStatus.Ciphers = c.Ciphers
 	} else {
-		serviceStatus.Ciphers = preferredCiphers
+		c.Ciphers = preferredCiphers
 	}
+	serverConfig.Ciphers = c.Ciphers
+	serviceStatus.Ciphers = c.Ciphers
+
 	if len(c.MACs) > 0 {
 		c.MACs = util.RemoveDuplicates(c.MACs, true)
 		for _, mac := range c.MACs {
@@ -492,11 +536,12 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 				return fmt.Errorf("unsupported MAC algorithm %q", mac)
 			}
 		}
-		serverConfig.MACs = c.MACs
-		serviceStatus.MACs = c.MACs
 	} else {
-		serviceStatus.MACs = preferredMACs
+		c.MACs = preferredMACs
 	}
+	serverConfig.MACs = c.MACs
+	serviceStatus.MACs = c.MACs
+
 	return nil
 }
 

+ 15 - 8
internal/sftpd/sftpd.go

@@ -77,14 +77,15 @@ func (h *HostKey) GetAlgosAsString() string {
 
 // ServiceStatus defines the service status
 type ServiceStatus struct {
-	IsActive        bool      `json:"is_active"`
-	Bindings        []Binding `json:"bindings"`
-	SSHCommands     []string  `json:"ssh_commands"`
-	HostKeys        []HostKey `json:"host_keys"`
-	Authentications []string  `json:"authentications"`
-	MACs            []string  `json:"macs"`
-	KexAlgorithms   []string  `json:"kex_algorithms"`
-	Ciphers         []string  `json:"ciphers"`
+	IsActive            bool      `json:"is_active"`
+	Bindings            []Binding `json:"bindings"`
+	SSHCommands         []string  `json:"ssh_commands"`
+	HostKeys            []HostKey `json:"host_keys"`
+	Authentications     []string  `json:"authentications"`
+	MACs                []string  `json:"macs"`
+	KexAlgorithms       []string  `json:"kex_algorithms"`
+	Ciphers             []string  `json:"ciphers"`
+	PublicKeyAlgorithms []string  `json:"public_key_algorithms"`
 }
 
 // GetSSHCommandsAsString returns enabled SSH commands as comma separated string
@@ -112,6 +113,12 @@ func (s *ServiceStatus) GetCiphersAsString() string {
 	return strings.Join(s.Ciphers, ", ")
 }
 
+// GetPublicKeysAlgosAsString returns enabled public key authentication
+// algorithms as comma separated string
+func (s *ServiceStatus) GetPublicKeysAlgosAsString() string {
+	return strings.Join(s.PublicKeyAlgorithms, ", ")
+}
+
 // GetStatus returns the server status
 func GetStatus() ServiceStatus {
 	return serviceStatus

+ 7 - 0
internal/sftpd/sftpd_test.go

@@ -438,6 +438,12 @@ func TestInitialization(t *testing.T) {
 		assert.Contains(t, err.Error(), "unsupported key-exchange algorithm")
 	}
 	sftpdConf.KexAlgorithms = nil
+	sftpdConf.PublicKeyAlgorithms = []string{"not a pub key algo"}
+	err = sftpdConf.Initialize(configDir)
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "unsupported public key authentication algorithm")
+	}
+	sftpdConf.PublicKeyAlgorithms = nil
 	sftpdConf.HostKeyAlgorithms = []string{"not a host key algo"}
 	err = sftpdConf.Initialize(configDir)
 	if assert.Error(t, err) {
@@ -581,6 +587,7 @@ func TestBasicSFTPHandling(t *testing.T) {
 	assert.NotEmpty(t, status.GetMACsAsString())
 	assert.NotEmpty(t, status.GetKEXsAsString())
 	assert.NotEmpty(t, status.GetCiphersAsString())
+	assert.NotEmpty(t, status.GetPublicKeysAlgosAsString())
 }
 
 func TestBasicSFTPFsHandling(t *testing.T) {

+ 4 - 0
openapi/openapi.yaml

@@ -6505,6 +6505,10 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/SSHAuthentications'
+        public_key_algorithms:
+          type: array
+          items:
+            type: string
         macs:
           type: array
           items:

+ 1 - 0
sftpgo.json

@@ -91,6 +91,7 @@
     "kex_algorithms": [],
     "ciphers": [],
     "macs": [],
+    "public_key_algorithms": [],
     "trusted_user_ca_keys": [],
     "revoked_user_certs_file": "",
     "login_banner_file": "",

+ 11 - 0
templates/webadmin/configs.html

@@ -65,6 +65,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
                                 </div>
                             </div>
 
+                            <div class="form-group row">
+                                <label for="idPubKeyAlgos" class="col-sm-2 col-form-label">Public Key Algos</label>
+                                <div class="col-sm-10">
+                                    <select class="form-control selectpicker" id="idPubKeyAlgos" name="sftp_pub_key_algos" multiple>
+                                        {{range $val := .Configs.SFTPD.GetSupportedPublicKeyAlgos}}
+                                        <option value="{{$val}}" {{range $algo :=$.Configs.SFTPD.PublicKeyAlgos }}{{if eq $algo $val}}selected{{end}}{{end}}>{{$val}}</option>
+                                        {{end}}
+                                    </select>
+                                </div>
+                            </div>
+
                             <div class="form-group row">
                                 <label for="idModuli" class="col-sm-2 col-form-label">Moduli</label>
                                 <div class="col-sm-10">

+ 4 - 2
templates/webadmin/status.html

@@ -50,9 +50,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
                     <br>
                     {{end}}
                     <br>
-                    MAC algorithms: "{{.Status.SSH.GetMACsAsString}}"
+                    Public key authentication algorithms: "{{.Status.SSH.GetPublicKeysAlgosAsString}}"
                     <br><br>
-                    KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}"
+                    Message authentication algorithms: "{{.Status.SSH.GetMACsAsString}}"
+                    <br><br>
+                    Key exchange algorithms: "{{.Status.SSH.GetKEXsAsString}}"
                     <br><br>
                     Ciphers: "{{.Status.SSH.GetCiphersAsString}}"
                     <br>