FTP: add support for client certificate authentication

This commit is contained in:
Nicola Murino 2020-12-29 09:20:09 +01:00
parent 141ca6777c
commit 40e759c983
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
10 changed files with 62 additions and 10 deletions

View file

@ -38,6 +38,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
- SCP and rsync are supported. - SCP and rsync are supported.
- FTP/S is supported. You can configure the FTP service to require TLS for both control and data connections. - FTP/S is supported. You can configure the FTP service to require TLS for both control and data connections.
- [WebDAV](./docs/webdav.md) is supported. - [WebDAV](./docs/webdav.md) is supported.
- Two-Way TLS authentication, aka TLS with client certificate authentication, is supported for FTPS and WebDAV over HTTPS.
- Support for serving local filesystem, encrypted local filesystem, S3 Compatible Object Storage, Google Cloud Storage, Azure Blob Storage or other SFTP accounts over SFTP/SCP/FTP/WebDAV. - Support for serving local filesystem, encrypted local filesystem, S3 Compatible Object Storage, Google Cloud Storage, Azure Blob Storage or other SFTP accounts over SFTP/SCP/FTP/WebDAV.
- Per user protocols restrictions. You can configure the allowed protocols (SSH/FTP/WebDAV) for each user. - Per user protocols restrictions. You can configure the allowed protocols (SSH/FTP/WebDAV) for each user.
- [Prometheus metrics](./docs/metrics.md) are exposed. - [Prometheus metrics](./docs/metrics.md) are exposed.

View file

@ -47,6 +47,9 @@ var (
Address: "", Address: "",
Port: 0, Port: 0,
ApplyProxyConfig: true, ApplyProxyConfig: true,
TLSMode: 0,
ForcePassiveIP: "",
ClientAuthType: 0,
} }
defaultWebDAVDBinding = webdavd.Binding{ defaultWebDAVDBinding = webdavd.Binding{
Address: "", Address: "",
@ -120,6 +123,7 @@ func Init() {
CombineSupport: 0, CombineSupport: 0,
CertificateFile: "", CertificateFile: "",
CertificateKeyFile: "", CertificateKeyFile: "",
CACertificates: []string{},
}, },
WebDAVD: webdavd.Configuration{ WebDAVD: webdavd.Configuration{
Bindings: []webdavd.Binding{defaultWebDAVDBinding}, Bindings: []webdavd.Binding{defaultWebDAVDBinding},
@ -596,6 +600,12 @@ func getFTPDBindingFromEnv(idx int) {
isSet = true isSet = true
} }
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
if ok {
binding.ClientAuthType = clientAuthType
isSet = true
}
if isSet { if isSet {
if len(globalConf.FTPD.Bindings) > idx { if len(globalConf.FTPD.Bindings) > idx {
globalConf.FTPD.Bindings[idx] = binding globalConf.FTPD.Bindings[idx] = binding
@ -678,6 +688,7 @@ func setViperDefaults() {
viper.SetDefault("ftpd.combine_support", globalConf.FTPD.CombineSupport) viper.SetDefault("ftpd.combine_support", globalConf.FTPD.CombineSupport)
viper.SetDefault("ftpd.certificate_file", globalConf.FTPD.CertificateFile) viper.SetDefault("ftpd.certificate_file", globalConf.FTPD.CertificateFile)
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile) viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
viper.SetDefault("ftpd.ca_certificates", globalConf.FTPD.CACertificates)
viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile) viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile)
viper.SetDefault("webdavd.certificate_key_file", globalConf.WebDAVD.CertificateKeyFile) viper.SetDefault("webdavd.certificate_key_file", globalConf.WebDAVD.CertificateKeyFile)
viper.SetDefault("webdavd.ca_certificates", globalConf.WebDAVD.CACertificates) viper.SetDefault("webdavd.ca_certificates", globalConf.WebDAVD.CACertificates)

View file

@ -549,6 +549,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG", "t") os.Setenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG", "t")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE", "1") os.Setenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE", "1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP", "127.0.1.1") os.Setenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP", "127.0.1.1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE", "1")
t.Cleanup(func() { t.Cleanup(func() {
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS") os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS")
@ -561,6 +562,7 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG") os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE") os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP") os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE")
}) })
configDir := ".." configDir := ".."
@ -571,13 +573,15 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
require.Equal(t, 2200, bindings[0].Port) require.Equal(t, 2200, bindings[0].Port)
require.Equal(t, "127.0.0.1", bindings[0].Address) require.Equal(t, "127.0.0.1", bindings[0].Address)
require.False(t, bindings[0].ApplyProxyConfig) require.False(t, bindings[0].ApplyProxyConfig)
require.Equal(t, bindings[0].TLSMode, 2) require.Equal(t, 2, bindings[0].TLSMode)
require.Equal(t, bindings[0].ForcePassiveIP, "127.0.1.2") require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP)
require.Equal(t, 0, bindings[0].ClientAuthType)
require.Equal(t, 2203, bindings[1].Port) require.Equal(t, 2203, bindings[1].Port)
require.Equal(t, "127.0.1.1", bindings[1].Address) require.Equal(t, "127.0.1.1", bindings[1].Address)
require.True(t, bindings[1].ApplyProxyConfig) require.True(t, bindings[1].ApplyProxyConfig)
require.Equal(t, bindings[1].TLSMode, 1) require.Equal(t, 1, bindings[1].TLSMode)
require.Equal(t, bindings[1].ForcePassiveIP, "127.0.1.1") require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
require.Equal(t, 1, bindings[1].ClientAuthType)
} }
func TestWebDAVBindingsFromEnv(t *testing.T) { func TestWebDAVBindingsFromEnv(t *testing.T) {
@ -611,6 +615,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.Equal(t, 8000, bindings[1].Port) require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address) require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS) require.False(t, bindings[1].EnableHTTPS)
require.Equal(t, 0, bindings[1].ClientAuthType)
require.Equal(t, 9000, bindings[2].Port) require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address) require.Equal(t, "127.0.1.1", bindings[2].Address)
require.True(t, bindings[2].EnableHTTPS) require.True(t, bindings[2].EnableHTTPS)

