From 3c79f0c68c9e480f0e8daf65c44a484296161786 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Tue, 13 Dec 2022 08:59:08 +0000 Subject: [PATCH] [installer, gitpod-db] Introduce database.ssl.ca --- components/gitpod-db/go/conn.go | 22 +++++++++- components/gitpod-db/src/config.ts | 19 +++++++- components/gitpod-db/src/wait-for-db.ts | 2 +- .../public-api-server/pkg/server/server.go | 1 + components/service-waiter/cmd/database.go | 25 ++++++++++- components/usage/pkg/server/server.go | 1 + install/installer/pkg/common/common.go | 11 +++++ install/installer/pkg/common/constants.go | 4 ++ .../pkg/components/database/init/constants.go | 1 + .../pkg/components/database/init/job.go | 44 ++++++++++++++----- install/installer/pkg/config/v1/config.go | 5 +++ install/installer/pkg/config/v1/validation.go | 5 +++ 12 files changed, 122 insertions(+), 18 deletions(-) diff --git a/components/gitpod-db/go/conn.go b/components/gitpod-db/go/conn.go index af04ab61d824c9..e5a1b2deb52e6a 100644 --- a/components/gitpod-db/go/conn.go +++ b/components/gitpod-db/go/conn.go @@ -5,6 +5,8 @@ package db import ( + "crypto/tls" + "crypto/x509" "fmt" "time" @@ -21,12 +23,13 @@ type ConnectionParams struct { Password string Host string Database string + CaCert string } func Connect(p ConnectionParams) (*gorm.DB, error) { loc, err := time.LoadLocation("UTC") if err != nil { - return nil, fmt.Errorf("failed to load UT location: %w", err) + return nil, fmt.Errorf("Failed to load UT location: %w", err) } cfg := driver_mysql.Config{ User: p.User, @@ -39,6 +42,23 @@ func Connect(p ConnectionParams) (*gorm.DB, error) { ParseTime: true, } + if p.CaCert != "" { + rootCertPool := x509.NewCertPool() + if ok := rootCertPool.AppendCertsFromPEM([]byte(p.CaCert)); !ok { + log.Fatal("Failed to append custom DB CA cert.") + } + + tlsConfigName := "custom" + err = driver_mysql.RegisterTLSConfig(tlsConfigName, &tls.Config{ + RootCAs: rootCertPool, + MinVersion: tls.VersionTLS12, // semgrep finding: set lower boundary to exclude insecure TLS1.0 + }) + if err != nil { + return nil, fmt.Errorf("Failed to register custom DB CA cert: %w", err) + } + cfg.TLSConfig = tlsConfigName + } + // refer to https://github.com/go-sql-driver/mysql#dsn-data-source-name for details return gorm.Open(mysql.Open(cfg.FormatDSN()), &gorm.Config{ Logger: logger.New(log.Log, logger.Config{ diff --git a/components/gitpod-db/src/config.ts b/components/gitpod-db/src/config.ts index f8d08ba3b2245e..67c465a17b2420 100644 --- a/components/gitpod-db/src/config.ts +++ b/components/gitpod-db/src/config.ts @@ -13,7 +13,7 @@ import { ConnectionConfig } from "mysql"; export class Config { get dbConfig(): DatabaseConfig { // defaults to be used only in tests - const dbSetup = { + const dbSetup: DatabaseConfig = { host: process.env.DB_HOST || "localhost", port: getEnvVarParsed("DB_PORT", Number.parseInt, "3306"), username: process.env.DB_USERNAME || "gitpod", @@ -21,6 +21,12 @@ export class Config { database: process.env.DB_NAME || "gitpod", }; + if (process.env.DB_CA_CERT) { + dbSetup.ssl = { + ca: process.env.DB_CA_CERT, + }; + } + log.info(`Using DB: ${dbSetup.host}:${dbSetup.port}/${dbSetup.database}`); return dbSetup; @@ -28,13 +34,19 @@ export class Config { get mysqlConfig(): ConnectionConfig { const dbConfig = this.dbConfig; - return { + const mysqlConfig: ConnectionConfig = { host: dbConfig.host, port: dbConfig.port, user: dbConfig.username, password: dbConfig.password, database: dbConfig.database, }; + if (dbConfig.ssl?.ca) { + mysqlConfig.ssl = { + ca: dbConfig.ssl.ca, + }; + } + return mysqlConfig; } get dbEncryptionKeys(): string { @@ -48,4 +60,7 @@ export interface DatabaseConfig { database?: string; username?: string; password?: string; + ssl?: { + ca?: string; + }; } diff --git a/components/gitpod-db/src/wait-for-db.ts b/components/gitpod-db/src/wait-for-db.ts index 84cab25aeeb061..c4074c131fcfc1 100644 --- a/components/gitpod-db/src/wait-for-db.ts +++ b/components/gitpod-db/src/wait-for-db.ts @@ -13,7 +13,7 @@ import * as mysql from "mysql"; const retryPeriod = 5000; // [ms] const totalAttempts = 30; -const connCfg = { +const connCfg: mysql.ConnectionConfig = { ...new Config().mysqlConfig, timeout: retryPeriod, }; diff --git a/components/public-api-server/pkg/server/server.go b/components/public-api-server/pkg/server/server.go index 75218c64ee3756..f7a114fb329a1e 100644 --- a/components/public-api-server/pkg/server/server.go +++ b/components/public-api-server/pkg/server/server.go @@ -49,6 +49,7 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro Password: os.Getenv("DB_PASSWORD"), Host: net.JoinHostPort(os.Getenv("DB_HOST"), os.Getenv("DB_PORT")), Database: "gitpod", + CaCert: os.Getenv("DB_CA_CERT"), }) if err != nil { return fmt.Errorf("failed to establish database connection: %w", err) diff --git a/components/service-waiter/cmd/database.go b/components/service-waiter/cmd/database.go index 8e336d95e04084..73623a93fdebbf 100644 --- a/components/service-waiter/cmd/database.go +++ b/components/service-waiter/cmd/database.go @@ -5,6 +5,8 @@ package cmd import ( + "crypto/tls" + "crypto/x509" "database/sql" "net" "os" @@ -24,7 +26,7 @@ var databaseCmd = &cobra.Command{ Short: "waits for a MySQL database to become available", Long: `Uses the default db env config of a Gitpod deployment to try and connect to a MySQL database, specifically DB_HOST, DB_PORT, DB_PASSWORD, -and DB_USER(=gitpod)`, +DB_CA_CERT and DB_USER(=gitpod)`, PreRun: func(cmd *cobra.Command, args []string) { err := viper.BindPFlags(cmd.Flags()) if err != nil { @@ -38,13 +40,31 @@ and DB_USER(=gitpod)`, cfg.User = viper.GetString("username") cfg.Passwd = viper.GetString("password") cfg.Timeout = 1 * time.Second - dsn := cfg.FormatDSN() + dsn := cfg.FormatDSN() censoredDSN := dsn if cfg.Passwd != "" { censoredDSN = strings.Replace(dsn, cfg.Passwd, "*****", -1) } + caCert := viper.GetString("caCert") + if caCert != "" { + rootCertPool := x509.NewCertPool() + if ok := rootCertPool.AppendCertsFromPEM([]byte(caCert)); !ok { + log.Fatal("Failed to append DB CA cert.") + } + + tlsConfigName := "custom" + err := mysql.RegisterTLSConfig(tlsConfigName, &tls.Config{ + RootCAs: rootCertPool, + MinVersion: tls.VersionTLS12, // semgrep finding: set lower boundary to exclude insecure TLS1.0 + }) + if err != nil { + log.WithError(err).Fatal("Failed to register DB CA cert") + } + cfg.TLSConfig = tlsConfigName + } + timeout := getTimeout() done := make(chan bool) go func() { @@ -92,4 +112,5 @@ func init() { databaseCmd.Flags().StringP("port", "p", envOrDefault("DB_PORT", "3306"), "Port to connect on") databaseCmd.Flags().StringP("password", "P", os.Getenv("DB_PASSWORD"), "Password to use when connecting") databaseCmd.Flags().StringP("username", "u", envOrDefault("DB_USERNAME", "gitpod"), "Username to use when connected") + databaseCmd.Flags().StringP("caCert", "", os.Getenv("DB_CA_CERT"), "Custom CA cert (chain) to use when connected") } diff --git a/components/usage/pkg/server/server.go b/components/usage/pkg/server/server.go index 0f8301e3c89717..630ed829c93419 100644 --- a/components/usage/pkg/server/server.go +++ b/components/usage/pkg/server/server.go @@ -55,6 +55,7 @@ func Start(cfg Config, version string) error { Password: os.Getenv("DB_PASSWORD"), Host: net.JoinHostPort(os.Getenv("DB_HOST"), os.Getenv("DB_PORT")), Database: "gitpod", + CaCert: os.Getenv("DB_CA_CERT"), }) if err != nil { return fmt.Errorf("failed to establish database connection: %w", err) diff --git a/install/installer/pkg/common/common.go b/install/installer/pkg/common/common.go index 403877a85c6516..e930c41ef63191 100644 --- a/install/installer/pkg/common/common.go +++ b/install/installer/pkg/common/common.go @@ -344,6 +344,17 @@ func DatabaseEnv(cfg *config.Config) (res []corev1.EnvVar) { }, ) + if cfg.Database.SSL != nil && cfg.Database.SSL.CaCert != nil { + secretRef = corev1.LocalObjectReference{Name: cfg.Database.SSL.CaCert.Name} + envvars = append(envvars, corev1.EnvVar{ + Name: DBCaCertEnvVarName, + ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: secretRef, + Key: DBCaFileName, + }}, + }) + } + return envvars } diff --git a/install/installer/pkg/common/constants.go b/install/installer/pkg/common/constants.go index ac56fbf4f8ff2a..eebf8657b1d845 100644 --- a/install/installer/pkg/common/constants.go +++ b/install/installer/pkg/common/constants.go @@ -47,6 +47,10 @@ const ( ImageBuilderComponent = "image-builder-mk3" ImageBuilderRPCPort = 8080 DebugNodePort = 9229 + DBCaCertEnvVarName = "DB_CA_CERT" + DBCaFileName = "ca.crt" + DBCaBasePath = "/db-ssl" + DBCaPath = DBCaBasePath + "/" + DBCaFileName AnnotationConfigChecksum = "gitpod.io/checksum_config" ) diff --git a/install/installer/pkg/components/database/init/constants.go b/install/installer/pkg/components/database/init/constants.go index 48085fbe3c8467..163b9d57101541 100644 --- a/install/installer/pkg/components/database/init/constants.go +++ b/install/installer/pkg/components/database/init/constants.go @@ -10,4 +10,5 @@ const ( dbSessionsTag = "5.7.34" initScriptDir = "files" sqlInitScripts = "db-init-scripts" + caCertMountName = "db-ca-cert" ) diff --git a/install/installer/pkg/components/database/init/job.go b/install/installer/pkg/components/database/init/job.go index f242a0f240bbdd..4e6c8906a0eb3f 100644 --- a/install/installer/pkg/components/database/init/job.go +++ b/install/installer/pkg/components/database/init/job.go @@ -31,6 +31,35 @@ func job(ctx *common.RenderContext) ([]runtime.Object, error) { Annotations: common.CustomizeAnnotation(ctx, Component, common.TypeMetaBatchJob), } + volumes := []corev1.Volume{{ + Name: sqlInitScripts, + VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: sqlInitScripts}, + }}, + }} + volumeMounts := []corev1.VolumeMount{{ + Name: sqlInitScripts, + MountPath: "/db-init-scripts", + ReadOnly: true, + }} + + // We already have CA loaded at common.DBCaCertEnvVarName, but mysql cli needs a file here, so we mount it like as one. + sslOptions := "" + if ctx.Config.Database.SSL != nil && ctx.Config.Database.SSL.CaCert != nil { + volumes = append(volumes, corev1.Volume{ + Name: caCertMountName, + VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{ + SecretName: ctx.Config.Database.SSL.CaCert.Name, + }}, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: caCertMountName, + MountPath: common.DBCaBasePath, + ReadOnly: true, + }) + sslOptions = fmt.Sprintf(" --ssl-mode=VERIFY_IDENTITY --ssl-ca=%s ", common.DBCaPath) + } + return []runtime.Object{&batchv1.Job{ TypeMeta: common.TypeMetaBatchJob, ObjectMeta: objectMeta, @@ -43,12 +72,7 @@ func job(ctx *common.RenderContext) ([]runtime.Object, error) { RestartPolicy: corev1.RestartPolicyNever, ServiceAccountName: Component, EnableServiceLinks: pointer.Bool(false), - Volumes: []corev1.Volume{{ - Name: sqlInitScripts, - VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: sqlInitScripts}, - }}, - }}, + Volumes: volumes, // The init container is designed to emulate Helm hooks InitContainers: []corev1.Container{*common.DatabaseWaiterContainer(ctx)}, Containers: []corev1.Container{{ @@ -61,13 +85,9 @@ func job(ctx *common.RenderContext) ([]runtime.Object, error) { Command: []string{ "sh", "-c", - "mysql -h $DB_HOST --port $DB_PORT -u $DB_USERNAME -p$DB_PASSWORD < /db-init-scripts/init.sql", + fmt.Sprintf("mysql -h $DB_HOST --port $DB_PORT -u $DB_USERNAME -p$DB_PASSWORD %s< /db-init-scripts/init.sql", sslOptions), }, - VolumeMounts: []corev1.VolumeMount{{ - Name: sqlInitScripts, - MountPath: "/db-init-scripts", - ReadOnly: true, - }}, + VolumeMounts: volumeMounts, }}, }, }, diff --git a/install/installer/pkg/config/v1/config.go b/install/installer/pkg/config/v1/config.go index d0b5975ff25910..767c6d23adf712 100644 --- a/install/installer/pkg/config/v1/config.go +++ b/install/installer/pkg/config/v1/config.go @@ -232,6 +232,7 @@ type Database struct { InCluster *bool `json:"inCluster,omitempty"` External *DatabaseExternal `json:"external,omitempty"` CloudSQL *DatabaseCloudSQL `json:"cloudSQL,omitempty"` + SSL *SSLOptions `json:"ssl,omitempty"` } type DatabaseExternal struct { @@ -243,6 +244,10 @@ type DatabaseCloudSQL struct { Instance string `json:"instance" validate:"required"` } +type SSLOptions struct { + CaCert *ObjectRef `json:"caCert,omitempty"` +} + type ObjectStorage struct { InCluster *bool `json:"inCluster,omitempty"` S3 *ObjectStorageS3 `json:"s3,omitempty"` diff --git a/install/installer/pkg/config/v1/validation.go b/install/installer/pkg/config/v1/validation.go index b9796061b4caa6..fa20e0c18a878f 100644 --- a/install/installer/pkg/config/v1/validation.go +++ b/install/installer/pkg/config/v1/validation.go @@ -166,6 +166,11 @@ func (v version) ClusterValidation(rcfg interface{}) cluster.ValidationChecks { res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("encryptionKeys", "host", "password", "port", "username"))) } + if cfg.Database.SSL != nil && cfg.Database.SSL.CaCert != nil { + secretName := cfg.Database.SSL.CaCert.Name + res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("ca.crt"))) + } + if cfg.License != nil { secretName := cfg.License.Name licensorKey := "type"