diff --git a/conf/defaults.ini b/conf/defaults.ini index 568af953c47..bc778fc1587 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -138,6 +138,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 29dfa4fcb96..92bff4aa316 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 6508a24bdc2..a9a5f3437d4 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -374,6 +374,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/database_config.go b/pkg/services/sqlstore/database_config.go index 1b627d0515e..f1ada93097e 100644 --- a/pkg/services/sqlstore/database_config.go +++ b/pkg/services/sqlstore/database_config.go @@ -25,6 +25,7 @@ type DatabaseConfig struct { Pwd string Path string SslMode string + SSLSNI string CaCertPath string ClientKeyPath string ClientCertPath string @@ -101,6 +102,7 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) dbCfg.SslMode = sec.Key("ssl_mode").String() + dbCfg.SSLSNI = sec.Key("ssl_sni").String() dbCfg.CaCertPath = sec.Key("ca_cert_path").String() dbCfg.ClientKeyPath = sec.Key("client_key_path").String() dbCfg.ClientCertPath = sec.Key("client_cert_path").String() @@ -168,12 +170,16 @@ func (dbCfg *DatabaseConfig) buildConnectionString(cfg *setting.Cfg, features fe args := []any{dbCfg.User, addr.Host, addr.Port, dbCfg.Name, dbCfg.SslMode, dbCfg.ClientCertPath, dbCfg.ClientKeyPath, 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 dbCfg.SSLSNI != "" { + cnnstr += fmt.Sprintf(" sslsni=%s", dbCfg.SSLSNI) + } if dbCfg.Pwd != "" { cnnstr += fmt.Sprintf(" password=%s", dbCfg.Pwd) } diff --git a/pkg/services/sqlstore/database_config_test.go b/pkg/services/sqlstore/database_config_test.go index 942cdc23561..cd13fc3b8fa 100644 --- a/pkg/services/sqlstore/database_config_test.go +++ b/pkg/services/sqlstore/database_config_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" ) @@ -145,3 +146,78 @@ func makeDatabaseTestConfig(t *testing.T, tc databaseConfigTest) *setting.Cfg { return cfg } +func TestBuildConnectionStringPostgres(t *testing.T) { + testCases := []struct { + name string + dbCfg *DatabaseConfig + expectedConnStr string + }{ + { + name: "Postgres with sslmode disable", + dbCfg: &DatabaseConfig{ + Type: migrator.Postgres, + User: "grafana", + Pwd: "password", + Host: "127.0.0.1:5432", + Name: "grafana_test", + 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", + dbCfg: &DatabaseConfig{ + Type: migrator.Postgres, + User: "grafana", + Pwd: "password", + Host: "127.0.0.1:5432", + Name: "grafana_test", + 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", + dbCfg: &DatabaseConfig{ + Type: migrator.Postgres, + User: "grafana", + Pwd: "password", + Host: "127.0.0.1:5432", + Name: "grafana_test", + 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", + dbCfg: &DatabaseConfig{ + Type: migrator.Postgres, + User: "grafana", + Pwd: "password", + Host: "127.0.0.1:5432", + Name: "grafana_test", + 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) { + err := tc.dbCfg.buildConnectionString(&setting.Cfg{}, nil) + assert.NoError(t, err) + assert.Equal(t, tc.expectedConnStr, tc.dbCfg.ConnectionString) + }) + } +}