From acf283a927a1461542df99dcc06479f26468c384 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 15 Sep 2022 15:53:38 +0000 Subject: [PATCH] [installer, gitpod-db] Introduce database.ssl.ca --- components/gitpod-db/src/config.ts | 19 +++++++- components/gitpod-db/src/wait-for-db.ts | 2 +- components/usage/pkg/db/conn.go | 32 +++++++++++--- components/usage/pkg/server/server.go | 9 ++-- install/installer/pkg/common/common.go | 11 +++++ install/installer/pkg/common/constants.go | 4 ++ .../pkg/components/database/init/constants.go | 11 ++--- .../pkg/components/database/init/job.go | 44 ++++++++++++++----- install/installer/pkg/config/v1/config.go | 5 +++ 9 files changed, 107 insertions(+), 30 deletions(-) diff --git a/components/gitpod-db/src/config.ts b/components/gitpod-db/src/config.ts index f8d08ba3b2245e..2f7158f98c96d0 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_CUSTOM_CA_CERT) { + dbSetup.ssl = { + ca: process.env.DB_CUSTOM_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/usage/pkg/db/conn.go b/components/usage/pkg/db/conn.go index 08db84eb8474d8..fb8202e2ba1734 100644 --- a/components/usage/pkg/db/conn.go +++ b/components/usage/pkg/db/conn.go @@ -5,27 +5,31 @@ package db import ( + "crypto/tls" + "crypto/x509" "fmt" + "time" + "github.com/gitpod-io/gitpod/common-go/log" driver_mysql "github.com/go-sql-driver/mysql" "github.com/sirupsen/logrus" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" - "time" ) type ConnectionParams struct { - User string - Password string - Host string - Database string + User string + Password string + Host string + Database string + CustomCACert 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, @@ -38,6 +42,22 @@ func Connect(p ConnectionParams) (*gorm.DB, error) { ParseTime: true, } + if p.CustomCACert != "" { + rootCertPool := x509.NewCertPool() + if ok := rootCertPool.AppendCertsFromPEM([]byte(p.CustomCACert)); !ok { + log.Fatal("Failed to append custom DB CA cert.") + } + + tlsConfigName := "custom" + err = driver_mysql.RegisterTLSConfig(tlsConfigName, &tls.Config{ + RootCAs: rootCertPool, + }) + 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/usage/pkg/server/server.go b/components/usage/pkg/server/server.go index f295b9c0c547ed..12a575ba00ebf8 100644 --- a/components/usage/pkg/server/server.go +++ b/components/usage/pkg/server/server.go @@ -44,10 +44,11 @@ func Start(cfg Config, version string) error { log.WithField("config", cfg).Info("Starting usage component.") conn, err := db.Connect(db.ConnectionParams{ - User: os.Getenv("DB_USERNAME"), - Password: os.Getenv("DB_PASSWORD"), - Host: net.JoinHostPort(os.Getenv("DB_HOST"), os.Getenv("DB_PORT")), - Database: "gitpod", + User: os.Getenv("DB_USERNAME"), + Password: os.Getenv("DB_PASSWORD"), + Host: net.JoinHostPort(os.Getenv("DB_HOST"), os.Getenv("DB_PORT")), + Database: "gitpod", + CustomCACert: os.Getenv("DB_CUSTOM_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 fd50ed84968b0a..67d6b7768ed6b0 100644 --- a/install/installer/pkg/common/common.go +++ b/install/installer/pkg/common/common.go @@ -309,6 +309,17 @@ func DatabaseEnv(cfg *config.Config) (res []corev1.EnvVar) { }, ) + if cfg.Database.SSL != nil && cfg.Database.SSL.CustomCA != nil { + secretRef = corev1.LocalObjectReference{Name: cfg.Database.SSL.CustomCA.Name} + envvars = append(envvars, corev1.EnvVar{ + Name: DBCustomCaCertEnvVarName, + ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: secretRef, + Key: DBCustomCaFileName, + }}, + }) + } + return envvars } diff --git a/install/installer/pkg/common/constants.go b/install/installer/pkg/common/constants.go index ac56fbf4f8ff2a..1dfb76711f5298 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 + DBCustomCaCertEnvVarName = "DB_CUSTOM_CA_CERT" + DBCustomCaFileName = "ca.crt" + DBCustomCaBasePath = "/" + DBCustomCaPath = DBCustomCaBasePath + DBCustomCaFileName 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..581c8afd2461db 100644 --- a/install/installer/pkg/components/database/init/constants.go +++ b/install/installer/pkg/components/database/init/constants.go @@ -5,9 +5,10 @@ package init const ( - Component = "dbinit" - dbSessionsImage = "library/mysql" - dbSessionsTag = "5.7.34" - initScriptDir = "files" - sqlInitScripts = "db-init-scripts" + Component = "dbinit" + dbSessionsImage = "library/mysql" + dbSessionsTag = "5.7.34" + initScriptDir = "files" + sqlInitScripts = "db-init-scripts" + customCaMountName = "db-custom-ca" ) diff --git a/install/installer/pkg/components/database/init/job.go b/install/installer/pkg/components/database/init/job.go index f242a0f240bbdd..3b930e37e9ad5a 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.DBCustomCaCertEnvVarName, 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.CustomCA != nil { + volumes = append(volumes, corev1.Volume{ + Name: customCaMountName, + VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{ + SecretName: ctx.Config.Database.SSL.CustomCA.Name, + }}, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: customCaMountName, + MountPath: common.DBCustomCaBasePath, + ReadOnly: true, + }) + sslOptions = fmt.Sprintf(" --ssl-mode=VERIFY_IDENTITY --ssl-ca=%s ", common.DBCustomCaPath) + } + 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 51d99af1770322..015339ee436f4d 100644 --- a/install/installer/pkg/config/v1/config.go +++ b/install/installer/pkg/config/v1/config.go @@ -219,6 +219,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 { @@ -230,6 +231,10 @@ type DatabaseCloudSQL struct { Instance string `json:"instance" validate:"required"` } +type SSLOptions struct { + CustomCA *ObjectRef `json:"customCa,omitempty"` +} + type ObjectStorage struct { InCluster *bool `json:"inCluster,omitempty"` S3 *ObjectStorageS3 `json:"s3,omitempty"`