Post

MySQL SSL 开发环境配置与排查深度复盘

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.128server-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 协议协商 (明文阶段)

  1. Server Greeting: 服务端发送初始包,包含 Capabilities Flags。若开启 SSL,标志位中包含 CLIENT_SSL
  2. SSL Request: 客户端响应,设置 CLIENT_SSL 标志,告知服务端“我要升级到 SSL”。
  3. Switch to SSL: 此时双方停止 MySQL 协议解析,直接在这个 TCP 连接上开始 TLS 握手。

B. TLS 握手 (加密协商阶段)

TLS 1.2 (传统模式 - 证书明文可见)

  1. Client Hello: 客户端发送支持的协议版本、加密套件列表、随机数。
  2. Server Hello: 服务端选定加密套件。
  3. Certificate: 服务端发送 server-cert.pem (明文)。Wireshark 可见
  4. Server Key Exchange: (可选) 发送密钥交换参数。
  5. Client Key Exchange: 客户端验证证书后,生成预主密钥并用服务端公钥加密发送。
  6. Change Cipher Spec: 双方通知“后续开始加密”。
  7. Finished: 握手完成。

TLS 1.3 (现代模式 - 证书被加密)

  1. Client Hello: 包含 Key Share (客户端公钥)。
  2. Server Hello: 包含 Key Share (服务端公钥) 和选定的加密套件 (0x1301)。
    • 关键点: 此时双方已根据 Diffie-Hellman 算法计算出了临时加密密钥。
  3. Encrypted Extensions: (开始加密) 所有的后续握手消息都被加密。
  4. Certificate: 服务端证书在加密通道中传输。Wireshark 只能看到 Encrypted Handshake Message
  5. Finished: 握手完成。

这也是为什么在排查中,虽然我们确认连接成功,但在抓包中却找不到证书原文的原因。

4. 客户端验证证书的详细步骤

当客户端(如 mysql-cli)收到服务端发来的证书后,会执行以下严格校验:

  1. 构建证书链 (Chain Building)
    • 客户端读取证书的 Issuer(颁发者)字段。
    • 在本地受信任库(或 --ssl-ca 指定的文件)中查找对应的根证书。
  2. 数字签名验证 (Signature Verification) —— 数学核心
    • 解密: 客户端使用本地 CA 证书中的公钥,解密服务器证书上的数字签名
    • 比对: 客户端自行计算服务器证书内容的哈希值,并与解密出的签名进行比对。
    • 原理: 只有持有 CA 私钥的人才能生成这个签名,但任何人有 CA 公钥都能验证它。比对一致证明证书未被篡改且确实由该 CA 签发。
  3. 有效期检查 (Validity Check)
    • 检查当前系统时间是否在 Not BeforeNot After 之间。
  4. 主机名验证 (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.pemclient-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

总结:最佳实践建议

  1. 本地快速开发: 推荐在 Docker Command 中使用 --skip-ssl,避免各种客户端的 SSL 兼容性烦恼。
  2. 生产环境/远程调试: 必须生成带 SAN (IP/域名) 的证书,并强制客户端使用 VERIFY_CAVERIFY_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 系统上:

  1. 将 CA 证书复制到信任源目录:
1
sudo cp controller/certs/ca.pem /etc/pki/ca-trust/source/anchors/sentinel-dev-ca.pem
  1. 更新信任库:
1
sudo update-ca-trust

执行后,系统会自动将你的 CA 合并到 ca-bundle.crt 中,以后 mysqlcurl 命令即可自动信任。

在 Debian / Ubuntu 系统上:

  1. 复制证书:
1
sudo cp controller/certs/ca.pem /usr/local/share/ca-certificates/sentinel-dev.crt
  1. 更新:
1
sudo update-ca-certificates
This post is licensed under CC BY 4.0 by the author.