From b5e32b31b14b11de7891c35c0d3f87441dc8bd6b Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:18:32 -0300 Subject: [PATCH] Grafana: Enables use of encrypted certificates with password for https (#91418) --- conf/defaults.ini | 1 + conf/sample.ini | 3 + pkg/api/http_server.go | 51 ++++++++++++++- pkg/api/http_server_test.go | 126 +++++++++++++++++++++++++++++++++++- pkg/setting/setting.go | 3 + 5 files changed, 181 insertions(+), 3 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 33cc2bdc9cd..02ff494a9f8 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -65,6 +65,7 @@ enable_gzip = false # https certs & key file cert_file = cert_key = +cert_pass = # Certificates file watch interval certs_watch_interval = diff --git a/conf/sample.ini b/conf/sample.ini index 780a2031fbc..b1c05b984b3 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -67,6 +67,9 @@ ;cert_file = ;cert_key = +# optional password to be used to decrypt key file +;cert_pass = + # Certificates file watch interval ;certs_watch_interval = diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 554d51b9099..75463d6c0d5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -109,6 +109,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" + "github.com/youmark/pkcs8" ) type HTTPServer struct { @@ -819,6 +820,10 @@ func (hs *HTTPServer) readCertificates() (*tls.Certificate, error) { return nil, fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile) } + if hs.Cfg.CertPassword != "" { + return handleEncryptedCertificates(hs.Cfg) + } + // previous implementation tlsCert, err := tls.LoadX509KeyPair(hs.Cfg.CertFile, hs.Cfg.KeyFile) if err != nil { return nil, fmt.Errorf("could not load SSL certificate: %w", err) @@ -826,6 +831,50 @@ func (hs *HTTPServer) readCertificates() (*tls.Certificate, error) { return &tlsCert, nil } +func handleEncryptedCertificates(cfg *setting.Cfg) (*tls.Certificate, error) { + certKeyFilePassword := cfg.CertPassword + certData, err := os.ReadFile(cfg.CertFile) + if err != nil { + return nil, fmt.Errorf("failed to read certificate file: %w", err) + } + + keyData, err := os.ReadFile(cfg.KeyFile) + if err != nil { + return nil, fmt.Errorf("failed to read private key file: %w", err) + } + + // handle encrypted private key + keyPemBlock, _ := pem.Decode(keyData) + + var keyBytes []byte + // Process the PKCS-encrypted PEM block. + if strings.Contains(keyPemBlock.Type, "ENCRYPTED") { + // The pkcs8 package only handles the PKCS #5 v2.0 scheme. + decrypted, err := pkcs8.ParsePKCS8PrivateKey(keyPemBlock.Bytes, []byte(certKeyFilePassword)) + if err != nil { + return nil, fmt.Errorf("error parsing PKCS8 Private key: %w", err) + } + keyBytes, err = x509.MarshalPKCS8PrivateKey(decrypted) + if err != nil { + return nil, fmt.Errorf("error marshaling PKCS8 Private key: %w", err) + } + } else { + return nil, fmt.Errorf("password provided but Private key is not encrypted or not supported") + } + + var encodedKey bytes.Buffer + err = pem.Encode(&encodedKey, &pem.Block{Type: keyPemBlock.Type, Bytes: keyBytes}) + if err != nil { + return nil, fmt.Errorf("error encoding pem file: %w", err) + } + + cert, err := tls.X509KeyPair(certData, encodedKey.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to parse X509 key pair: %w", err) + } + return &cert, nil +} + func (hs *HTTPServer) configureTLS() error { tlsCerts, err := hs.tlsCertificates() if err != nil { @@ -869,7 +918,7 @@ func (hs *HTTPServer) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, er return tlsCerts, nil } -// fsnotify module can be used to detect file changes and based on the event certs can be reloaded +// WatchAndUpdateCerts 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. diff --git a/pkg/api/http_server_test.go b/pkg/api/http_server_test.go index 6f827934577..38497520245 100644 --- a/pkg/api/http_server_test.go +++ b/pkg/api/http_server_test.go @@ -1,11 +1,12 @@ package api import ( + "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHTTPServer_MetricsBasicAuth(t *testing.T) { @@ -37,3 +38,124 @@ func TestHTTPServer_readCertificates(t *testing.T) { assert.NotNil(t, err) }) } + +func TestHTTPServer_readEncryptedCertificates(t *testing.T) { + t.Run("readCertificates should return certificate if configuration is correct", func(t *testing.T) { + cfg, cleanUpFunc := getHttpServerCfg(t) + defer cleanUpFunc() + + ts := &HTTPServer{ + Cfg: cfg, + } + + c, err := ts.readCertificates() + require.Nil(t, err) + require.NotNil(t, c) + }) + + t.Run("readCertificates should return error if the password provided is not the correct one", func(t *testing.T) { + cfg, cleanUpFunc := getHttpServerCfg(t) + defer cleanUpFunc() + // change for a wrong password - 32char for consistency + cfg.CertPassword = "somethingThatIsNotTheCorrectPass" + + ts := &HTTPServer{ + Cfg: cfg, + } + + c, err := ts.readCertificates() + require.Nil(t, c) + require.NotNil(t, err) + require.Equal(t, err.Error(), "error parsing PKCS8 Private key: pkcs8: incorrect password") + }) +} + +// returns Cfg and cleanup function for the created files +func getHttpServerCfg(t *testing.T) (*setting.Cfg, func()) { + // create cert files + cert, err := os.CreateTemp("", "certWithPass*.crt") + require.NoError(t, err) + _, err = cert.Write(certWithPass) + require.NoError(t, err) + + privateKey, err := os.CreateTemp("", "privateKey*.key") + require.NoError(t, err) + _, err = privateKey.Write(privateKeyWithPass) + require.NoError(t, err) + + cfg := setting.NewCfg() + cfg.CertPassword = password + cfg.CertFile = cert.Name() + cfg.KeyFile = privateKey.Name() + cfg.Protocol = "https" + + cleanupFunc := func() { + _ = os.Remove(cert.Name()) + _ = os.Remove(privateKey.Name()) + } + + return cfg, cleanupFunc +} + +/* +* Certificates encrypted with password used for testing. These are valid until Aug 1st 2027. +* To generate new ones, use this commands: +* +* # Generate RSA private key with a passphrase '12345678901234567890123456789012' +* sudo openssl genrsa -aes256 -passout pass:12345678901234567890123456789012 -out ./grafana_pass.key 2048 +* # Create a new Certificate Signing Request (CSR) using the private key passing passphrase '12345678901234567890123456789012' +* sudo openssl req -new -nodes -sha256 -key ./grafana_pass.key -subj '/CN=testCertWithPass/C=us' -passin pass:12345678901234567890123456789012 -out ./grafana_pass.csr +* # Sign the CSR using the private key to create a self-signed certificate valid for 365 days +* sudo openssl x509 -req -days 1095 -in ./grafana_pass.csr -signkey ./grafana_pass.key -passin pass:12345678901234567890123456789012 -out ./grafana_pass.crt + */ +var certWithPass = []byte(`-----BEGIN CERTIFICATE----- +MIIC1zCCAb8CFGUb9G3+Dl7bTJgCsV0HatdD6jnkMA0GCSqGSIb3DQEBCwUAMCgx +GTAXBgNVBAMMEHRlc3RDZXJ0V2l0aFBhc3MxCzAJBgNVBAYTAnVzMB4XDTI0MDgw +MTE4MzM0OFoXDTI3MDgwMTE4MzM0OFowKDEZMBcGA1UEAwwQdGVzdENlcnRXaXRo +UGFzczELMAkGA1UEBhMCdXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCKnZHWYZLgfpV2MqhTHxpONwQ6dUWWwAl3sQaLV2VH6e0qBhCaO4gCKQbv3KeH +4sXmdYG4fKJ+SnwGhljfW4anQjb/puVSX8E4EXwf81DBUKbUGs5GvIx6oIx2HkoO +BoKBNgsk8K/Eq4XcVUo8PfxbsJzoCyxcrjelV4UDgxpwDCTaewmiIUb+V/JvQi65 +J1EWWofghKkNwhZ0Qyh6I9O8N7ZbkEUSbATcZ32AoDhpzhbVXQkNhJJV5SSa2zaA +Bv50cni9Te4PEYq97xUkq2KaD3c+Ie1VrAAmJVCgcUylG1YeZUohyaLbY7DG/PaW +ZPu6OqKddfH1UxUG0xzRjbmJAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEZXVWWV +GdaSUuBlc9Rd6DvSQSBYzBm5zfoQlw1IQT93tI4SVD2U04RPfxUdCh6QxsssitRn +tz2x3EKFBQ3x0jYk+JHxBLdTWAhdWrhFB+beUuOUQ5++cBDTHvpyoROAg/cIz4Fg +PvdhneOlQBe7Vh1Uv4ez+H7U1MtgUAt2LYhb5hundhUpH/WCsn1mlehyhrbDBzPc +f9JeTlZbe6wyvS/26qGPSCgP0KNvltR0Cjf2AV2gjX/7+BUr9qFBRjs4+jZkIRkP +fsYk656OSlFMbYlst1ktnBrmBE7AOHdW/WRynfIFQACNkwnrnPO1u8ZRSUzVlg/2 +lzZlmPUgKBVA0kA= +-----END CERTIFICATE-----`) + +var privateKeyWithPass = []byte(`-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIpLpJYDO3y4wCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAx2HkCNR7WRCmF3QiOqhRzBIIE +0O40A8q91zh6j2bseuIMGUQNEeRSf46fUUqtucgV/KAgpQMHL0/tTfhS5GaRcBlm +vry+9Yzfy2So5/SzC6eljdLzOuKHthgn8bBlNb8Z6atmcftr1Geeaw7lXhQqfIj7 +qVWQZuU+idSPR3QqKHCpubso4ydyANxDeAuylkdHix9LZFH8oYeJZB48o1adkjVG +nrPuupH/Rm6P7oC8E5x1lMcaAt3DFUaojycXFhGl6vnaejC6oMQqJ58KkHnNrLe+ +ltwNCphH35rDGY6mS6a7xMHEfuFHS0bg1Tl5N+vspDg99lFBL92pwdHp8hsoS8Pl +jh4nzsNc0BUQOzDcxh8uHbyAbH8jC7rLs6DUxswSJEE+tDfsKtAu6dcMsbobETTQ ++OIQ0mi2uOQ0G/Fmflf6wPPnWJpWZI/ivHmK4Gmakp+ZSFCyROekO4a5K7J5KbWM +dmv9qFbm0LacQpT/XrS+m1TKNLd1udiJpXULmmWisQTxyorjw84WAvOlaVt1ilSQ +vSYSc1dOvdZO8G0PWa0EoDOIXDohAFeHy+tfBQ/gxSWj2SyC8wpFibchjT9FrMwI +S5NRUmbjHLiIBcHQYhE+ICP238H7v4JaE2LRhljWESRb5eNlD6Ybf0h8WzEjLWmz +RJMNedHnUFV/S1eph3BXUMt+3EKYcAqs+xB80Bi/QgyRBrghlolQS55p3gOyZu8w +NCJ+qsHtFJIaZHDPgD7JOvG8E5Jy8NoFf6qsqROEkVZY3AP9XdK4vx/tn8bSIijX +oTZ04nzud1TKNBaow5/AoyTlPZvToN1IUPXHhpcpvDlz4IvTTL3Owb+//eHphwhS +tbkJyFg7PWQSpL8HcX4zFizmlqhq+hVlPrddlAmR45AL3U10J2TTHyNBo1Lvy9YS +jSe3Ux+gIk30oPRzoVNOXLnACt25LljZ28usuuXTiL2EXL/E7to0z5srOSFpwcZX +0hkokKKqYwjEvGVolfEB9wSxJ9SsapFj+GrEnKdjZacm4rxmzDGaHwKOm/Rbwg2b +XCl3LKFiyJPL0rssMvv6qgelkBzbRwjctXjEa8SIR6s1nOumP2QlYHT1Di66k0+E +zAYm0FNSo2OleRR6pbbXZJXbkUDU931JnON2OPvZ7UhHM2hWfAQq5Nl2KcaqKx/C +eiRV8o8qOuXyNnckWtv7btFj8Y+MLMIt+Ee6ZWeUWQKEFUoGInPUj8KAN8w8K3Z7 +BX1JyIJD/qNV9mgKFjmhCI3m2xox5b+RO1NDsDz3S33hsPdBHJHWwBCZLquwq+mM +aSiWiFL8KCK6Fc478J6iUg7Jzd8z3TC02VhCc4p+xWTYEgQN8yUxV2rxSk9mwsWq +v/iOCp07NN9uhNbF4KIrIX010sUYIq8iI1QeiFtQgmooBUHvd3RQH5fLaa5hwozt +hmVfJ7Wl0aBpD516QC09QhQS0jqnFRr433dVRI6zFNdxw3joZPUp4MKBlJ7g0CJV +Iv0fKNJwfT7Vmmwu2M3T5O0NzNx6VkGYXei5+NaJvUwXNwUzmdBUieXyP1bHMhr9 +cobRX9pYWflHCH4n0PshBo/quh98Omy7MVcSQtP4S2kQ4uYtZV8pZj1L5K9DekK0 +Fx113Ns6T2LzzdARMN7S3qsiRveFRrz+Xm0Rtrl//KB5 +-----END ENCRYPTED PRIVATE KEY----- +`) +var password = "12345678901234567890123456789012" diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 57ab9e7e21b..ce111891dd1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -92,6 +92,7 @@ type Cfg struct { // HTTP Server Settings CertFile string KeyFile string + CertPassword string CertWatchInterval time.Duration HTTPAddr string HTTPPort string @@ -1874,11 +1875,13 @@ func (cfg *Cfg) readServerSettings(iniFile *ini.File) error { cfg.Protocol = HTTPSScheme cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() + cfg.CertPassword = server.Key("cert_pass").String() } if protocolStr == "h2" { cfg.Protocol = HTTP2Scheme cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() + cfg.CertPassword = server.Key("cert_pass").String() } if protocolStr == "socket" { cfg.Protocol = SocketScheme