diff --git a/conf/defaults.ini b/conf/defaults.ini index 60e460fdf17..9ac214a1b6c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -134,6 +134,9 @@ log_queries = # For "mysql", use either "true", "false", or "skip-verify". ssl_mode = disable +# For "postregs", use either "1" to enable or "0" to disable SNI +ssl_sni = + # Database drivers may support different transaction isolation levels. # Currently, only "mysql" driver supports isolation levels. # If the value is empty - driver's default isolation level is applied. diff --git a/conf/sample.ini b/conf/sample.ini index c8280d2780f..74847806a37 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -124,6 +124,9 @@ # For "mysql", use either "true", "false", or "skip-verify". ;ssl_mode = disable +# For "postregs", use either "1" to enable or "0" to disable SNI +;ssl_sni = + # Database drivers may support different transaction isolation levels. # Currently, only "mysql" driver supports isolation levels. # If the value is empty - driver's default isolation level is applied. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 23530a42d4d..ec5e01d4fb3 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -388,6 +388,10 @@ Set to `true` to log the sql calls and execution times. For Postgres, use use any [valid libpq `sslmode`](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS), e.g.`disable`, `require`, `verify-full`, etc. For MySQL, use either `true`, `false`, or `skip-verify`. +### ssl_sni + +For Postgres, set to `0` to disable [Server Name Indication](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLSNI). This is enabled by default on SSL-enabled connections. + ### isolation_level Only the MySQL driver supports isolation levels in Grafana. In case the value is empty, the driver's default isolation level is applied. Available options are "READ-UNCOMMITTED", "READ-COMMITTED", "REPEATABLE-READ" or "SERIALIZABLE". diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 04356f23ec4..fdcf5a698ee 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -318,12 +318,16 @@ func (ss *SQLStore) buildConnectionString() (string, error) { args := []any{ss.dbCfg.User, addr.Host, addr.Port, ss.dbCfg.Name, ss.dbCfg.SslMode, ss.dbCfg.ClientCertPath, ss.dbCfg.ClientKeyPath, ss.dbCfg.CaCertPath} + for i, arg := range args { if arg == "" { args[i] = "''" } } cnnstr = fmt.Sprintf("user=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", args...) + if ss.dbCfg.SSLSNI != "" { + cnnstr += fmt.Sprintf(" sslsni=%s", ss.dbCfg.SSLSNI) + } if ss.dbCfg.Pwd != "" { cnnstr += fmt.Sprintf(" password=%s", ss.dbCfg.Pwd) } @@ -498,6 +502,7 @@ func (ss *SQLStore) readConfig() error { ss.dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) ss.dbCfg.SslMode = sec.Key("ssl_mode").String() + ss.dbCfg.SSLSNI = sec.Key("ssl_sni").String() ss.dbCfg.CaCertPath = sec.Key("ca_cert_path").String() ss.dbCfg.ClientKeyPath = sec.Key("client_key_path").String() ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String() @@ -780,6 +785,7 @@ type DatabaseConfig struct { Pwd string Path string SslMode string + SSLSNI string CaCertPath string ClientKeyPath string ClientCertPath string diff --git a/pkg/services/sqlstore/sqlstore_test.go b/pkg/services/sqlstore/sqlstore_test.go index 83154c62eef..eb2301833a6 100644 --- a/pkg/services/sqlstore/sqlstore_test.go +++ b/pkg/services/sqlstore/sqlstore_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" ) @@ -196,3 +197,81 @@ func makeSQLStoreTestConfig(t *testing.T, tc sqlStoreTest) *setting.Cfg { return cfg } + +func TestBuildConnectionStringPostgres(t *testing.T) { + testCases := []struct { + name string + SslMode string + SSLSNI string + CaCertPath string + ClientKeyPath string + ClientCertPath string + expectedConnStr string + }{ + { + name: "Postgres with sslmode disable", + SslMode: "disable", + expectedConnStr: "user=grafana host=127.0.0.1 port=5432 dbname=grafana_test sslmode=disable sslcert='' sslkey='' sslrootcert='' password=password", + }, + { + name: "Postgres with sslmode verify-ca", + SslMode: "verify-ca", + CaCertPath: "/path/to/ca_cert", + ClientKeyPath: "/path/to/client_key", + ClientCertPath: "/path/to/client_cert", + expectedConnStr: "user=grafana host=127.0.0.1 port=5432 dbname=grafana_test sslmode=verify-ca sslcert=/path/to/client_cert sslkey=/path/to/client_key sslrootcert=/path/to/ca_cert password=password", + }, + { + name: "Postgres with sslmode verify-ca without SNI", + SslMode: "verify-ca", + CaCertPath: "/path/to/ca_cert", + ClientKeyPath: "/path/to/client_key", + ClientCertPath: "/path/to/client_cert", + SSLSNI: "0", + expectedConnStr: "user=grafana host=127.0.0.1 port=5432 dbname=grafana_test sslmode=verify-ca sslcert=/path/to/client_cert sslkey=/path/to/client_key sslrootcert=/path/to/ca_cert sslsni=0 password=password", + }, + { + name: "Postgres with sslmode verify-ca with SNI", + SslMode: "verify-ca", + CaCertPath: "/path/to/ca_cert", + ClientKeyPath: "/path/to/client_key", + ClientCertPath: "/path/to/client_cert", + SSLSNI: "1", + expectedConnStr: "user=grafana host=127.0.0.1 port=5432 dbname=grafana_test sslmode=verify-ca sslcert=/path/to/client_cert sslkey=/path/to/client_key sslrootcert=/path/to/ca_cert sslsni=1 password=password", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sqlstore := &SQLStore{} + sqlstore.Cfg = setting.NewCfg() + sec, err := sqlstore.Cfg.Raw.NewSection("database") + require.NoError(t, err) + _, err = sec.NewKey("type", migrator.Postgres) + require.NoError(t, err) + _, err = sec.NewKey("host", "127.0.0.1") + require.NoError(t, err) + _, err = sec.NewKey("port", "5432") + require.NoError(t, err) + _, err = sec.NewKey("user", "grafana") + require.NoError(t, err) + _, err = sec.NewKey("name", "grafana_test") + require.NoError(t, err) + _, err = sec.NewKey("password", "password") + require.NoError(t, err) + _, err = sec.NewKey("ssl_mode", tc.SslMode) + require.NoError(t, err) + _, err = sec.NewKey("ca_cert_path", tc.CaCertPath) + require.NoError(t, err) + _, err = sec.NewKey("client_key_path", tc.ClientKeyPath) + require.NoError(t, err) + _, err = sec.NewKey("client_cert_path", tc.ClientCertPath) + require.NoError(t, err) + _, err = sec.NewKey("ssl_sni", tc.SSLSNI) + require.NoError(t, err) + connectionString, err := sqlstore.buildConnectionString() + require.NoError(t, err) + assert.Equal(t, tc.expectedConnStr, connectionString) + }) + } +}