diff --git a/README.md b/README.md index 65fd7f70..2c35efc7 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,38 @@ production: See [here](https://github.com/go-sql-driver/mysql#parsetime) for more information. +### MySQL TLS Certificates + +For MySQL servers that use certificates signed by a private CA, configure `mysql-ca-cert`. `sql-migrate` registers a MySQL TLS config and adds the matching `tls` option to the datasource automatically. + +```yml +production: + dialect: mysql + datasource: user:password@tcp(mysql.example.com:3306)/dbname?parseTime=true + dir: migrations/mysql + table: migrations + mysql-ca-cert: /path/ca-cert.pem + mysql-server-name: mysql.example.com + mysql-tls-config: sql-migrate +``` + +For MySQL servers that require client certificate authentication, also configure the client certificate and key: + +```yml +production: + dialect: mysql + datasource: user:password@tcp(mysql.example.com:3306)/dbname?parseTime=true + dir: migrations/mysql + table: migrations + mysql-client-cert: /path/client-cert.pem + mysql-client-key: /path/client-key.pem + mysql-ca-cert: /path/ca-cert.pem + mysql-server-name: mysql.example.com + mysql-tls-config: sql-migrate +``` + +`mysql-client-cert` and `mysql-client-key` must be provided together. `mysql-ca-cert` can be used without a client certificate for server certificate verification. Do not set `tls=` in the datasource when using these fields, because `sql-migrate` manages that option. + ### Oracle (oci8) Oracle Driver is [oci8](https://github.com/mattn/go-oci8), it is not pure Go code and relies on Oracle Office Client ([Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html)), more detailed information is in the [oci8 repo](https://github.com/mattn/go-oci8). diff --git a/sql-migrate/config.go b/sql-migrate/config.go index 6947a534..89fe2fbe 100644 --- a/sql-migrate/config.go +++ b/sql-migrate/config.go @@ -1,19 +1,23 @@ package main import ( + "crypto/tls" + "crypto/x509" "database/sql" "errors" "flag" "fmt" + "net/url" "os" "runtime/debug" + "strings" "github.com/go-gorp/gorp/v3" + "github.com/go-sql-driver/mysql" "gopkg.in/yaml.v2" migrate "github.com/rubenv/sql-migrate" - _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) @@ -41,6 +45,12 @@ type Environment struct { TableName string `yaml:"table"` SchemaName string `yaml:"schema"` IgnoreUnknown bool `yaml:"ignoreunknown"` + + MySQLClientCert string `yaml:"mysql-client-cert"` + MySQLClientKey string `yaml:"mysql-client-key"` + MySQLCACert string `yaml:"mysql-ca-cert"` + MySQLServerName string `yaml:"mysql-server-name"` + MySQLTLSConfig string `yaml:"mysql-tls-config"` } func ReadConfig() (map[string]*Environment, error) { @@ -96,6 +106,10 @@ func GetEnvironment() (*Environment, error) { } func GetConnection(env *Environment) (*sql.DB, string, error) { + if err := prepareMySQLTLS(env); err != nil { + return nil, "", fmt.Errorf("Cannot configure MySQL TLS: %w", err) + } + db, err := sql.Open(env.Dialect, env.DataSource) if err != nil { return nil, "", fmt.Errorf("Cannot connect to database: %w", err) @@ -110,6 +124,88 @@ func GetConnection(env *Environment) (*sql.DB, string, error) { return db, env.Dialect, nil } +func prepareMySQLTLS(env *Environment) error { + if env.Dialect != "mysql" || !env.hasMySQLTLSConfig() { + return nil + } + + if env.MySQLClientCert == "" && env.MySQLClientKey != "" { + return errors.New("mysql-client-cert is required when mysql-client-key is set") + } + if env.MySQLClientCert != "" && env.MySQLClientKey == "" { + return errors.New("mysql-client-key is required when mysql-client-cert is set") + } + if env.MySQLClientCert == "" && env.MySQLCACert == "" { + return errors.New("mysql-ca-cert or mysql-client-cert/mysql-client-key is required when configuring MySQL TLS") + } + if dataSourceHasTLSParam(env.DataSource) { + return errors.New("datasource tls parameter conflicts with MySQL client certificate config") + } + + cfg, err := mysql.ParseDSN(env.DataSource) + if err != nil { + return fmt.Errorf("parse MySQL datasource: %w", err) + } + + tlsConfig := &tls.Config{ + ServerName: env.MySQLServerName, + } + + if env.MySQLCACert != "" { + rootCAs := x509.NewCertPool() + caCert, err := os.ReadFile(env.MySQLCACert) + if err != nil { + return fmt.Errorf("read MySQL CA certificate %q: %w", env.MySQLCACert, err) + } + if ok := rootCAs.AppendCertsFromPEM(caCert); !ok { + return fmt.Errorf("read MySQL CA certificate %q: no certificates found", env.MySQLCACert) + } + tlsConfig.RootCAs = rootCAs + } + + if env.MySQLClientCert != "" { + cert, err := tls.LoadX509KeyPair(env.MySQLClientCert, env.MySQLClientKey) + if err != nil { + return fmt.Errorf("load MySQL client certificate %q and key %q: %w", env.MySQLClientCert, env.MySQLClientKey, err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + configName := env.MySQLTLSConfig + if configName == "" { + configName = "sql-migrate" + } + if err := mysql.RegisterTLSConfig(configName, tlsConfig); err != nil { + return fmt.Errorf("register MySQL TLS config %q: %w", configName, err) + } + + cfg.TLSConfig = configName + env.DataSource = cfg.FormatDSN() + return nil +} + +func (env *Environment) hasMySQLTLSConfig() bool { + return env.MySQLClientCert != "" || + env.MySQLClientKey != "" || + env.MySQLCACert != "" || + env.MySQLServerName != "" || + env.MySQLTLSConfig != "" +} + +func dataSourceHasTLSParam(dataSource string) bool { + questionMark := strings.Index(dataSource, "?") + if questionMark == -1 { + return false + } + + values, err := url.ParseQuery(dataSource[questionMark+1:]) + if err != nil { + return strings.Contains(dataSource[questionMark+1:], "tls=") + } + _, ok := values["tls"] + return ok +} + // GetVersion returns the version. func GetVersion() string { if buildInfo, ok := debug.ReadBuildInfo(); ok && buildInfo.Main.Version != "(devel)" { diff --git a/sql-migrate/config_test.go b/sql-migrate/config_test.go new file mode 100644 index 00000000..5eda49c4 --- /dev/null +++ b/sql-migrate/config_test.go @@ -0,0 +1,327 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "time" + + "github.com/go-sql-driver/mysql" + //revive:disable-next-line:dot-imports + . "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type ConfigSuite struct{} + +var _ = Suite(&ConfigSuite{}) + +func (*ConfigSuite) TestEnvironmentParsesMySQLTLSFields(c *C) { + config := map[string]*Environment{} + err := yaml.Unmarshal([]byte(` +development: + dialect: mysql + datasource: user:password@tcp(localhost:3306)/dbname?parseTime=true + dir: migrations + mysql-client-cert: /certs/client-cert.pem + mysql-client-key: /certs/client-key.pem + mysql-ca-cert: /certs/ca.pem + mysql-server-name: mysql.example.com + mysql-tls-config: sql-migrate-test +`), &config) + c.Assert(err, IsNil) + + env := config["development"] + c.Assert(env.MySQLClientCert, Equals, "/certs/client-cert.pem") + c.Assert(env.MySQLClientKey, Equals, "/certs/client-key.pem") + c.Assert(env.MySQLCACert, Equals, "/certs/ca.pem") + c.Assert(env.MySQLServerName, Equals, "mysql.example.com") + c.Assert(env.MySQLTLSConfig, Equals, "sql-migrate-test") +} + +func (*ConfigSuite) TestPrepareMySQLTLSRegistersConfigAndPreservesDatasource(c *C) { + dir := c.MkDir() + certFile, keyFile, caFile := writeTestCertificates(c, dir) + configName := "sql-migrate-test-valid" + defer mysql.DeregisterTLSConfig(configName) + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true&timeout=5s", + MySQLClientCert: certFile, + MySQLClientKey: keyFile, + MySQLCACert: caFile, + MySQLServerName: "localhost", + MySQLTLSConfig: configName, + } + + err := prepareMySQLTLS(env) + c.Assert(err, IsNil) + + cfg, err := mysql.ParseDSN(env.DataSource) + c.Assert(err, IsNil) + c.Assert(cfg.TLSConfig, Equals, configName) + c.Assert(cfg.ParseTime, Equals, true) + c.Assert(cfg.Timeout, Equals, 5*time.Second) +} + +func (*ConfigSuite) TestPrepareMySQLTLSAllowsCAOnlyConfig(c *C) { + dir := c.MkDir() + _, _, caFile := writeTestCertificates(c, dir) + configName := "sql-migrate-test-ca-only" + defer mysql.DeregisterTLSConfig(configName) + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true&timeout=5s", + MySQLCACert: caFile, + MySQLServerName: "localhost", + MySQLTLSConfig: configName, + } + + err := prepareMySQLTLS(env) + c.Assert(err, IsNil) + + cfg, err := mysql.ParseDSN(env.DataSource) + c.Assert(err, IsNil) + c.Assert(cfg.TLSConfig, Equals, configName) + c.Assert(cfg.ParseTime, Equals, true) + c.Assert(cfg.Timeout, Equals, 5*time.Second) +} + +func (*ConfigSuite) TestPrepareMySQLTLSDefaultsConfigName(c *C) { + dir := c.MkDir() + certFile, keyFile, _ := writeTestCertificates(c, dir) + defer mysql.DeregisterTLSConfig("sql-migrate") + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLClientCert: certFile, + MySQLClientKey: keyFile, + } + + err := prepareMySQLTLS(env) + c.Assert(err, IsNil) + + cfg, err := mysql.ParseDSN(env.DataSource) + c.Assert(err, IsNil) + c.Assert(cfg.TLSConfig, Equals, "sql-migrate") + c.Assert(cfg.ParseTime, Equals, true) +} + +func (*ConfigSuite) TestPrepareMySQLTLSDefaultsConfigNameForCAOnly(c *C) { + dir := c.MkDir() + _, _, caFile := writeTestCertificates(c, dir) + defer mysql.DeregisterTLSConfig("sql-migrate") + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLCACert: caFile, + } + + err := prepareMySQLTLS(env) + c.Assert(err, IsNil) + + cfg, err := mysql.ParseDSN(env.DataSource) + c.Assert(err, IsNil) + c.Assert(cfg.TLSConfig, Equals, "sql-migrate") + c.Assert(cfg.ParseTime, Equals, true) +} + +func (*ConfigSuite) TestPrepareMySQLTLSNoFieldsLeavesDatasource(c *C) { + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + } + + err := prepareMySQLTLS(env) + c.Assert(err, IsNil) + c.Assert(env.DataSource, Equals, "user:password@tcp(localhost:3306)/dbname?parseTime=true") +} + +func (*ConfigSuite) TestPrepareMySQLTLSNonMySQLIgnoresFields(c *C) { + env := &Environment{ + Dialect: "postgres", + DataSource: "dbname=test sslmode=disable", + MySQLClientCert: "/missing/client-cert.pem", + MySQLClientKey: "/missing/client-key.pem", + MySQLCACert: "/missing/ca.pem", + MySQLServerName: "localhost", + MySQLTLSConfig: "sql-migrate-test-postgres", + } + + err := prepareMySQLTLS(env) + c.Assert(err, IsNil) + c.Assert(env.DataSource, Equals, "dbname=test sslmode=disable") +} + +func (*ConfigSuite) TestPrepareMySQLTLSRequiresClientKey(c *C) { + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLClientCert: "/certs/client-cert.pem", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*mysql-client-key.*required.*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSRequiresClientCert(c *C) { + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLClientKey: "/certs/client-key.pem", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*mysql-client-cert.*required.*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSReportsMissingCertFile(c *C) { + dir := c.MkDir() + _, keyFile, _ := writeTestCertificates(c, dir) + missingCert := filepath.Join(dir, "missing-client-cert.pem") + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLClientCert: missingCert, + MySQLClientKey: keyFile, + MySQLTLSConfig: "sql-migrate-test-missing-cert", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*"+missingCert+".*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSReportsMissingCAFile(c *C) { + missingCA := filepath.Join(c.MkDir(), "missing-ca.pem") + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLCACert: missingCA, + MySQLTLSConfig: "sql-migrate-test-missing-ca", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*"+missingCA+".*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSReportsInvalidCertPair(c *C) { + dir := c.MkDir() + certFile := filepath.Join(dir, "client-cert.pem") + keyFile := filepath.Join(dir, "client-key.pem") + c.Assert(os.WriteFile(certFile, []byte("not a cert"), 0644), IsNil) + c.Assert(os.WriteFile(keyFile, []byte("not a key"), 0644), IsNil) + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLClientCert: certFile, + MySQLClientKey: keyFile, + MySQLTLSConfig: "sql-migrate-test-invalid-pair", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*client certificate.*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSRejectsDatasourceTLSConflict(c *C) { + dir := c.MkDir() + certFile, keyFile, _ := writeTestCertificates(c, dir) + + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true&tls=skip-verify", + MySQLClientCert: certFile, + MySQLClientKey: keyFile, + MySQLTLSConfig: "sql-migrate-test-conflict", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*datasource.*tls.*conflict.*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSRejectsConfigNameWithoutTLSMaterial(c *C) { + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLTLSConfig: "sql-migrate-test-no-material", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*mysql-ca-cert.*mysql-client-cert.*required.*") +} + +func (*ConfigSuite) TestPrepareMySQLTLSRejectsServerNameWithoutTLSMaterial(c *C) { + env := &Environment{ + Dialect: "mysql", + DataSource: "user:password@tcp(localhost:3306)/dbname?parseTime=true", + MySQLServerName: "localhost", + } + + err := prepareMySQLTLS(env) + c.Assert(err, ErrorMatches, ".*mysql-ca-cert.*mysql-client-cert.*required.*") +} + +func writeTestCertificates(c *C, dir string) (string, string, string) { + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + c.Assert(err, IsNil) + clientKey, err := rsa.GenerateKey(rand.Reader, 2048) + c.Assert(err, IsNil) + + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "sql-migrate-test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + } + caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + c.Assert(err, IsNil) + + clientTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "sql-migrate-client"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + clientDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caTemplate, &clientKey.PublicKey, caKey) + c.Assert(err, IsNil) + + certFile := filepath.Join(dir, "client-cert.pem") + keyFile := filepath.Join(dir, "client-key.pem") + caFile := filepath.Join(dir, "ca.pem") + + writePEM(c, certFile, "CERTIFICATE", clientDER) + writePEM(c, caFile, "CERTIFICATE", caDER) + writePrivateKey(c, keyFile, clientKey) + + return certFile, keyFile, caFile +} + +func writePEM(c *C, filename, typ string, der []byte) { + file, err := os.Create(filename) + c.Assert(err, IsNil) + defer func() { _ = file.Close() }() + + err = pem.Encode(file, &pem.Block{Type: typ, Bytes: der}) + c.Assert(err, IsNil) +} + +func writePrivateKey(c *C, filename string, key *rsa.PrivateKey) { + der := x509.MarshalPKCS1PrivateKey(key) + writePEM(c, filename, "RSA PRIVATE KEY", der) +} diff --git a/sql-migrate/main_test.go b/sql-migrate/main_test.go index 06ab7d0f..7e7907cc 100644 --- a/sql-migrate/main_test.go +++ b/sql-migrate/main_test.go @@ -1 +1,10 @@ package main + +import ( + "testing" + + //revive:disable-next-line:dot-imports + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } diff --git a/test-integration/mysql-tls-setup.sh b/test-integration/mysql-tls-setup.sh new file mode 100755 index 00000000..5b637e6e --- /dev/null +++ b/test-integration/mysql-tls-setup.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# Tweak PATH for Travis +export PATH=$PATH:$HOME/gopath/bin + +set -e + +export MYSQL_TLS_WORKDIR=${MYSQL_TLS_WORKDIR:-/tmp/sql-migrate-mtls-test} +export CERT_DIR=${CERT_DIR:-$MYSQL_TLS_WORKDIR/certs} +export MYSQL_CONTAINER=${MYSQL_CONTAINER:-sql-migrate-mtls-mysql} +export MYSQL_IMAGE=${MYSQL_IMAGE:-mysql:8.0} +export MYSQL_PORT=${MYSQL_PORT:-13306} +export MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-rootpass} + +export MYSQL_CA_USER=${MYSQL_CA_USER:-caonly} +export MYSQL_CA_PASSWORD=${MYSQL_CA_PASSWORD:-caonlypass} +export MYSQL_CA_DATABASE=${MYSQL_CA_DATABASE:-test_caonly} + +export MYSQL_X509_USER=${MYSQL_X509_USER:-migrate} +export MYSQL_X509_PASSWORD=${MYSQL_X509_PASSWORD:-migratepass} +export MYSQL_X509_DATABASE=${MYSQL_X509_DATABASE:-test_mtls} + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +log() { + printf '\n==> %s\n' "$*" >&2 +} + +generate_certs() { + if [ -f "$CERT_DIR/ca.pem" ] && + [ -f "$CERT_DIR/server-cert.pem" ] && + [ -f "$CERT_DIR/server-key.pem" ] && + [ -f "$CERT_DIR/client-cert.pem" ] && + [ -f "$CERT_DIR/client-key.pem" ]; then + log "Using existing certificates in $CERT_DIR" + return + fi + + log "Generating test certificates in $CERT_DIR" + mkdir -p "$CERT_DIR" + rm -f "$CERT_DIR"/*.pem "$CERT_DIR"/*.cnf "$CERT_DIR"/*.srl + + ( + cd "$CERT_DIR" + + openssl genrsa 2048 >ca-key.pem + openssl req -new -x509 -nodes -days 3650 \ + -key ca-key.pem \ + -subj "/CN=sql-migrate-test-ca" \ + -out ca.pem + + cat >server-ext.cnf <client-ext.cnf </dev/null 2>&1 +} + +start_mysql() { + log "Starting MySQL container $MYSQL_CONTAINER on port $MYSQL_PORT" + docker rm -f "$MYSQL_CONTAINER" >/dev/null 2>&1 || true + + docker run --name "$MYSQL_CONTAINER" --rm -d \ + -e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" \ + -p "$MYSQL_PORT:3306" \ + -v "$CERT_DIR:/etc/mysql/certs:ro" \ + "$MYSQL_IMAGE" \ + --ssl-ca=/etc/mysql/certs/ca.pem \ + --ssl-cert=/etc/mysql/certs/server-cert.pem \ + --ssl-key=/etc/mysql/certs/server-key.pem \ + --require_secure_transport=ON >/dev/null +} + +wait_mysql() { + log "Waiting for MySQL to become ready" + + for _ in {1..60}; do + if root_auth_succeeds; then + return + fi + + sleep 2 + done + + docker logs "$MYSQL_CONTAINER" >&2 || true + echo "MySQL did not become ready in time." >&2 + exit 1 +} + +configure_users() { + log "Resetting TLS test users and databases" + + docker exec "$MYSQL_CONTAINER" mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e " +DROP DATABASE IF EXISTS ${MYSQL_CA_DATABASE}; +CREATE DATABASE ${MYSQL_CA_DATABASE}; +DROP USER IF EXISTS '${MYSQL_CA_USER}'@'%'; +CREATE USER '${MYSQL_CA_USER}'@'%' IDENTIFIED BY '${MYSQL_CA_PASSWORD}' REQUIRE SSL; +GRANT ALL PRIVILEGES ON ${MYSQL_CA_DATABASE}.* TO '${MYSQL_CA_USER}'@'%'; + +DROP DATABASE IF EXISTS ${MYSQL_X509_DATABASE}; +CREATE DATABASE ${MYSQL_X509_DATABASE}; +DROP USER IF EXISTS '${MYSQL_X509_USER}'@'%'; +CREATE USER '${MYSQL_X509_USER}'@'%' IDENTIFIED BY '${MYSQL_X509_PASSWORD}' REQUIRE X509; +GRANT ALL PRIVILEGES ON ${MYSQL_X509_DATABASE}.* TO '${MYSQL_X509_USER}'@'%'; + +FLUSH PRIVILEGES; +" +} + +show_setup() { + log "Checking MySQL TLS setup" + + docker exec "$MYSQL_CONTAINER" mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e " +SHOW VARIABLES LIKE 'have_ssl'; +SHOW VARIABLES LIKE 'require_secure_transport'; +SELECT user, host, ssl_type FROM mysql.user WHERE user IN ('${MYSQL_CA_USER}', '${MYSQL_X509_USER}'); +" + + cat <&2 + echo "Run test-integration/mysql-tls-setup.sh to create the default Docker TLS environment." >&2 + exit 1 +fi + +if [ -n "$MYSQL_CLIENT_CERT" ] && [ -z "$MYSQL_CLIENT_KEY" ]; then + echo "MYSQL_CLIENT_KEY is required when MYSQL_CLIENT_CERT is set" >&2 + exit 1 +fi + +if [ -n "$MYSQL_CLIENT_KEY" ] && [ -z "$MYSQL_CLIENT_CERT" ]; then + echo "MYSQL_CLIENT_CERT is required when MYSQL_CLIENT_KEY is set" >&2 + exit 1 +fi + +cat >"$MYSQL_TLS_CONFIG_FILE" <>"$MYSQL_TLS_CONFIG_FILE" <