View file

@ -97,6 +97,7 @@ The configuration file contains the following sections:
- `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Default `true` - `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Default `true`
- `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0. - `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0.
- `force_passive_ip`, ip address. External IP address to expose for passive connections. Leavy empty to autodetect. Defaut: "". - `force_passive_ip`, ip address. External IP address to expose for passive connections. Leavy empty to autodetect. Defaut: "".
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to FTP authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `bind_port`, integer. Deprecated, please use `bindings` - `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings` - `bind_address`, string. Deprecated, please use `bindings`
- `banner`, string. Greeting banner displayed when a connection first comes in. Leave empty to use the default banner. Default `SFTPGo <version> ready`, for example `SFTPGo 1.0.0-dev ready`. - `banner`, string. Greeting banner displayed when a connection first comes in. Leave empty to use the default banner. Default `SFTPGo <version> ready`, for example `SFTPGo 1.0.0-dev ready`.
@ -110,13 +111,14 @@ The configuration file contains the following sections:
- `combine_support`, integer. Set to 1 to enable support for the non standard `COMB` FTP command. Combine is only supported for local filesystem, for cloud backends it has no advantage as it will download the partial files and will upload the combined one. Cloud backends natively support multipart uploads. Default `0`. - `combine_support`, integer. Set to 1 to enable support for the non standard `COMB` FTP command. Combine is only supported for local filesystem, for cloud backends it has no advantage as it will download the partial files and will upload the combined one. Cloud backends natively support multipart uploads. Default `0`.
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir. - `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `ca_certificates`, list of strings. Set of root certificate authorities to use to verify client certificates.
- `tls_mode`, integer. Deprecated, please use `bindings` - `tls_mode`, integer. Deprecated, please use `bindings`
- **webdavd**, the configuration for the WebDAV server, more info [here](./webdav.md) - **webdavd**, the configuration for the WebDAV server, more info [here](./webdav.md)
- `bindings`, list of structs. Each struct has the following fields: - `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0. - `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
- `address`, string. Leave blank to listen on all available network interfaces. Default: "". - `address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false` - `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to basic auth. You need to define at least a certificate authority for this to work. Default: 0. - `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to basic authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `bind_port`, integer. Deprecated, please use `bindings` - `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings` - `bind_address`, string. Deprecated, please use `bindings`
- `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir. - `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir.

View file

@ -1,6 +1,6 @@
# WebDAV # WebDAV
The experimental `WebDAV` support can be enabled by setting a `bind_port` inside the `webdavd` configuration section. The experimental `WebDAV` support can be enabled by configuring one or more `bindings` inside the `webdavd` configuration section.
Each user has his own path like `http/s://<SFTPGo ip>:<WevDAVPORT>/<username>` and it must authenticate using password credentials. Each user has his own path like `http/s://<SFTPGo ip>:<WevDAVPORT>/<username>` and it must authenticate using password credentials.

View file

