Skip to content

Commit 87afc74

Browse files
committed
Allow client certificate authentication for source database
1 parent b0ca415 commit 87afc74

File tree

4 files changed

+92
-18
lines changed

4 files changed

+92
-18
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ The following environment variables are used by migration script:
7373
* `TARGET_SERVICE_URI` - service URI to the target MySQL database with admin credentials, which will be used for dump import.
7474
* `TARGET_MASTER_SERVICE_URI` - service URI for managing replication while migrating, omitting this variable will
7575
lead to fall-back to dump solution.
76+
* `MYSQL_CONNECTION_TIMEOUT` - MySQL connect_timeout (default: 5 seconds)
77+
* `MYSQL_WRITE_TIMEOUT` - MySQL write_timeout (default: 5 seconds)
78+
* `MYSQL_READ_TIMEOUT` - MySQL read_timeout (default: 5 seconds)
79+
SOURCE_SSL_* are optional, when provided it uses client certificate authentication.
80+
* `SOURCE_SSL_CA` - The path name of the Certificate Authority (CA) certificate file in PEM format.
81+
* `SOURCE_SSL_CERT` - The path name of the server SSL public key certificate file in PEM format.
82+
* `SOURCE_SSL_KEY` - The path name of the server SSL private key file in PEM format.
83+
84+
7685

7786
Environment variable are used here instead of usual arguments so that it's not possible to see credentials in the list
7887
of long-running processes. As for the `mysqldump/mysql` subprocesses they won't be visible, because they are hidden by

aiven_mysql_migrate/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@
1111
SOURCE_SERVICE_URI = os.getenv("SOURCE_SERVICE_URI")
1212
TARGET_SERVICE_URI = os.getenv("TARGET_SERVICE_URI")
1313
TARGET_MASTER_SERVICE_URI = os.getenv("TARGET_MASTER_SERVICE_URI")
14+
15+
SOURCE_SSL_CA = os.getenv("SOURCE_SSL_CA")
16+
SOURCE_SSL_CERT = os.getenv("SOURCE_SSL_CERT")
17+
SOURCE_SSL_KEY = os.getenv("SOURCE_SSL_KEY")

