Skip to content

[installer, gitpod-db] Introduce database.ssl.ca #15320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -48,6 +48,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 @@ -64,13 +88,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