Add initial support for ssl encryption connections to database servers.

- Adding a command line option for users to enforce tls/ssl connections
  for the applier, inspector, and binlog reader.
- The user can optionally request server certificate verification through
  a command line option to specify the ca cert via a file path.
- Fixes an existing bug appending the timeout option to the singleton
  applier connection.
This commit is contained in:
Brandon Bodnar 2019-01-31 10:03:48 -06:00
parent 79ddcecb1f
commit 23617f287f
6 changed files with 83 additions and 7 deletions

View File

@ -99,6 +99,8 @@ type MigrationContext struct {
ConfigFile string ConfigFile string
CliUser string CliUser string
CliPassword string CliPassword string
UseTLS bool
TlsCACertificate string
CliMasterUser string CliMasterUser string
CliMasterPassword string CliMasterPassword string
@ -695,6 +697,13 @@ func (this *MigrationContext) ApplyCredentials() {
} }
} }
func (this *MigrationContext) SetupTLS() error {
if this.UseTLS {
return this.InspectorConnectionConfig.UseTLS(this.TlsCACertificate)
}
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()

View File

@ -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)

View File

@ -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,9 @@ 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")
flag.StringVar(&migrationContext.TlsCACertificate, "ssl-ca", "", "CA certificate in PEM format for TLS connections. 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)")
@ -194,6 +198,9 @@ 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 *replicationLagQuery != "" { if *replicationLagQuery != "" {
log.Warningf("--replication-lag-query is deprecated") log.Warningf("--replication-lag-query is deprecated")
} }
@ -238,6 +245,7 @@ func main() {
migrationContext.SetThrottleHTTP(*throttleHTTP) migrationContext.SetThrottleHTTP(*throttleHTTP)
migrationContext.SetDefaultNumRetries(*defaultRetries) migrationContext.SetDefaultNumRetries(*defaultRetries)
migrationContext.ApplyCredentials() migrationContext.ApplyCredentials()
migrationContext.SetupTLS()
if err := migrationContext.SetCutOverLockTimeoutSeconds(*cutOverLockTimeoutSeconds); err != nil { if err := migrationContext.SetCutOverLockTimeoutSeconds(*cutOverLockTimeoutSeconds); err != nil {
log.Errore(err) log.Errore(err)
} }

View File

@ -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
} }

View File

@ -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 {
@ -32,6 +39,7 @@ func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionC
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,42 @@ 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) error {
skipVerify := caCertificatePath == ""
var rootCertPool *x509.CertPool
if !skipVerify {
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: skipVerify,
}
if err := mysql.RegisterTLSConfig(this.Key.StringCode(), this.tlsConfig); err != nil {
return err
}
return nil
}
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 +94,9 @@ 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) 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)
} }

View File

@ -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")
} }