@ -34,6 +34,9 @@ type Binding struct {
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"` TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
// External IP address to expose for passive connections. // External IP address to expose for passive connections.
ForcePassiveIP string `json:"force_passive_ip" mapstructure:"force_passive_ip"` ForcePassiveIP string `json:"force_passive_ip" mapstructure:"force_passive_ip"`
// set to 1 to require client certificate authentication in addition to FTP auth.
// You need to define at least a certificate authority for this to work
ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
} }
// GetAddress returns the binding address // GetAddress returns the binding address
@ -102,6 +105,8 @@ type Configuration struct {
// "paramchange" request to the running service on Windows. // "paramchange" request to the running service on Windows.
CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"` CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"` CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
// CACertificates defines the set of root certificate authorities to use to verify client certificates.
CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
// Do not impose the port 20 for active data transfer. Enabling this option allows to run SFTPGo with less privilege // Do not impose the port 20 for active data transfer. Enabling this option allows to run SFTPGo with less privilege
ActiveTransfersPortNon20 bool `json:"active_transfers_port_non_20" mapstructure:"active_transfers_port_non_20"` ActiveTransfersPortNon20 bool `json:"active_transfers_port_non_20" mapstructure:"active_transfers_port_non_20"`
// Set to true to disable active FTP // Set to true to disable active FTP
@ -151,6 +156,9 @@ func (c *Configuration) Initialize(configDir string) error {
if err != nil { if err != nil {
return err return err
} }
if err := mgr.LoadRootCAs(c.CACertificates, configDir); err != nil {
return err
}
certMgr = mgr certMgr = mgr
} }
serviceStatus = ServiceStatus{ serviceStatus = ServiceStatus{

View file

@ -282,6 +282,14 @@ func TestInitializationFailure(t *testing.T) {
ftpdConf.Bindings[1].TLSMode = 1 ftpdConf.Bindings[1].TLSMode = 1
err = ftpdConf.Initialize(configDir) err = ftpdConf.Initialize(configDir)
require.Error(t, err) require.Error(t, err)
certPath := filepath.Join(os.TempDir(), "test_ftpd.crt")
keyPath := filepath.Join(os.TempDir(), "test_ftpd.key")
ftpdConf.CertificateFile = certPath
ftpdConf.CertificateKeyFile = keyPath
ftpdConf.CACertificates = []string{"invalid ca cert"}
err = ftpdConf.Initialize(configDir)
require.Error(t, err)
} }
func TestBasicFTPHandling(t *testing.T) { func TestBasicFTPHandling(t *testing.T) {

View file

@ -1,6 +1,7 @@
package ftpd package ftpd
import ( import (
"crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -164,6 +165,15 @@ func TestInitialization(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
certMgr = oldMgr certMgr = oldMgr
binding = Binding{
Port: 2121,
ClientAuthType: 1,
}
server = NewServer(c, configDir, binding, 0)
cfg, err := server.GetTLSConfig()
assert.NoError(t, err)
assert.Equal(t, tls.RequireAndVerifyClientCert, cfg.ClientAuth)
} }
func TestServerGetSettings(t *testing.T) { func TestServerGetSettings(t *testing.T) {

View file

@ -152,10 +152,15 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
// GetTLSConfig returns a TLS Certificate to use // GetTLSConfig returns a TLS Certificate to use
func (s *Server) GetTLSConfig() (*tls.Config, error) { func (s *Server) GetTLSConfig() (*tls.Config, error) {
if certMgr != nil { if certMgr != nil {
return &tls.Config{ tlsConfig := &tls.Config{
GetCertificate: certMgr.GetCertificateFunc(), GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
}, nil }
if s.binding.ClientAuthType == 1 {
tlsConfig.ClientCAs = certMgr.GetRootCAs()
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
return tlsConfig, nil
} }
return nil, errors.New("no TLS certificate configured") return nil, errors.New("no TLS certificate configured")
} }

View file

@ -45,7 +45,8 @@
"port": 0, "port": 0,
"apply_proxy_config": true, "apply_proxy_config": true,
"tls_mode": 0, "tls_mode": 0,
"force_passive_ip": "" "force_passive_ip": "",
"client_auth_type": 0
} }
], ],
"banner": "", "banner": "",
@ -60,7 +61,8 @@
"hash_support": 0, "hash_support": 0,
"combine_support": 0, "combine_support": 0,
"certificate_file": "", "certificate_file": "",
"certificate_key_file": "" "certificate_key_file": "",
"ca_certificates": []
}, },
"webdavd": { "webdavd": {
"bindings": [ "bindings": [