mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 14:20:55 +03:00
@@ -2,9 +2,14 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/logger"
|
"github.com/drakkan/sftpgo/logger"
|
||||||
|
"github.com/drakkan/sftpgo/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CertManager defines a TLS certificate manager
|
// CertManager defines a TLS certificate manager
|
||||||
@@ -12,7 +17,8 @@ type CertManager struct {
|
|||||||
certPath string
|
certPath string
|
||||||
keyPath string
|
keyPath string
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
cert *tls.Certificate
|
cert *tls.Certificate
|
||||||
|
rootCAs *x509.CertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadCertificate loads the configured x509 key pair
|
// LoadCertificate loads the configured x509 key pair
|
||||||
@@ -39,6 +45,44 @@ func (m *CertManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Cert
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRootCAs returns the set of root certificate authorities that servers
|
||||||
|
// use if required to verify a client certificate
|
||||||
|
func (m *CertManager) GetRootCAs() *x509.CertPool {
|
||||||
|
return m.rootCAs
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRootCAs tries to load root CA certificate authorities from the given paths
|
||||||
|
func (m *CertManager) LoadRootCAs(caCertificates []string, configDir string) error {
|
||||||
|
if len(caCertificates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
|
||||||
|
for _, rootCA := range caCertificates {
|
||||||
|
if !utils.IsFileInputValid(rootCA) {
|
||||||
|
return fmt.Errorf("invalid root CA certificate %#v", rootCA)
|
||||||
|
}
|
||||||
|
if rootCA != "" && !filepath.IsAbs(rootCA) {
|
||||||
|
rootCA = filepath.Join(configDir, rootCA)
|
||||||
|
}
|
||||||
|
crt, err := ioutil.ReadFile(rootCA)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rootCAs.AppendCertsFromPEM(crt) {
|
||||||
|
logger.Debug(logSender, "", "TLS certificate authority %#v successfully loaded", rootCA)
|
||||||
|
} else {
|
||||||
|
err := fmt.Errorf("unable to load TLS certificate authority %#v", rootCA)
|
||||||
|
logger.Debug(logSender, "", "%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.rootCAs = rootCAs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewCertManager creates a new certificate manager
|
// NewCertManager creates a new certificate manager
|
||||||
func NewCertManager(certificateFile, certificateKeyFile, logSender string) (*CertManager, error) {
|
func NewCertManager(certificateFile, certificateKeyFile, logSender string) (*CertManager, error) {
|
||||||
manager := &CertManager{
|
manager := &CertManager{
|
||||||
|
|||||||
@@ -56,6 +56,25 @@ func TestLoadCertificate(t *testing.T) {
|
|||||||
assert.Equal(t, certManager.cert, cert)
|
assert.Equal(t, certManager.cert, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = certManager.LoadRootCAs(nil, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = certManager.LoadRootCAs([]string{""}, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = certManager.LoadRootCAs([]string{"invalid"}, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// laoding the key as root CA must fail
|
||||||
|
err = certManager.LoadRootCAs([]string{keyPath}, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = certManager.LoadRootCAs([]string{certPath}, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
rootCa := certManager.GetRootCAs()
|
||||||
|
assert.NotNil(t, rootCa)
|
||||||
|
|
||||||
err = os.Remove(certPath)
|
err = os.Remove(certPath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.Remove(keyPath)
|
err = os.Remove(keyPath)
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ var (
|
|||||||
ApplyProxyConfig: true,
|
ApplyProxyConfig: true,
|
||||||
}
|
}
|
||||||
defaultWebDAVDBinding = webdavd.Binding{
|
defaultWebDAVDBinding = webdavd.Binding{
|
||||||
Address: "",
|
Address: "",
|
||||||
Port: 0,
|
Port: 0,
|
||||||
EnableHTTPS: false,
|
EnableHTTPS: false,
|
||||||
|
ClientAuthType: 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ func Init() {
|
|||||||
Bindings: []webdavd.Binding{defaultWebDAVDBinding},
|
Bindings: []webdavd.Binding{defaultWebDAVDBinding},
|
||||||
CertificateFile: "",
|
CertificateFile: "",
|
||||||
CertificateKeyFile: "",
|
CertificateKeyFile: "",
|
||||||
|
CACertificates: []string{},
|
||||||
Cors: webdavd.Cors{
|
Cors: webdavd.Cors{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
AllowedOrigins: []string{},
|
AllowedOrigins: []string{},
|
||||||
@@ -629,6 +631,12 @@ func getWebDAVDBindingFromEnv(idx int) {
|
|||||||
isSet = true
|
isSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
|
||||||
|
if ok {
|
||||||
|
binding.ClientAuthType = clientAuthType
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
if isSet {
|
if isSet {
|
||||||
if len(globalConf.WebDAVD.Bindings) > idx {
|
if len(globalConf.WebDAVD.Bindings) > idx {
|
||||||
globalConf.WebDAVD.Bindings[idx] = binding
|
globalConf.WebDAVD.Bindings[idx] = binding
|
||||||
@@ -672,6 +680,7 @@ func setViperDefaults() {
|
|||||||
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
|
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
|
||||||
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.cors.enabled", globalConf.WebDAVD.Cors.Enabled)
|
viper.SetDefault("webdavd.cors.enabled", globalConf.WebDAVD.Cors.Enabled)
|
||||||
viper.SetDefault("webdavd.cors.allowed_origins", globalConf.WebDAVD.Cors.AllowedOrigins)
|
viper.SetDefault("webdavd.cors.allowed_origins", globalConf.WebDAVD.Cors.AllowedOrigins)
|
||||||
viper.SetDefault("webdavd.cors.allowed_methods", globalConf.WebDAVD.Cors.AllowedMethods)
|
viper.SetDefault("webdavd.cors.allowed_methods", globalConf.WebDAVD.Cors.AllowedMethods)
|
||||||
|
|||||||
@@ -589,6 +589,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1")
|
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1")
|
||||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000")
|
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000")
|
||||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1")
|
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1")
|
||||||
|
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
|
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
|
||||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
|
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
|
||||||
@@ -596,6 +597,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
|
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
|
||||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
|
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
|
||||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
|
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
|
||||||
|
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE")
|
||||||
})
|
})
|
||||||
|
|
||||||
configDir := ".."
|
configDir := ".."
|
||||||
@@ -612,6 +614,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||||||
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)
|
||||||
|
require.Equal(t, 1, bindings[2].ClientAuthType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigFromEnv(t *testing.T) {
|
func TestConfigFromEnv(t *testing.T) {
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ The configuration file contains the following sections:
|
|||||||
- `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.
|
||||||
- `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.
|
||||||
- `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 a private key are required to enable HTTPS connections. 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 a private key are required to enable HTTPS connections. 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.
|
||||||
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
|
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
|
||||||
- `enabled`, boolean, set to true to enable CORS.
|
- `enabled`, boolean, set to true to enable CORS.
|
||||||
- `allowed_origins`, list of strings.
|
- `allowed_origins`, list of strings.
|
||||||
|
|||||||
@@ -67,11 +67,13 @@
|
|||||||
{
|
{
|
||||||
"address": "",
|
"address": "",
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"enable_https": false
|
"enable_https": false,
|
||||||
|
"client_auth_type": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"certificate_file": "",
|
"certificate_file": "",
|
||||||
"certificate_key_file": "",
|
"certificate_key_file": "",
|
||||||
|
"ca_certificates": [],
|
||||||
"cors": {
|
"cors": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"allowed_origins": [],
|
"allowed_origins": [],
|
||||||
|
|||||||
@@ -44,11 +44,14 @@ func newServer(config *Configuration, configDir string) (*webDavServer, error) {
|
|||||||
}
|
}
|
||||||
certificateFile := getConfigPath(config.CertificateFile, configDir)
|
certificateFile := getConfigPath(config.CertificateFile, configDir)
|
||||||
certificateKeyFile := getConfigPath(config.CertificateKeyFile, configDir)
|
certificateKeyFile := getConfigPath(config.CertificateKeyFile, configDir)
|
||||||
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
|
if certificateFile != "" && certificateKeyFile != "" {
|
||||||
server.certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, logSender)
|
server.certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, logSender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server, err
|
return server, err
|
||||||
}
|
}
|
||||||
|
if err := server.certMgr.LoadRootCAs(config.CACertificates, configDir); err != nil {
|
||||||
|
return server, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
@@ -79,6 +82,10 @@ func (s *webDavServer) listenAndServe(binding Binding) error {
|
|||||||
GetCertificate: s.certMgr.GetCertificateFunc(),
|
GetCertificate: s.certMgr.GetCertificateFunc(),
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
if binding.ClientAuthType == 1 {
|
||||||
|
httpServer.TLSConfig.ClientCAs = s.certMgr.GetRootCAs()
|
||||||
|
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
return httpServer.ListenAndServeTLS("", "")
|
return httpServer.ListenAndServeTLS("", "")
|
||||||
}
|
}
|
||||||
binding.EnableHTTPS = false
|
binding.EnableHTTPS = false
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ type Binding struct {
|
|||||||
Port int `json:"port" mapstructure:"port"`
|
Port int `json:"port" mapstructure:"port"`
|
||||||
// you also need to provide a certificate for enabling HTTPS
|
// you also need to provide a certificate for enabling HTTPS
|
||||||
EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
|
EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
|
||||||
|
// 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
|
||||||
|
ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAddress returns the binding address
|
// GetAddress returns the binding address
|
||||||
@@ -94,6 +97,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"`
|
||||||
// CORS configuration
|
// CORS configuration
|
||||||
Cors Cors `json:"cors" mapstructure:"cors"`
|
Cors Cors `json:"cors" mapstructure:"cors"`
|
||||||
// Cache configuration
|
// Cache configuration
|
||||||
@@ -169,7 +174,7 @@ func getConfigPath(name, configDir string) string {
|
|||||||
if !utils.IsFileInputValid(name) {
|
if !utils.IsFileInputValid(name) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if len(name) > 0 && !filepath.IsAbs(name) {
|
if name != "" && !filepath.IsAbs(name) {
|
||||||
return filepath.Join(configDir, name)
|
return filepath.Join(configDir, name)
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
|
|||||||
@@ -256,6 +256,24 @@ func TestInitialization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err = cfg.Initialize(configDir)
|
err = cfg.Initialize(configDir)
|
||||||
assert.EqualError(t, err, common.ErrNoBinding.Error())
|
assert.EqualError(t, err, common.ErrNoBinding.Error())
|
||||||
|
|
||||||
|
cfg.CertificateFile = certPath
|
||||||
|
cfg.CertificateKeyFile = keyPath
|
||||||
|
cfg.CACertificates = []string{""}
|
||||||
|
|
||||||
|
cfg.Bindings = []webdavd.Binding{
|
||||||
|
{
|
||||||
|
Port: 9022,
|
||||||
|
ClientAuthType: 1,
|
||||||
|
EnableHTTPS: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = cfg.Initialize(configDir)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
cfg.CACertificates = nil
|
||||||
|
err = cfg.Initialize(configDir)
|
||||||
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicHandling(t *testing.T) {
|
func TestBasicHandling(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user