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:
parent
79ddcecb1f
commit
23617f287f
@ -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()
|
||||||
|
@ -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,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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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…
Reference in New Issue
Block a user