MySQL SSL 开发环境配置与排查深度复盘
MySQL SSL 开发环境配置与排查深度复盘
本文档详细记录了在 Docker 环境下配置 MySQL 8.0 SSL 开发环境的全过程。按排查逻辑顺序组织,还原从报错到解决的完整上下文。
第一阶段:起因与初次报错
1. 目标
我们需要在开发环境(Docker)中运行 MySQL,并将端口 3306 映射到宿主机的 33306,以便本地工具(如 Gobang, DBeaver)连接。
2. 遇到的问题:InvalidDNSNameError
当我们尝试通过 172.16.58.128:33306 连接时,Gobang 报错:
error occurred while attempting to establish a TLS connection: InvalidDNSNameError
3. 原因分析
- MySQL 默认启用 SSL,并使用自签名证书。
- 默认证书的
Common Name (CN)通常是MySQL_Server_...或localhost。 - 客户端尝试连接 IP
172.16.58.128,但证书里没有这个 IP。 - 客户端的安全机制(Rustls/OpenSSL)发现证书持有者名称与目标 IP 不一致,拒绝连接。
第二阶段:尝试修复 —— 生成自签名证书
为了解决上述错误,我们决定生成一张包含 IP 地址的证书。
1. 操作步骤
我们创建了 san.cnf 配置文件,指定了 Subject Alternative Name (SAN):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# controller/certs/san.cnf
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = MySQL Dev Server
O = Sentinel Flow
OU = Dev
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 172.16.58.128
DNS.1 = localhost
然后使用 OpenSSL 命令生成证书链(脚本路径:gen_certs.sh):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 生成 CA (根证书)
openssl genrsa 2048 > ca-key.pem
openssl req -new -x509 -nodes -days 3650 -key ca-key.pem -out ca.pem -subj "/CN=Sentinel Dev CA"
# 2. 生成服务器私钥和请求 (CSR)
openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-req.pem -config san.cnf
# 3. 使用 CA 签署服务器证书 (带 SAN 扩展)
# 这一步至关重要,必须把 san.cnf 作为 extfile 传入,否则 IP 信息不会被写入证书
openssl x509 -req -in server-req.pem -days 3650 -CA ca.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem -extensions v3_req -extfile san.cnf
# 4. 生成客户端证书 (用于双向认证)
openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-req.pem -subj "/CN=Sentinel Dev Client"
openssl x509 -req -in client-req.pem -days 3650 -CA ca.pem -CAkey ca-key.pem -set_serial 02 -out client-cert.pem
这样我们就得到了包含 IP:172.16.58.128 的 server-cert.pem,可以被客户端正确验证。
2. 新的问题:Unable to get private key
将证书挂载到 Docker 后,MySQL 容器启动失败:
[Server] SSL error: Unable to get private key from '/etc/mysql/certs/server-key.pem'
- 原因: 宿主机上生成的私钥文件权限默认是
600(仅 root 可读)。映射进容器后,容器内的mysql用户(uid 999)无法读取。 - 解决: 在宿主机执行
chmod 644 server-key.pem。
第三阶段:深入原理 —— 证书交换与 TLS 协议
在配置好证书后,我们深入探讨了 SSL 握手的细节。
1. 为什么抓包看不到证书 (server-cert.pem)?
我们尝试用 Wireshark 抓包,却发现找不到证书的明文传输。
- 现象: 看到
Server Hello后,紧接着全是Encrypted Handshake Message。 - 原因: 双方协商使用了 TLS 1.3 协议。
- TLS 1.2: 证书在握手阶段明文发送。
- TLS 1.3: 在
Server Hello之后立即切换到加密模式,证书本身也是被加密传输的。
- 结论: 看不到是正常的,说明 TLS 1.3 安全机制生效了。
2. CA 签名的必要性
- 问题: 既然公钥能加密,为什么一定要 CA 签名?
- 解答: 防止中间人攻击 (Man-in-the-Middle)。
- 黑客可以给客户端发自己的公钥。如果没有 CA 证书(公章)做验证,客户端无法区分这个公钥是属于服务器的还是黑客的。
- CA 证书 (
ca.pem) 充当了“公安局”的角色,用来验证服务端出示的“身份证”是否伪造。
3. SSL/TLS 握手流程详解
为了更清晰地理解连接过程,我们将 MySQL 的 SSL 建立过程拆解为以下步骤:
A. MySQL 协议协商 (明文阶段)
- Server Greeting: 服务端发送初始包,包含
Capabilities Flags。若开启 SSL,标志位中包含CLIENT_SSL。 - SSL Request: 客户端响应,设置
CLIENT_SSL标志,告知服务端“我要升级到 SSL”。 - Switch to SSL: 此时双方停止 MySQL 协议解析,直接在这个 TCP 连接上开始 TLS 握手。
B. TLS 握手 (加密协商阶段)
TLS 1.2 (传统模式 - 证书明文可见)
- Client Hello: 客户端发送支持的协议版本、加密套件列表、随机数。
- Server Hello: 服务端选定加密套件。
- Certificate: 服务端发送
server-cert.pem(明文)。Wireshark 可见。 - Server Key Exchange: (可选) 发送密钥交换参数。
- Client Key Exchange: 客户端验证证书后,生成预主密钥并用服务端公钥加密发送。
- Change Cipher Spec: 双方通知“后续开始加密”。
- Finished: 握手完成。
TLS 1.3 (现代模式 - 证书被加密)
- Client Hello: 包含
Key Share(客户端公钥)。 - Server Hello: 包含
Key Share(服务端公钥) 和选定的加密套件 (0x1301)。- 关键点: 此时双方已根据 Diffie-Hellman 算法计算出了临时加密密钥。
- Encrypted Extensions: (开始加密) 所有的后续握手消息都被加密。
- Certificate: 服务端证书在加密通道中传输。Wireshark 只能看到
Encrypted Handshake Message。 - Finished: 握手完成。
这也是为什么在排查中,虽然我们确认连接成功,但在抓包中却找不到证书原文的原因。
4. 客户端验证证书的详细步骤
当客户端(如 mysql-cli)收到服务端发来的证书后,会执行以下严格校验:
- 构建证书链 (Chain Building)
- 客户端读取证书的
Issuer(颁发者)字段。 - 在本地受信任库(或
--ssl-ca指定的文件)中查找对应的根证书。
- 客户端读取证书的
- 数字签名验证 (Signature Verification) —— 数学核心
- 解密: 客户端使用本地 CA 证书中的公钥,解密服务器证书上的数字签名。
- 比对: 客户端自行计算服务器证书内容的哈希值,并与解密出的签名进行比对。
- 原理: 只有持有 CA 私钥的人才能生成这个签名,但任何人有 CA 公钥都能验证它。比对一致证明证书未被篡改且确实由该 CA 签发。
- 有效期检查 (Validity Check)
- 检查当前系统时间是否在
Not Before和Not After之间。
- 检查当前系统时间是否在
- 主机名验证 (Hostname Verification) —— 报错根源
- 这是防范中间人攻击的最后防线。即便证书是真的,也不能证明它属于你正在连接的这台机器。
- 客户端检查证书的
SAN (Subject Alternative Name)字段。 - 必须匹配: SAN 列表必须包含当前连接的目标 IP(
172.16.58.128)。如果不包含,报错InvalidDNSNameError。
第四阶段:连接断开与客户端兼容性
1. 现象:Greeting 后连接断开
在解决了证书和权限后,Gobang 客户端连接时表现为:收到服务端的 Greeting 包后立即断开连接。
2. 原因推测
- 兼容性: Gobang(或其底层驱动)可能在处理 MySQL 8.0 的 SSL 握手或特定的 Greeting 包格式时存在 bug。
- 强制策略冲突: 客户端配置可能与服务端实际协商的参数(如 TLS 版本)不匹配。
3. 验证方法
使用官方 mysql 客户端连接成功,证明服务端配置无误。
1
mysql -h 172.16.58.128 -P 33306 -u root -p --ssl-ca=~/.config/gobang/certs/ca.pem --ssl-mode=VERIFY_IDENTITY
这确认了问题出在 Gobang 客户端对特定环境的支持上。
第五阶段:双向认证 (mTLS) 模拟
最后,我们模拟了最高安全级别的双向认证。
1. 配置逻辑
- 服务端: 不仅出示自己的证书,还要求客户端出示证书 (
REQUIRE X509)。 - 客户端: 连接时必须提供
client-cert.pem和client-key.pem。
2. 关键误区:密码还需要吗?
- 误区: 以为有了证书就不用输密码了。
- 现实:
REQUIRE X509是在密码验证之上的叠加条件。- 连接流程:TCP -> TLS 握手(互换证书验证) -> 成功建立加密通道 -> MySQL 协议登录(发送用户名/密码)。
- 免密方案: 若要实现仅凭证书登录,需将数据库用户密码设为空,并配合
REQUIRE SUBJECT锁定特定证书。
1
mysql -h 172.16.58.128 -P 33306 -u client_mtls -p --ssl-ca=~/.config/gobang/certs/ca.pem --ssl-cert=~/.config/gobang/certs/client-cert.pem --ssl-key=~/.config/gobang/certs/client-key.pem --ssl-mode=VERIFY_CA
总结:最佳实践建议
- 本地快速开发: 推荐在 Docker Command 中使用
--skip-ssl,避免各种客户端的 SSL 兼容性烦恼。 生产环境/远程调试: 必须生成带 SAN (IP/域名) 的证书,并强制客户端使用
VERIFY_CA或VERIFY_IDENTITY模式,确保连接不被劫持。
附录:完整的 Docker Compose 配置
这是我们最终调试成功的 controller/docker-compose-dev.yaml 文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3'
services:
mysql:
image: mysql:8.0
container_name: sentinel_controller_db_dev
command:
- --skip-name-resolve
- --ssl-ca=/etc/mysql/certs/ca.pem
- --ssl-cert=/etc/mysql/certs/server-cert.pem
- --ssl-key=/etc/mysql/certs/server-key.pem
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: sentinel_flow_controller_dev
ports:
- "33306:3306"
volumes:
GRANT ALL PRIVILEGES ON *.* TO 'client_cert_only'@'%';
FLUSH PRIVILEGES;
附录 C:系统默认 CA 路径与手动安装指南
如果不想在每次连接时都显式指定 --ssl-ca,可以将自签名 CA 证书安装到操作系统的默认信任库中。
1. 默认信任库位置
客户端在未指定 CA 时,会去以下路径查找根证书:
- RHEL / CentOS / Fedora / Alpine (当前环境):
/etc/pki/tls/certs/ca-bundle.crt
- Debian / Ubuntu:
/etc/ssl/certs/ca-certificates.crt
2. 如何安装自签名 CA
在 RHEL / CentOS 系统上:
- 将 CA 证书复制到信任源目录:
1
sudo cp controller/certs/ca.pem /etc/pki/ca-trust/source/anchors/sentinel-dev-ca.pem
- 更新信任库:
1
sudo update-ca-trust
执行后,系统会自动将你的 CA 合并到 ca-bundle.crt 中,以后 mysql 或 curl 命令即可自动信任。
在 Debian / Ubuntu 系统上:
- 复制证书:
1
sudo cp controller/certs/ca.pem /usr/local/share/ca-certificates/sentinel-dev.crt
- 更新:
1
sudo update-ca-certificates