Skip to content

Commit 4ff5d6a

Browse files
author
Shlomi Noach
authored
Merge pull request #705 from brandonbodnar-wk/add-tls-support
Initial SSL Connection Support
2 parents 909ef0e + 09ef7f4 commit 4ff5d6a

File tree

7 files changed

+109
-7
lines changed

7 files changed

+109
-7
lines changed

doc/command-line-flags.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ By default `gh-ost` verifies no foreign keys exist on the migrated table. On ser
177177

178178
See [`approve-renamed-columns`](#approve-renamed-columns)
179179

180+
### ssl
181+
182+
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`.
183+
184+
### ssl-allow-insecure
185+
186+
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`.
187+
188+
### ssl-ca
189+
190+
`--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`.
191+
180192
### test-on-replica
181193

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

go/base/context.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ type MigrationContext struct {
9999
ConfigFile string
100100
CliUser string
101101
CliPassword string
102+
UseTLS bool
103+
TLSAllowInsecure bool
104+
TLSCACertificate string
102105
CliMasterUser string
103106
CliMasterPassword string
104107

@@ -697,6 +700,13 @@ func (this *MigrationContext) ApplyCredentials() {
697700
}
698701
}
699702

703+
func (this *MigrationContext) SetupTLS() error {
704+
if this.UseTLS {
705+
return this.InspectorConnectionConfig.UseTLS(this.TLSCACertificate, this.TLSAllowInsecure)
706+
}
707+
return nil
708+
}
709+
700710
// ReadConfigFile attempts to read the config file, if it exists
701711
func (this *MigrationContext) ReadConfigFile() error {
702712
this.configMutex.Lock()

go/binlog/gomysql_reader.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func NewGoMySQLReader(migrationContext *base.MigrationContext) (binlogReader *Go
4646
Port: uint16(binlogReader.connectionConfig.Key.Port),
4747
User: binlogReader.connectionConfig.User,
4848
Password: binlogReader.connectionConfig.Password,
49+
TLSConfig: binlogReader.connectionConfig.TLSConfig(),
4950
UseDecimal: true,
5051
}
5152
binlogReader.binlogSyncer = replication.NewBinlogSyncer(binlogSyncerConfig)

go/cmd/gh-ost/main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/github/gh-ost/go/base"
1616
"github.com/github/gh-ost/go/logic"
17+
_ "github.com/go-sql-driver/mysql"
1718
"github.com/outbrain/golib/log"
1819

1920
"golang.org/x/crypto/ssh/terminal"
@@ -54,6 +55,10 @@ func main() {
5455
flag.StringVar(&migrationContext.ConfigFile, "conf", "", "Config file")
5556
askPass := flag.Bool("ask-pass", false, "prompt for MySQL password")
5657

58+
flag.BoolVar(&migrationContext.UseTLS, "ssl", false, "Enable SSL encrypted connections to MySQL hosts")
59+
flag.StringVar(&migrationContext.TLSCACertificate, "ssl-ca", "", "CA certificate in PEM format for TLS connections to MySQL hosts. Requires --ssl")
60+
flag.BoolVar(&migrationContext.TLSAllowInsecure, "ssl-allow-insecure", false, "Skips verification of MySQL hosts' certificate chain and host name. Requires --ssl")
61+
5762
flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)")
5863
flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)")
5964
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
@@ -196,6 +201,12 @@ func main() {
196201
if migrationContext.CliMasterPassword != "" && migrationContext.AssumeMasterHostname == "" {
197202
log.Fatalf("--master-password requires --assume-master-host")
198203
}
204+
if migrationContext.TLSCACertificate != "" && !migrationContext.UseTLS {
205+
log.Fatalf("--ssl-ca requires --ssl")
206+
}
207+
if migrationContext.TLSAllowInsecure && !migrationContext.UseTLS {
208+
log.Fatalf("--ssl-allow-insecure requires --ssl")
209+
}
199210
if *replicationLagQuery != "" {
200211
log.Warningf("--replication-lag-query is deprecated")
201212
}
@@ -240,6 +251,9 @@ func main() {
240251
migrationContext.SetThrottleHTTP(*throttleHTTP)
241252
migrationContext.SetDefaultNumRetries(*defaultRetries)
242253
migrationContext.ApplyCredentials()
254+
if err := migrationContext.SetupTLS(); err != nil {
255+
log.Fatale(err)
256+
}
243257
if err := migrationContext.SetCutOverLockTimeoutSeconds(*cutOverLockTimeoutSeconds); err != nil {
244258
log.Errore(err)
245259
}

go/logic/applier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (this *Applier) InitDBConnections() (err error) {
7373
if this.db, _, err = mysql.GetDB(this.migrationContext.Uuid, applierUri); err != nil {
7474
return err
7575
}
76-
singletonApplierUri := fmt.Sprintf("%s?timeout=0", applierUri)
76+
singletonApplierUri := fmt.Sprintf("%s&timeout=0", applierUri)
7777
if this.singletonDB, _, err = mysql.GetDB(this.migrationContext.Uuid, singletonApplierUri); err != nil {
7878
return err
7979
}

go/mysql/connection.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
package mysql
77

88
import (
9+
"crypto/tls"
10+
"crypto/x509"
11+
"errors"
912
"fmt"
13+
"io/ioutil"
1014
"net"
15+
16+
"github.com/go-sql-driver/mysql"
1117
)
1218

1319
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
@@ -16,6 +22,7 @@ type ConnectionConfig struct {
1622
User string
1723
Password string
1824
ImpliedKey *InstanceKey
25+
tlsConfig *tls.Config
1926
}
2027

2128
func NewConnectionConfig() *ConnectionConfig {
@@ -29,9 +36,10 @@ func NewConnectionConfig() *ConnectionConfig {
2936
// DuplicateCredentials creates a new connection config with given key and with same credentials as this config
3037
func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionConfig {
3138
config := &ConnectionConfig{
32-
Key: key,
33-
User: this.User,
34-
Password: this.Password,
39+
Key: key,
40+
User: this.User,
41+
Password: this.Password,
42+
tlsConfig: this.tlsConfig,
3543
}
3644
config.ImpliedKey = &config.Key
3745
return config
@@ -42,13 +50,47 @@ func (this *ConnectionConfig) Duplicate() *ConnectionConfig {
4250
}
4351

4452
func (this *ConnectionConfig) String() string {
45-
return fmt.Sprintf("%s, user=%s", this.Key.DisplayString(), this.User)
53+
return fmt.Sprintf("%s, user=%s, usingTLS=%t", this.Key.DisplayString(), this.User, this.tlsConfig != nil)
4654
}
4755

4856
func (this *ConnectionConfig) Equals(other *ConnectionConfig) bool {
4957
return this.Key.Equals(&other.Key) || this.ImpliedKey.Equals(other.ImpliedKey)
5058
}
5159

60+
func (this *ConnectionConfig) UseTLS(caCertificatePath string, allowInsecure bool) error {
61+
var rootCertPool *x509.CertPool
62+
var err error
63+
64+
if !allowInsecure {
65+
if caCertificatePath == "" {
66+
rootCertPool, err = x509.SystemCertPool()
67+
if err != nil {
68+
return err
69+
}
70+
} else {
71+
rootCertPool = x509.NewCertPool()
72+
pem, err := ioutil.ReadFile(caCertificatePath)
73+
if err != nil {
74+
return err
75+
}
76+
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
77+
return errors.New("could not add ca certificate to cert pool")
78+
}
79+
}
80+
}
81+
82+
this.tlsConfig = &tls.Config{
83+
RootCAs: rootCertPool,
84+
InsecureSkipVerify: allowInsecure,
85+
}
86+
87+
return mysql.RegisterTLSConfig(this.Key.StringCode(), this.tlsConfig)
88+
}
89+
90+
func (this *ConnectionConfig) TLSConfig() *tls.Config {
91+
return this.tlsConfig
92+
}
93+
5294
func (this *ConnectionConfig) GetDBUri(databaseName string) string {
5395
hostname := this.Key.Hostname
5496
var ip = net.ParseIP(hostname)
@@ -57,5 +99,11 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
5799
hostname = fmt.Sprintf("[%s]", hostname)
58100
}
59101
interpolateParams := true
60-
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)
102+
// go-mysql-driver defaults to false if tls param is not provided; explicitly setting here to
103+
// simplify construction of the DSN below.
104+
tlsOption := "false"
105+
if this.tlsConfig != nil {
106+
tlsOption = this.Key.StringCode()
107+
}
108+
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)
61109
}

go/mysql/connection_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package mysql
77

88
import (
9+
"crypto/tls"
910
"testing"
1011

1112
"github.com/outbrain/golib/log"
@@ -31,6 +32,10 @@ func TestDuplicateCredentials(t *testing.T) {
3132
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
3233
c.User = "gromit"
3334
c.Password = "penguin"
35+
c.tlsConfig = &tls.Config{
36+
InsecureSkipVerify: true,
37+
ServerName: "feathers",
38+
}
3439

3540
dup := c.DuplicateCredentials(InstanceKey{Hostname: "otherhost", Port: 3310})
3641
test.S(t).ExpectEquals(dup.Key.Hostname, "otherhost")
@@ -39,6 +44,7 @@ func TestDuplicateCredentials(t *testing.T) {
3944
test.S(t).ExpectEquals(dup.ImpliedKey.Port, 3310)
4045
test.S(t).ExpectEquals(dup.User, "gromit")
4146
test.S(t).ExpectEquals(dup.Password, "penguin")
47+
test.S(t).ExpectEquals(dup.tlsConfig, c.tlsConfig)
4248
}
4349

4450
func TestDuplicate(t *testing.T) {
@@ -63,5 +69,16 @@ func TestGetDBUri(t *testing.T) {
6369
c.Password = "penguin"
6470

6571
uri := c.GetDBUri("test")
66-
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1")
72+
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=false")
73+
}
74+
75+
func TestGetDBUriWithTLSSetup(t *testing.T) {
76+
c := NewConnectionConfig()
77+
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
78+
c.User = "gromit"
79+
c.Password = "penguin"
80+
c.tlsConfig = &tls.Config{}
81+
82+
uri := c.GetDBUri("test")
83+
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=myhost:3306")
6784
}

0 commit comments

Comments
 (0)