Merge pull request #705 from brandonbodnar-wk/add-tls-support
Initial SSL Connection Support
This commit is contained in:
commit
4ff5d6a055
@ -177,6 +177,18 @@ By default `gh-ost` verifies no foreign keys exist on the migrated table. On ser
|
|||||||
|
|
||||||
See [`approve-renamed-columns`](#approve-renamed-columns)
|
See [`approve-renamed-columns`](#approve-renamed-columns)
|
||||||
|
|
||||||
|
### ssl
|
||||||
|
|
||||||
|
By default `gh-ost` does not use ssl/tls connections to the database servers when performing migrations. This flag instructs `gh-ost` to use encrypted connections. If enabled, `gh-ost` will use the system's ca certificate pool for server certificate verification. If a different certificate is needed for server verification, see `--ssl-ca`. If you wish to skip server verification, but still use encrypted connections, use with `--ssl-allow-insecure`.
|
||||||
|
|
||||||
|
### ssl-allow-insecure
|
||||||
|
|
||||||
|
Allows `gh-ost` to connect to the MySQL servers using encrypted connections, but without verifying the validity of the certificate provided by the server during the connection. Requires `--ssl`.
|
||||||
|
|
||||||
|
### ssl-ca
|
||||||
|
|
||||||
|
`--ssl-ca=/path/to/ca-cert.pem`: ca certificate file (in PEM format) to use for server certificate verification. If specified, the default system ca cert pool will not be used for verification, only the ca cert provided here. Requires `--ssl`.
|
||||||
|
|
||||||
### test-on-replica
|
### test-on-replica
|
||||||
|
|
||||||
Issue the migration on a replica; do not modify data on master. Useful for validating, testing and benchmarking. See [`testing-on-replica`](testing-on-replica.md)
|
Issue the migration on a replica; do not modify data on master. Useful for validating, testing and benchmarking. See [`testing-on-replica`](testing-on-replica.md)
|
||||||
|
@ -99,6 +99,9 @@ type MigrationContext struct {
|
|||||||
ConfigFile string
|
ConfigFile string
|
||||||
CliUser string
|
CliUser string
|
||||||
CliPassword string
|
CliPassword string
|
||||||
|
UseTLS bool
|
||||||
|
TLSAllowInsecure bool
|
||||||
|
TLSCACertificate string
|
||||||
CliMasterUser string
|
CliMasterUser string
|
||||||
CliMasterPassword string
|
CliMasterPassword string
|
||||||
|
|
||||||
@ -697,6 +700,13 @@ func (this *MigrationContext) ApplyCredentials() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *MigrationContext) SetupTLS() error {
|
||||||
|
if this.UseTLS {
|
||||||
|
return this.InspectorConnectionConfig.UseTLS(this.TLSCACertificate, this.TLSAllowInsecure)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReadConfigFile attempts to read the config file, if it exists
|
// ReadConfigFile attempts to read the config file, if it exists
|
||||||
func (this *MigrationContext) ReadConfigFile() error {
|
func (this *MigrationContext) ReadConfigFile() error {
|
||||||
this.configMutex.Lock()
|
this.configMutex.Lock()
|
||||||
|
@ -46,6 +46,7 @@ func NewGoMySQLReader(migrationContext *base.MigrationContext) (binlogReader *Go
|
|||||||
Port: uint16(binlogReader.connectionConfig.Key.Port),
|
Port: uint16(binlogReader.connectionConfig.Key.Port),
|
||||||
User: binlogReader.connectionConfig.User,
|
User: binlogReader.connectionConfig.User,
|
||||||
Password: binlogReader.connectionConfig.Password,
|
Password: binlogReader.connectionConfig.Password,
|
||||||
|
TLSConfig: binlogReader.connectionConfig.TLSConfig(),
|
||||||
UseDecimal: true,
|
UseDecimal: true,
|
||||||
}
|
}
|
||||||
binlogReader.binlogSyncer = replication.NewBinlogSyncer(binlogSyncerConfig)
|
binlogReader.binlogSyncer = replication.NewBinlogSyncer(binlogSyncerConfig)
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/github/gh-ost/go/base"
|
"github.com/github/gh-ost/go/base"
|
||||||
"github.com/github/gh-ost/go/logic"
|
"github.com/github/gh-ost/go/logic"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
"github.com/outbrain/golib/log"
|
"github.com/outbrain/golib/log"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
@ -54,6 +55,10 @@ func main() {
|
|||||||
flag.StringVar(&migrationContext.ConfigFile, "conf", "", "Config file")
|
flag.StringVar(&migrationContext.ConfigFile, "conf", "", "Config file")
|
||||||
askPass := flag.Bool("ask-pass", false, "prompt for MySQL password")
|
askPass := flag.Bool("ask-pass", false, "prompt for MySQL password")
|
||||||
|
|
||||||
|
flag.BoolVar(&migrationContext.UseTLS, "ssl", false, "Enable SSL encrypted connections to MySQL hosts")
|
||||||
|
flag.StringVar(&migrationContext.TLSCACertificate, "ssl-ca", "", "CA certificate in PEM format for TLS connections to MySQL hosts. Requires --ssl")
|
||||||
|
flag.BoolVar(&migrationContext.TLSAllowInsecure, "ssl-allow-insecure", false, "Skips verification of MySQL hosts' certificate chain and host name. Requires --ssl")
|
||||||
|
|
||||||
flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)")
|
flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)")
|
||||||
flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)")
|
flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)")
|
||||||
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
|
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
|
||||||
@ -196,6 +201,12 @@ func main() {
|
|||||||
if migrationContext.CliMasterPassword != "" && migrationContext.AssumeMasterHostname == "" {
|
if migrationContext.CliMasterPassword != "" && migrationContext.AssumeMasterHostname == "" {
|
||||||
log.Fatalf("--master-password requires --assume-master-host")
|
log.Fatalf("--master-password requires --assume-master-host")
|
||||||
}
|
}
|
||||||
|
if migrationContext.TLSCACertificate != "" && !migrationContext.UseTLS {
|
||||||
|
log.Fatalf("--ssl-ca requires --ssl")
|
||||||
|
}
|
||||||
|
if migrationContext.TLSAllowInsecure && !migrationContext.UseTLS {
|
||||||
|
log.Fatalf("--ssl-allow-insecure requires --ssl")
|
||||||
|
}
|
||||||
if *replicationLagQuery != "" {
|
if *replicationLagQuery != "" {
|
||||||
log.Warningf("--replication-lag-query is deprecated")
|
log.Warningf("--replication-lag-query is deprecated")
|
||||||
}
|
}
|
||||||
@ -240,6 +251,9 @@ func main() {
|
|||||||
migrationContext.SetThrottleHTTP(*throttleHTTP)
|
migrationContext.SetThrottleHTTP(*throttleHTTP)
|
||||||
migrationContext.SetDefaultNumRetries(*defaultRetries)
|
migrationContext.SetDefaultNumRetries(*defaultRetries)
|
||||||
migrationContext.ApplyCredentials()
|
migrationContext.ApplyCredentials()
|
||||||
|
if err := migrationContext.SetupTLS(); err != nil {
|
||||||
|
log.Fatale(err)
|
||||||
|
}
|
||||||
if err := migrationContext.SetCutOverLockTimeoutSeconds(*cutOverLockTimeoutSeconds); err != nil {
|
if err := migrationContext.SetCutOverLockTimeoutSeconds(*cutOverLockTimeoutSeconds); err != nil {
|
||||||
log.Errore(err)
|
log.Errore(err)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ func (this *Applier) InitDBConnections() (err error) {
|
|||||||
if this.db, _, err = mysql.GetDB(this.migrationContext.Uuid, applierUri); err != nil {
|
if this.db, _, err = mysql.GetDB(this.migrationContext.Uuid, applierUri); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
singletonApplierUri := fmt.Sprintf("%s?timeout=0", applierUri)
|
singletonApplierUri := fmt.Sprintf("%s&timeout=0", applierUri)
|
||||||
if this.singletonDB, _, err = mysql.GetDB(this.migrationContext.Uuid, singletonApplierUri); err != nil {
|
if this.singletonDB, _, err = mysql.GetDB(this.migrationContext.Uuid, singletonApplierUri); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,14 @@
|
|||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
|
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
|
||||||
@ -16,6 +22,7 @@ type ConnectionConfig struct {
|
|||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
ImpliedKey *InstanceKey
|
ImpliedKey *InstanceKey
|
||||||
|
tlsConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectionConfig() *ConnectionConfig {
|
func NewConnectionConfig() *ConnectionConfig {
|
||||||
@ -29,9 +36,10 @@ func NewConnectionConfig() *ConnectionConfig {
|
|||||||
// DuplicateCredentials creates a new connection config with given key and with same credentials as this config
|
// DuplicateCredentials creates a new connection config with given key and with same credentials as this config
|
||||||
func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionConfig {
|
func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionConfig {
|
||||||
config := &ConnectionConfig{
|
config := &ConnectionConfig{
|
||||||
Key: key,
|
Key: key,
|
||||||
User: this.User,
|
User: this.User,
|
||||||
Password: this.Password,
|
Password: this.Password,
|
||||||
|
tlsConfig: this.tlsConfig,
|
||||||
}
|
}
|
||||||
config.ImpliedKey = &config.Key
|
config.ImpliedKey = &config.Key
|
||||||
return config
|
return config
|
||||||
@ -42,13 +50,47 @@ func (this *ConnectionConfig) Duplicate() *ConnectionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *ConnectionConfig) String() string {
|
func (this *ConnectionConfig) String() string {
|
||||||
return fmt.Sprintf("%s, user=%s", this.Key.DisplayString(), this.User)
|
return fmt.Sprintf("%s, user=%s, usingTLS=%t", this.Key.DisplayString(), this.User, this.tlsConfig != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *ConnectionConfig) Equals(other *ConnectionConfig) bool {
|
func (this *ConnectionConfig) Equals(other *ConnectionConfig) bool {
|
||||||
return this.Key.Equals(&other.Key) || this.ImpliedKey.Equals(other.ImpliedKey)
|
return this.Key.Equals(&other.Key) || this.ImpliedKey.Equals(other.ImpliedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *ConnectionConfig) UseTLS(caCertificatePath string, allowInsecure bool) error {
|
||||||
|
var rootCertPool *x509.CertPool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if !allowInsecure {
|
||||||
|
if caCertificatePath == "" {
|
||||||
|
rootCertPool, err = x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootCertPool = x509.NewCertPool()
|
||||||
|
pem, err := ioutil.ReadFile(caCertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
||||||
|
return errors.New("could not add ca certificate to cert pool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tlsConfig = &tls.Config{
|
||||||
|
RootCAs: rootCertPool,
|
||||||
|
InsecureSkipVerify: allowInsecure,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mysql.RegisterTLSConfig(this.Key.StringCode(), this.tlsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ConnectionConfig) TLSConfig() *tls.Config {
|
||||||
|
return this.tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
func (this *ConnectionConfig) GetDBUri(databaseName string) string {
|
func (this *ConnectionConfig) GetDBUri(databaseName string) string {
|
||||||
hostname := this.Key.Hostname
|
hostname := this.Key.Hostname
|
||||||
var ip = net.ParseIP(hostname)
|
var ip = net.ParseIP(hostname)
|
||||||
@ -57,5 +99,11 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
|
|||||||
hostname = fmt.Sprintf("[%s]", hostname)
|
hostname = fmt.Sprintf("[%s]", hostname)
|
||||||
}
|
}
|
||||||
interpolateParams := true
|
interpolateParams := true
|
||||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?interpolateParams=%t&autocommit=true&charset=utf8mb4,utf8,latin1", this.User, this.Password, hostname, this.Key.Port, databaseName, interpolateParams)
|
// go-mysql-driver defaults to false if tls param is not provided; explicitly setting here to
|
||||||
|
// simplify construction of the DSN below.
|
||||||
|
tlsOption := "false"
|
||||||
|
if this.tlsConfig != nil {
|
||||||
|
tlsOption = this.Key.StringCode()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?interpolateParams=%t&autocommit=true&charset=utf8mb4,utf8,latin1&tls=%s", this.User, this.Password, hostname, this.Key.Port, databaseName, interpolateParams, tlsOption)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package mysql
|
package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/outbrain/golib/log"
|
"github.com/outbrain/golib/log"
|
||||||
@ -31,6 +32,10 @@ func TestDuplicateCredentials(t *testing.T) {
|
|||||||
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
||||||
c.User = "gromit"
|
c.User = "gromit"
|
||||||
c.Password = "penguin"
|
c.Password = "penguin"
|
||||||
|
c.tlsConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
ServerName: "feathers",
|
||||||
|
}
|
||||||
|
|
||||||
dup := c.DuplicateCredentials(InstanceKey{Hostname: "otherhost", Port: 3310})
|
dup := c.DuplicateCredentials(InstanceKey{Hostname: "otherhost", Port: 3310})
|
||||||
test.S(t).ExpectEquals(dup.Key.Hostname, "otherhost")
|
test.S(t).ExpectEquals(dup.Key.Hostname, "otherhost")
|
||||||
@ -39,6 +44,7 @@ func TestDuplicateCredentials(t *testing.T) {
|
|||||||
test.S(t).ExpectEquals(dup.ImpliedKey.Port, 3310)
|
test.S(t).ExpectEquals(dup.ImpliedKey.Port, 3310)
|
||||||
test.S(t).ExpectEquals(dup.User, "gromit")
|
test.S(t).ExpectEquals(dup.User, "gromit")
|
||||||
test.S(t).ExpectEquals(dup.Password, "penguin")
|
test.S(t).ExpectEquals(dup.Password, "penguin")
|
||||||
|
test.S(t).ExpectEquals(dup.tlsConfig, c.tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDuplicate(t *testing.T) {
|
func TestDuplicate(t *testing.T) {
|
||||||
@ -63,5 +69,16 @@ func TestGetDBUri(t *testing.T) {
|
|||||||
c.Password = "penguin"
|
c.Password = "penguin"
|
||||||
|
|
||||||
uri := c.GetDBUri("test")
|
uri := c.GetDBUri("test")
|
||||||
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1")
|
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDBUriWithTLSSetup(t *testing.T) {
|
||||||
|
c := NewConnectionConfig()
|
||||||
|
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
|
||||||
|
c.User = "gromit"
|
||||||
|
c.Password = "penguin"
|
||||||
|
c.tlsConfig = &tls.Config{}
|
||||||
|
|
||||||
|
uri := c.GetDBUri("test")
|
||||||
|
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=myhost:3306")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user