aiven_mysql_migrate/migration.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,17 @@ def __init__(
5252
self.mysqldump_proc: Optional[Popen] = None
5353
self.mysql_proc: Optional[Popen] = None
5454

55-
self.source = MySQLConnectionInfo.from_uri(source_uri, name="source")
55+
if config.SOURCE_SSL_CA and config.SOURCE_SSL_CERT and config.SOURCE_SSL_KEY:
56+
self.source = MySQLConnectionInfo.from_uri(
57+
source_uri,
58+
name="source",
59+
sslca=config.SOURCE_SSL_CA,
60+
sslcert=config.SOURCE_SSL_CERT,
61+
sslkey=config.SOURCE_SSL_KEY
62+
)
63+
else:
64+
self.source = MySQLConnectionInfo.from_uri(source_uri, name="source")
65+
5666
self.target = MySQLConnectionInfo.from_uri(target_uri, name="target")
5767
self.target_master = MySQLConnectionInfo.from_uri(
5868
target_master_uri, name="target master"
@@ -160,6 +170,8 @@ def _check_connections(self):
160170
conn_infos.append(self.target_master)
161171

162172
for conn_info in conn_infos:
173+
LOGGER.debug("conn_info :[%s]", conn_info)
174+
163175
try:
164176
with conn_info.cur():
165177
pass

aiven_mysql_migrate/utils.py

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
from urllib.parse import parse_qs, urlparse
77

88
import contextlib
9+
import logging
910
import pymysql
1011
import re
1112

13+
LOGGER = logging.getLogger(__name__)
14+
1215
DEFAULT_MYSQL_PORT = 3306
1316

1417
ROUTINE_DEFINER_RE = re.compile("^CREATE DEFINER *= *(`.*?`@`.*?`) +(.*$)")
@@ -30,14 +33,23 @@ class MySQLConnectionInfo:
3033
username: str
3134
password: str
3235
ssl: Optional[bool] = True
36+
sslca: Optional[str] = None
37+
sslcert: Optional[str] = None
38+
sslkey: Optional[str] = None
3339

3440
name: Optional[str] = None
3541

3642
_version: Optional[str] = None
3743
_global_grants: Optional[List[str]] = None
3844

3945
@staticmethod
40-
def from_uri(uri: str, name: Optional[str] = None):
46+
def from_uri(
47+
uri: str,
48+
name: Optional[str] = None,
49+
sslca: Optional[str] = None,
50+
sslcert: Optional[str] = None,
51+
sslkey: Optional[str] = None
52+
):
4153
try:
4254
res = urlparse(uri, scheme="mysql")
4355
if res.scheme != "mysql" or not res.username or not res.password or not res.hostname:
@@ -49,13 +61,31 @@ def from_uri(uri: str, name: Optional[str] = None):
4961
port = res.port or DEFAULT_MYSQL_PORT
5062
options = parse_qs(res.query)
5163
ssl = not (options and options.get("ssl-mode", ["DISABLE"]) == ["DISABLE"])
52-
return MySQLConnectionInfo(
53-
hostname=res.hostname, port=port, username=res.username, password=res.password, ssl=ssl, name=name
54-
)
64+
65+
LOGGER.debug("from_uri - sslca:[%s], sslcert:[%s], sslkey:[%s]", sslca, sslcert, sslkey)
66+
if sslca and sslcert and sslkey:
67+
return MySQLConnectionInfo(
68+
hostname=res.hostname,
69+
port=port,
70+
username=res.username,
71+
password=res.password,
72+
ssl=ssl,
73+
sslca=sslca,
74+
sslcert=sslcert,
75+
sslkey=sslkey,
76+
name=name
77+
)
78+
else:
79+
return MySQLConnectionInfo(
80+
hostname=res.hostname, port=port, username=res.username, password=res.password, ssl=ssl, name=name
81+
)
5582

5683
def to_uri(self):
5784
ssl_mode = "DISABLE" if not self.ssl else "REQUIRE"
58-
return f"mysql://{self.username}:{self.password}@{self.hostname}:{self.port}/?ssl-mode={ssl_mode}"
85+
ssl_auth = f"&ssl-ca={self.sslca}&ssl-cert={self.sslcert}&ssl-key={self.sslkey}" \
86+
if self.sslca and self.sslcert and self.sslcert else ""
87+
LOGGER.debug("ssl_auth:[%s]]", ssl_auth)
88+
return f"mysql://{self.username}:{self.password}@{self.hostname}:{self.port}/?ssl-mode={ssl_mode}{ssl_auth}"
5989

6090
def repr(self):
6191
return self.name
@@ -64,18 +94,37 @@ def _connect(self):
6494
ssl = None
6595
if self.ssl:
6696
ssl = {"require": True}
67-
return pymysql.connect(
68-
charset="utf8mb4",
69-
connect_timeout=config.MYSQL_CONNECTION_TIMEOUT,
70-
cursorclass=pymysql.cursors.DictCursor,
71-
host=self.hostname,
72-
password=self.password,
73-
read_timeout=config.MYSQL_READ_TIMEOUT,
74-
port=self.port,
75-
ssl=ssl,
76-
user=self.username,
77-
write_timeout=config.MYSQL_WRITE_TIMEOUT,
78-
)
97+
98+
if self.sslca and self.sslcert and self.sslkey:
99+
LOGGER.debug("connect [%s]- sslca:[%s], sslcert:[%s], sslkey:[%s]",
100+
self.name, self.sslca, self.sslcert, self.sslkey)
101+
return pymysql.connect(
102+
charset="utf8mb4",
103+
connect_timeout=config.MYSQL_CONNECTION_TIMEOUT,
104+
cursorclass=pymysql.cursors.DictCursor,
105+
host=self.hostname,
106+
password=self.password,
107+
read_timeout=config.MYSQL_READ_TIMEOUT,
108+
port=self.port,
109+
ssl_ca=self.sslca,
110+
ssl_cert=self.sslcert,
111+
ssl_key=self.sslkey,
112+
user=self.username,
113+
write_timeout=config.MYSQL_WRITE_TIMEOUT,
114+
)
115+
else:
116+
return pymysql.connect(
117+
charset="utf8mb4",
118+
connect_timeout=config.MYSQL_CONNECTION_TIMEOUT,
119+
cursorclass=pymysql.cursors.DictCursor,
120+
host=self.hostname,
121+
password=self.password,
122+
read_timeout=config.MYSQL_READ_TIMEOUT,
123+
port=self.port,
124+
ssl=ssl,
125+
user=self.username,
126+
write_timeout=config.MYSQL_WRITE_TIMEOUT,
127+
)
79128

80129
@property
81130
def version(self) -> str:

0 commit comments

Comments
 (0)