Skip to content
Open
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
98 changes: 97 additions & 1 deletion sql-migrate/config.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)" {
Expand Down
Loading