Server: Reload TLS certs without a server restart (#83589)

* server: reload of grafana server certs when renewed without restart.

Signed-off-by: Rao, B V Chalapathi <b_v_chalapathi.rao@nokia.com>

* server: reload of grafana server certs when renewed without restart.

Signed-off-by: Rao, B V Chalapathi <b_v_chalapathi.rao@nokia.com>

* Update http_server.go

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>

* Update http_server.go

Address the comments

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Dan Cech <dan@aussiedan.com>

* Update http_server.go

Align the spaces

* Update http_server.go

* Update http_server.go

* Update pkg/api/http_server.go

Co-authored-by: Dan Cech <dan@aussiedan.com>

---------

Signed-off-by: Rao, B V Chalapathi <b_v_chalapathi.rao@nokia.com>
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
Co-authored-by: Dan Cech <dan@aussiedan.com>
This commit is contained in:
chalapat
2024-03-22 20:43:22 +05:30
committed by GitHub
parent 658183d792
commit 65c0669f01
7 changed files with 213 additions and 100 deletions

View File

@@ -66,6 +66,9 @@ enable_gzip = false
cert_file =
cert_key =
# Certificates file watch interval
certs_watch_interval =
# Unix socket gid
# Changing the gid of a file without privileges requires that the target group is in the group of the process and that the process is the file owner
# It is recommended to set the gid as http server user gid

View File

@@ -67,6 +67,9 @@
;cert_file =
;cert_key =
# Certificates file watch interval
;certs_watch_interval =
# Unix socket gid
# Changing the gid of a file without privileges requires that the target group is in the group of the process and that the process is the file owner
# It is recommended to set the gid as http server user gid

View File

@@ -152,6 +152,7 @@ Content-Type: application/json
**Example Request**:
```http
GET /api/admin/usage-report-preview
Accept: application/json
Content-Type: application/json
```

View File

@@ -272,6 +272,17 @@ Path to the certificate file (if `protocol` is set to `https` or `h2`).
Path to the certificate key file (if `protocol` is set to `https` or `h2`).
### certs_watch_interval
Controls whether `cert_key` and `cert_file` are periodically watched for changes.
Disabled, by default. When enabled, `cert_key` and `cert_file`
are watched for changes. If there is change, the new certificates are loaded automatically.
{{% admonition type="warning" %}}
After the new certificates are loaded, connections with old certificates
will not work. You must reload the connections to the old certs for them to work.
{{% /admonition %}}
### socket_gid
GID where the socket should be set when `protocol=socket`.

View File

@@ -215,6 +215,14 @@ type HTTPServer struct {
namespacer request.NamespaceMapper
anonService anonymous.Service
userVerifier user.Verifier
tlsCerts TLSCerts
}
type TLSCerts struct {
certLock sync.RWMutex
certMtime time.Time
keyMtime time.Time
certs *tls.Certificate
}
type ServerOptions struct {
@@ -395,13 +403,18 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
ReadTimeout: hs.Cfg.ReadTimeout,
}
switch hs.Cfg.Protocol {
case setting.HTTP2Scheme:
if err := hs.configureHttp2(); err != nil {
case setting.HTTP2Scheme, setting.HTTPSScheme:
if err := hs.configureTLS(); err != nil {
return err
}
case setting.HTTPSScheme:
if err := hs.configureHttps(); err != nil {
return err
if hs.Cfg.CertFile != "" && hs.Cfg.KeyFile != "" {
if hs.Cfg.CertWatchInterval > 0 {
hs.httpSrv.TLSConfig.GetCertificate = hs.GetCertificate
go hs.WatchAndUpdateCerts(ctx)
hs.log.Debug("HTTP Server certificates reload feature is enabled")
} else {
hs.log.Debug("HTTP Server certificates reload feature is NOT enabled")
}
}
default:
}
@@ -549,84 +562,16 @@ func (hs *HTTPServer) tlsCertificates() ([]tls.Certificate, error) {
return hs.selfSignedCert()
}
if hs.Cfg.CertFile == "" {
return nil, errors.New("cert_file cannot be empty when using HTTPS")
}
if hs.Cfg.KeyFile == "" {
return nil, errors.New("cert_key cannot be empty when using HTTPS")
}
if _, err := os.Stat(hs.Cfg.CertFile); os.IsNotExist(err) {
return nil, fmt.Errorf(`cannot find SSL cert_file at %q`, hs.Cfg.CertFile)
}
if _, err := os.Stat(hs.Cfg.KeyFile); os.IsNotExist(err) {
return nil, fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
}
tlsCert, err := tls.LoadX509KeyPair(hs.Cfg.CertFile, hs.Cfg.KeyFile)
tlsCert, err := hs.readCertificates()
if err != nil {
return nil, fmt.Errorf("could not load SSL certificate: %w", err)
return nil, err
}
hs.tlsCerts.certs = tlsCert
return []tls.Certificate{tlsCert}, nil
}
func (hs *HTTPServer) configureHttps() error {
tlsCerts, err := hs.tlsCertificates()
if err != nil {
return err
if err := hs.updateMtimeOfServerCerts(); err != nil {
return nil, err
}
minTlsVersion, err := util.TlsNameToVersion(hs.Cfg.MinTLSVersion)
if err != nil {
return err
}
tlsCiphers := hs.getDefaultCiphers(minTlsVersion, string(setting.HTTPSScheme))
hs.log.Info("HTTP Server TLS settings", "Min TLS Version", hs.Cfg.MinTLSVersion,
"configured ciphers", util.TlsCipherIdsToString(tlsCiphers))
tlsCfg := &tls.Config{
Certificates: tlsCerts,
MinVersion: minTlsVersion,
CipherSuites: tlsCiphers,
}
hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
return nil
}
func (hs *HTTPServer) configureHttp2() error {
tlsCerts, err := hs.tlsCertificates()
if err != nil {
return err
}
minTlsVersion, err := util.TlsNameToVersion(hs.Cfg.MinTLSVersion)
if err != nil {
return err
}
tlsCiphers := hs.getDefaultCiphers(minTlsVersion, string(setting.HTTP2Scheme))
hs.log.Info("HTTP Server TLS settings", "Min TLS Version", hs.Cfg.MinTLSVersion,
"configured ciphers", util.TlsCipherIdsToString(tlsCiphers))
tlsCfg := &tls.Config{
Certificates: tlsCerts,
MinVersion: minTlsVersion,
CipherSuites: tlsCiphers,
NextProtos: []string{"h2", "http/1.1"},
}
hs.httpSrv.TLSConfig = tlsCfg
return nil
return []tls.Certificate{*tlsCert}, nil
}
func (hs *HTTPServer) applyRoutes() {
@@ -839,3 +784,141 @@ func (hs *HTTPServer) getDefaultCiphers(tlsVersion uint16, protocol string) []ui
}
return nil
}
func (hs *HTTPServer) readCertificates() (*tls.Certificate, error) {
if hs.Cfg.CertFile == "" {
return nil, errors.New("cert_file cannot be empty when using HTTPS")
}
if hs.Cfg.KeyFile == "" {
return nil, errors.New("cert_key cannot be empty when using HTTPS")
}
if _, err := os.Stat(hs.Cfg.CertFile); os.IsNotExist(err) {
return nil, fmt.Errorf(`cannot find SSL cert_file at %q`, hs.Cfg.CertFile)
}
if _, err := os.Stat(hs.Cfg.KeyFile); os.IsNotExist(err) {
return nil, fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
}
tlsCert, err := tls.LoadX509KeyPair(hs.Cfg.CertFile, hs.Cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("could not load SSL certificate: %w", err)
}
return &tlsCert, nil
}
func (hs *HTTPServer) configureTLS() error {
tlsCerts, err := hs.tlsCertificates()
if err != nil {
return err
}
minTlsVersion, err := util.TlsNameToVersion(hs.Cfg.MinTLSVersion)
if err != nil {
return err
}
tlsCiphers := hs.getDefaultCiphers(minTlsVersion, string(hs.Cfg.Protocol))
hs.log.Info("HTTP Server TLS settings", "scheme", hs.Cfg.Protocol, "Min TLS Version", hs.Cfg.MinTLSVersion,
"configured ciphers", util.TlsCipherIdsToString(tlsCiphers))
tlsCfg := &tls.Config{
Certificates: tlsCerts,
MinVersion: minTlsVersion,
CipherSuites: tlsCiphers,
}
hs.httpSrv.TLSConfig = tlsCfg
if hs.Cfg.Protocol == setting.HTTP2Scheme {
hs.httpSrv.TLSConfig.NextProtos = []string{"h2", "http/1.1"}
}
if hs.Cfg.Protocol == setting.HTTPSScheme {
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
}
return nil
}
func (hs *HTTPServer) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
hs.tlsCerts.certLock.RLock()
defer hs.tlsCerts.certLock.RUnlock()
tlsCerts := hs.tlsCerts.certs
return tlsCerts, nil
}
// fsnotify module can be used to detect file changes and based on the event certs can be reloaded
// since it adds a direct dependency for the optional feature. So that is the reason periodic watching
// of cert files is chosen. If fsnotify is added as direct dependency in future, then the implementation
// can be revisited to align to fsnotify.
func (hs *HTTPServer) WatchAndUpdateCerts(ctx context.Context) {
ticker := time.NewTicker(hs.Cfg.CertWatchInterval)
for {
select {
case <-ticker.C:
if err := hs.updateCerts(); err != nil {
hs.log.Error("Not able to reload certificates", "error", err)
}
case <-ctx.Done():
hs.log.Debug("Stopping the CertWatchInterval ticker")
ticker.Stop()
return
}
}
}
func (hs *HTTPServer) updateCerts() error {
tlsInfo := &hs.tlsCerts
cMtime, err := getMtime(hs.Cfg.CertFile)
if err != nil {
return err
}
kMtime, err := getMtime(hs.Cfg.KeyFile)
if err != nil {
return err
}
if cMtime.Compare(tlsInfo.certMtime) != 0 || kMtime.Compare(tlsInfo.keyMtime) != 0 {
certs, err := hs.readCertificates()
if err != nil {
return err
}
tlsInfo.certLock.Lock()
defer tlsInfo.certLock.Unlock()
tlsInfo.certs = certs
tlsInfo.certMtime = cMtime
tlsInfo.keyMtime = kMtime
hs.log.Info("Server certificates updated", "cMtime", tlsInfo.certMtime, "kMtime", tlsInfo.keyMtime)
}
return nil
}
func getMtime(name string) (time.Time, error) {
fInfo, err := os.Stat(name)
if err != nil {
return time.Time{}, err
}
return fInfo.ModTime(), nil
}
func (hs *HTTPServer) updateMtimeOfServerCerts() error {
var err error
hs.tlsCerts.certMtime, err = getMtime(hs.Cfg.CertFile)
if err != nil {
return err
}
hs.tlsCerts.keyMtime, err = getMtime(hs.Cfg.KeyFile)
if err != nil {
return err
}
return nil
}

View File

@@ -27,3 +27,13 @@ func TestHTTPServer_MetricsBasicAuth(t *testing.T) {
assert.False(t, ts.metricsEndpointBasicAuthEnabled())
})
}
func TestHTTPServer_readCertificates(t *testing.T) {
ts := &HTTPServer{
Cfg: setting.NewCfg(),
}
t.Run("ReadCertificates should return error when cert files are not configured", func(t *testing.T) {
_, err := ts.readCertificates()
assert.NotNil(t, err)
})
}

