mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -152,6 +152,7 @@ Content-Type: application/json
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/admin/usage-report-preview
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user