Files
Gábor Farkas 8c18d06386 Postgres: Switch the datasource plugin from lib/pq to pgx (#81353)
* postgres: switch from lib/pq to pgx

* postgres: improved tls handling
2024-02-28 07:52:45 +01:00

148 lines
4.5 KiB
Go

package tls
import (
"crypto/tls"
"crypto/x509"
"errors"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
)
// we support 4 postgres tls modes:
// disable - no tls
// require - use tls
// verify-ca - use tls, verify root cert but not the hostname
// verify-full - use tls, verify root cert
// (for all the options except `disable`, you can optionally use client certificates)
var errNoRootCert = errors.New("tls: missing root certificate")
func getTLSConfigRequire(certs *Certs) (*tls.Config, error) {
// we may have a client-cert, we do not have a root-cert
// see https://www.postgresql.org/docs/12/libpq-ssl.html ,
// mode=require + provided root-cert should behave as mode=verify-ca
if certs.rootCerts != nil {
return getTLSConfigVerifyCA(certs)
}
return &tls.Config{
InsecureSkipVerify: true, // we do not verify the root cert
Certificates: certs.clientCerts,
}, nil
}
// to implement the verify-ca mode, we need to do this:
// - for the root certificate
// - verify that the certificate we receive from the server is trusted,
// meaning it relates to our root certificate
// - we DO NOT verify that the hostname of the database matches
// the hostname in the certificate
//
// the problem is, `go“ does not offer such an option.
// by default, it will verify both things.
//
// so what we do is:
// - we turn off the default-verification with `InsecureSkipVerify`
// - we implement our own verification using `VerifyConnection`
//
// extra info about this:
// - there is a rejected feature-request about this at https://github.com/golang/go/issues/21971
// - the recommended workaround is based on VerifyPeerCertificate
// - there is even example code at https://github.com/golang/go/commit/29cfb4d3c3a97b6f426d1b899234da905be699aa
// - but later the example code was changed to use VerifyConnection instead:
// https://github.com/golang/go/commit/7eb5941b95a588a23f18fa4c22fe42ff0119c311
//
// a verifyConnection example is at https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection .
//
// this is how the `pgx` library handles verify-ca:
//
// https://github.com/jackc/pgx/blob/5c63f646f820ca9696fc3515c1caf2a557d562e5/pgconn/config.go#L657-L690
// (unfortunately pgx only handles this for certificate-provided-as-path, so we cannot rely on it)
func getTLSConfigVerifyCA(certs *Certs) (*tls.Config, error) {
// we must have a root certificate
if certs.rootCerts == nil {
return nil, errNoRootCert
}
conf := tls.Config{
Certificates: certs.clientCerts,
InsecureSkipVerify: true, // we turn off the default-verification, we'll do VerifyConnection instead
VerifyConnection: func(state tls.ConnectionState) error {
// we add all the certificates to the pool, we skip the first cert.
intermediates := x509.NewCertPool()
for _, c := range state.PeerCertificates[1:] {
intermediates.AddCert(c)
}
opts := x509.VerifyOptions{
Roots: certs.rootCerts,
Intermediates: intermediates,
}
// we call `Verify()` on the first cert (that we skipped previously)
_, err := state.PeerCertificates[0].Verify(opts)
return err
},
RootCAs: certs.rootCerts,
}
return &conf, nil
}
func getTLSConfigVerifyFull(certs *Certs, serverName string) (*tls.Config, error) {
// we must have a root certificate
if certs.rootCerts == nil {
return nil, errNoRootCert
}
conf := tls.Config{
Certificates: certs.clientCerts,
ServerName: serverName,
RootCAs: certs.rootCerts,
}
return &conf, nil
}
func IsTLSEnabled(dsInfo sqleng.DataSourceInfo) bool {
mode := dsInfo.JsonData.Mode
return mode != "disable"
}
// returns `nil` if tls is disabled
func GetTLSConfig(dsInfo sqleng.DataSourceInfo, readFile ReadFileFunc, serverName string) (*tls.Config, error) {
mode := dsInfo.JsonData.Mode
// we need to special-case the no-tls-mode
if mode == "disable" {
return nil, nil
}
// for all the remaining cases we need to load
// both the root-cert if exists, and the client-cert if exists.
certBytes, err := loadCertificateBytes(dsInfo, readFile)
if err != nil {
return nil, err
}
certs, err := createCertificates(certBytes)
if err != nil {
return nil, err
}
switch mode {
// `disable` already handled
case "":
// for backward-compatibility reasons this is the same as `require`
return getTLSConfigRequire(certs)
case "require":
return getTLSConfigRequire(certs)
case "verify-ca":
return getTLSConfigVerifyCA(certs)
case "verify-full":
return getTLSConfigVerifyFull(certs, serverName)
default:
return nil, errors.New("tls: invalid mode " + mode)
}
}