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

@@ -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
}