httpd: allow to reload the https certificate without restarting the service
HTTPS certificate can be reloaded on demand sending a SIGHUP signal on Unix based systems and a "paramchange" request to the running service on Windows
This commit is contained in:
parent
9359669cd4
commit
5bfaae9202
7 changed files with 134 additions and 11 deletions
|
@ -187,7 +187,7 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
|
||||
- `auth_user_file`, string. Path to a file used to store usernames and password for basic authentication. This can be an absolute path or a path relative to the config dir. We support HTTP basic authentication and the file format must conform to the one generated using the Apache tool. The supported password formats are bcrypt (`$2y$` prefix) and md5 crypt (`$apr1$` prefix). If empty HTTP authentication is disabled.
|
||||
- `certificate_file`, string. Certificate for 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. If both the certificate and the private key are provided the the server will expect HTTPS connections.
|
||||
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided the the server will expect 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.
|
||||
|
||||
Here is a full example showing the default config in JSON format:
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
@ -42,6 +43,7 @@ var (
|
|||
dataProvider dataprovider.Provider
|
||||
backupsPath string
|
||||
httpAuth httpAuthProvider
|
||||
certMgr *certManager
|
||||
)
|
||||
|
||||
// Conf httpd daemon configuration
|
||||
|
@ -63,7 +65,9 @@ type Conf struct {
|
|||
// If empty HTTP authentication is disabled
|
||||
AuthUserFile string `json:"auth_user_file" mapstructure:"auth_user_file"`
|
||||
// If files containing a certificate and matching private key for the server are provided the server will expect
|
||||
// HTTPS connections
|
||||
// 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.
|
||||
CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
|
||||
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
|
||||
}
|
||||
|
@ -103,11 +107,26 @@ func (c Conf) Initialize(configDir string) error {
|
|||
MaxHeaderBytes: 1 << 16, // 64KB
|
||||
}
|
||||
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
|
||||
return httpServer.ListenAndServeTLS(certificateFile, certificateKeyFile)
|
||||
certMgr, err = newCertManager(certificateFile, certificateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
}
|
||||
httpServer.TLSConfig = config
|
||||
return httpServer.ListenAndServeTLS("", "")
|
||||
}
|
||||
return httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths
|
||||
func ReloadTLSCertificate() {
|
||||
if certMgr != nil {
|
||||
certMgr.loadCertificate()
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPath(name, configDir string) string {
|
||||
if len(name) > 0 && !filepath.IsAbs(name) {
|
||||
return filepath.Join(configDir, name)
|
||||
|
|
|
@ -53,6 +53,29 @@ const (
|
|||
webUserPath = "/web/user"
|
||||
webConnectionsPath = "/web/connections"
|
||||
configDir = ".."
|
||||
httpsCert = `-----BEGIN CERTIFICATE-----
|
||||
MIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw
|
||||
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
|
||||
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw
|
||||
OTUzMDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD
|
||||
VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVqWvrJ51t5OxV0v25NsOgR82CA
|
||||
NXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIVCzgWkxiz7XE4lgUwX44FCXZM
|
||||
3+JeUbKjUzBRMB0GA1UdDgQWBBRhLw+/o3+Z02MI/d4tmaMui9W16jAfBgNVHSME
|
||||
GDAWgBRhLw+/o3+Z02MI/d4tmaMui9W16jAPBgNVHRMBAf8EBTADAQH/MAoGCCqG
|
||||
SM49BAMCA2kAMGYCMQDqLt2lm8mE+tGgtjDmtFgdOcI72HSbRQ74D5rYTzgST1rY
|
||||
/8wTi5xl8TiFUyLMUsICMQC5ViVxdXbhuG7gX6yEqSkMKZICHpO8hqFwOD/uaFVI
|
||||
dV4vKmHUzwK/eIx+8Ay3neE=
|
||||
-----END CERTIFICATE-----`
|
||||
httpsKey = `-----BEGIN EC PARAMETERS-----
|
||||
BgUrgQQAIg==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDCfMNsN6miEE3rVyUPwElfiJSWaR5huPCzUenZOfJT04GAcQdWvEju3
|
||||
UM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq
|
||||
WvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV
|
||||
CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
|
||||
-----END EC PRIVATE KEY-----`
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -93,14 +116,28 @@ func TestMain(m *testing.M) {
|
|||
httpd.SetDataProvider(dataProvider)
|
||||
|
||||
go func() {
|
||||
go func() {
|
||||
if err := httpdConf.Initialize(configDir); err != nil {
|
||||
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
||||
}
|
||||
}()
|
||||
if err := httpdConf.Initialize(configDir); err != nil {
|
||||
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
|
||||
// now start an https server
|
||||
certPath := filepath.Join(os.TempDir(), "test.crt")
|
||||
keyPath := filepath.Join(os.TempDir(), "test.key")
|
||||
ioutil.WriteFile(certPath, []byte(httpsCert), 0666)
|
||||
ioutil.WriteFile(keyPath, []byte(httpsKey), 0666)
|
||||
httpdConf.BindPort = 8443
|
||||
httpdConf.CertificateFile = certPath
|
||||
httpdConf.CertificateKeyFile = keyPath
|
||||
|
||||
go func() {
|
||||
if err := httpdConf.Initialize(configDir); err != nil {
|
||||
logger.Error(logSender, "", "could not start HTTPS server: %v", err)
|
||||
}
|
||||
}()
|
||||
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
|
||||
httpd.ReloadTLSCertificate()
|
||||
|
||||
testServer = httptest.NewServer(httpd.GetHTTPRouter())
|
||||
defer testServer.Close()
|
||||
|
@ -109,6 +146,8 @@ func TestMain(m *testing.M) {
|
|||
os.Remove(logfilePath)
|
||||
os.RemoveAll(backupsPath)
|
||||
os.RemoveAll(credentialsPath)
|
||||
os.Remove(certPath)
|
||||
os.Remove(keyPath)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
|
@ -932,6 +971,17 @@ func TestLoaddataMode(t *testing.T) {
|
|||
os.Remove(backupFilePath)
|
||||
}
|
||||
|
||||
func TestHTTPSConnection(t *testing.T) {
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
_, err := client.Get("https://localhost:8443" + metricsPath)
|
||||
if err == nil || (!strings.Contains(err.Error(), "certificate is not valid") &&
|
||||
!strings.Contains(err.Error(), "certificate signed by unknown authority")) {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// test using mock http server
|
||||
|
||||
func TestBasicUserHandlingMock(t *testing.T) {
|
||||
|
|
50
httpd/tlsutils.go
Normal file
50
httpd/tlsutils.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
type certManager struct {
|
||||
cert *tls.Certificate
|
||||
certPath string
|
||||
keyPath string
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *certManager) loadCertificate() error {
|
||||
newCert, err := tls.LoadX509KeyPair(m.certPath, m.keyPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "unable to load https certificate: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Debug(logSender, "", "https certificate successfully loaded")
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.cert = &newCert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *certManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
return m.cert, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newCertManager(certificateFile, certificateKeyFile string) (*certManager, error) {
|
||||
manager := &certManager{
|
||||
cert: nil,
|
||||
certPath: certificateFile,
|
||||
keyPath: certificateKeyFile,
|
||||
lock: new(sync.RWMutex),
|
||||
}
|
||||
err := manager.loadCertificate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return manager, nil
|
||||
}
|
|
@ -114,14 +114,14 @@ func (s *Service) Start() error {
|
|||
logger.DebugToConsole("HTTP server not started, disabled in config file")
|
||||
}
|
||||
}
|
||||
if s.PortableMode != 1 {
|
||||
registerSigHup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks until the service exits
|
||||
func (s *Service) Wait() {
|
||||
if s.PortableMode != 1 {
|
||||
registerSigHup()
|
||||
}
|
||||
<-s.Shutdown
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
|
@ -83,6 +84,7 @@ loop:
|
|||
case svc.ParamChange:
|
||||
logger.Debug(logSender, "", "Received reload request")
|
||||
dataprovider.ReloadConfig()
|
||||
httpd.ReloadTLSCertificate()
|
||||
default:
|
||||
continue loop
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"syscall"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
|
@ -18,6 +19,7 @@ func registerSigHup() {
|
|||
for range sig {
|
||||
logger.Debug(logSender, "", "Received reload request")
|
||||
dataprovider.ReloadConfig()
|
||||
httpd.ReloadTLSCertificate()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue