Skip to content

Commit

Permalink
[installer, gitpod-db] Introduce database.ssl.ca
Browse files Browse the repository at this point in the history
  • Loading branch information
geropl authored and Simon Emms committed Dec 15, 2022
1 parent 5372a5a commit 3c79f0c
Show file tree
Hide file tree
Showing 12 changed files with 122 additions and 18 deletions.
22 changes: 21 additions & 1 deletion components/gitpod-db/go/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package db

import (
"crypto/tls"
"crypto/x509"
"fmt"
"time"

Expand All @@ -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,
Expand All @@ -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{
Expand Down
19 changes: 17 additions & 2 deletions components/gitpod-db/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,40 @@ 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",
password: process.env.DB_PASSWORD || "test",
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;
}

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 {
Expand All @@ -48,4 +60,7 @@ export interface DatabaseConfig {
database?: string;
username?: string;
password?: string;
ssl?: {
ca?: string;
};
}
2 changes: 1 addition & 1 deletion components/gitpod-db/src/wait-for-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
1 change: 1 addition & 0 deletions components/public-api-server/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 23 additions & 2 deletions components/service-waiter/cmd/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package cmd

import (
"crypto/tls"
"crypto/x509"
"database/sql"
"net"
"os"
Expand All @@ -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 {
Expand All @@ -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() {
Expand Down Expand Up @@ -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")
}
1 change: 1 addition & 0 deletions components/usage/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions install/installer/pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions install/installer/pkg/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ const (
dbSessionsTag = "5.7.34"
initScriptDir = "files"
sqlInitScripts = "db-init-scripts"
caCertMountName = "db-ca-cert"
)
44 changes: 32 additions & 12 deletions install/installer/pkg/components/database/init/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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{{
Expand All @@ -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,
}},
},
},
Expand Down
5 changes: 5 additions & 0 deletions install/installer/pkg/config/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions install/installer/pkg/config/v1/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 3c79f0c

Please sign in to comment.