View File

@@ -91,27 +91,28 @@ type Cfg struct {
appliedEnvOverrides []string
// HTTP Server Settings
CertFile string
KeyFile string
HTTPAddr string
HTTPPort string
Env string
AppURL string
AppSubURL string
InstanceName string
ServeFromSubPath bool
StaticRootPath string
Protocol Scheme
SocketGid int
SocketMode int
SocketPath string
RouterLogging bool
Domain string
CDNRootURL *url.URL
ReadTimeout time.Duration
EnableGzip bool
EnforceDomain bool
MinTLSVersion string
CertFile string
KeyFile string
CertWatchInterval time.Duration
HTTPAddr string
HTTPPort string
Env string
AppURL string
AppSubURL string
InstanceName string
ServeFromSubPath bool
StaticRootPath string
Protocol Scheme
SocketGid int
SocketMode int
SocketPath string
RouterLogging bool
Domain string
CDNRootURL *url.URL
ReadTimeout time.Duration
EnableGzip bool
EnforceDomain bool
MinTLSVersion string
// Security settings
SecretKey string
@@ -1837,6 +1838,7 @@ func (cfg *Cfg) readServerSettings(iniFile *ini.File) error {
cfg.AppSubURL = AppSubUrl
cfg.Protocol = HTTPScheme
cfg.ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false)
cfg.CertWatchInterval = server.Key("certs_watch_interval").MustDuration(0)
protocolStr := valueAsString(server, "protocol", "http")