跨 CA 签发多个证书的 Nginx mTLS 配置

研究过用同一个 CA 签发的服务端和客户端证书的 Nginx mTLS 配置,本文要试验一番服务端和客户端证书由不同 CA 机构签发的情形。这是常有事,比如与客户间采用 mTLS 加密方式,需要文件交付可能是


  1. 客户端证书由甲方生成,发送客户端私钥和证书(或放在一起的 PKCS#12 格式证书)给乙方
  2. 或由乙方生成客户端私钥或证书,乙方把签发用的 CA 证书发给甲方已配置信任链
  3. 甚至服务端,客户端的证书都由甲方生成的情况下也可能使用不同的 CA 签发

下面来测试不同 CA 签发证书的 Nginx mTLS 配置。

今天升级了 ChatGPT 为 Plus 版本,可以用 ChatGPT 4o, 确实是比较强,输入 "mtls 不同 ca 签发的服务端客户端证书在 nginx 中的配置" 提示符产生的内容几乎可以直接作为博文。但本人必须遵循本博客非 AI 产生的原则,只参考 ChatGTP 的答案,关键是一个要自己亲自动手验证并理解每一项配置的功用。

本文需要用到的所有 key 和证书按如下步骤操作

生成 server 和 client 的 CA 证书

1openssl req -x509 -newkey rsa:2048 -nodes -keyout server-ca.key -out server-ca.crt  \
2  -subj "/C=US/ST=Illinois/L=Chicago/O=Server CA Company/CN=ServerCA"
3
4openssl req -x509 -newkey rsa:2048 -nodes -keyout client-ca.key -out client-ca.crt  \
5  -subj "/C=US/ST=New York/L=New York/O=Client CA Company/CN=ClientCA"

分别生成 server-ca.key, server-ca.crt, client-ca.key, client-ca.crt

然后分别用它们签发服务端与客户端证书

用 server ca 签发服务端证书

1openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
2  -subj "/C=US/ST=Illinois/L=Chicago/O=X Company/CN=server.local"
3
4openssl x509 -req -in server.csr -out server.crt -CA server-ca.crt -CAkey server-ca.key -days 365

生成了 server.key, server.crt

注:CN=server.local 是指定的域名,本机或其他机器要访问该 Nginx, 应在 /etc/hosts 中配置 <nginx ip> server.local, 然后就可以解析 server.local 了

用  client ca 签发客户端证书

1openssl req -newkey rsa:2048 -nodes -keyout client1.key -out client1.csr \
2  -subj "/C=US/ST=Iowa/L=Iowa/O=Company 1/CN=client1"
3
4openssl x509 -req -in client1.csr -out client1.crt -CA client-ca.crt -CAkey client-ca.key -days 365

生成了 client1.key, client1.crt

Nginx 中的 mTLS 配置

先尝试一下错误的配置,在 /etc/nginx/nginx.conf 中如果 server {} 配置是
 1server {
 2    listen 443 ssl;
 3    server_name server.local;
 4    ssl_certificate /certs/server.crt;
 5    ssl_certificate_key /certs/server.key;
 6
 7    ssl_verify_client on;
 8
 9    ssl_client_certificate /certs/server-ca.crt;
10}

也就是 ssl_client_certificate 指定为 /certs/server-ca.crt

curl 带上 --cert client1.crt --key client1.key  也访问不了
1$ curl --cert client1.crt --key client1.key https://server.local -k
2<html>
3<head><title>400 The SSL certificate error</title></head>
4<body>
5<center><h1>400 Bad Request</h1></center>
6<center>The SSL certificate error</center>
7<hr><center>nginx/1.24.0 (Ubuntu)</center>
8</body>
9</html>

因为用于签发客户端 client1.key, client1.crt 的 CA 证书未配置在 nginx.conf 中,也就是说它们的证书不被 Nginx 信任

如果把 ssl_client_certificate 换成 /certs/client-ca.crt
 1server {
 2    listen 443 ssl;
 3    server_name server.local;
 4    ssl_certificate /certs/server.crt;
 5    ssl_certificate_key /certs/server.key;
 6
 7    ssl_verify_client on;
 8
 9    ssl_client_certificate /certs/client-ca.crt;
10}

就能用 curl 正常访问了
1$
2curl --cert client1.crt --key client1.key https://server.local -k
3Ok

因为 client1.crt 是用 /certs/client-ca.crt 签发的,所以 client1.crt 是被信任的。

配置项 ssl_client_certificate 看上去就是要配置一个客户端证书,其实不对,而是要一个用于签发客户端证书的 CA 的证书。

假如生成更多的 client key  和证书,如  client2.key 和  client2.crt
1openssl req -newkey rsa:2048 -nodes -keyout client2.key -out client2.csr \
2  -subj "/C=US/ST=Iowa/L=Iowa/O=Company 2/CN=client2"
3
4openssl x509 -req -in client2.csr -out client2.crt -CA client-ca.crt -CAkey client-ca.key -days 365

由于 client2.crt 也是由 client-ca 签发的,所以不用改 nginx.conf 配置,下面 curl 换成了 client2.crt, client2.key 后也没问题
1$ curl --cert client2.crt --key client2.key https://server.local -k
2Ok

如果再有新 CA 签发的客户端证书,如
1openssl req -x509 -newkey rsa:2048 -nodes -keyout client-x-ca.key -out client-x-ca.crt  \
2  -subj "/C=US/ST=New York/L=New York/O=ClientX CA Company/CN=ClientXCA"
3
4openssl req -newkey rsa:2048 -nodes -keyout client3.key -out client3.csr \
5  -subj "/C=US/ST=Iowa/L=Iowa/O=Company 3/CN=client3"
6
7openssl x509 -req -in client3.csr -out client3.crt -CA client-x-ca.crt -CAkey client-x-ca.key -days 365

生成的 client-x-ca.key, client-x-ca.crt, client3.key, client3.crt

若要让 curl --cert client3.crt --key client3.key https://server.local -k 可工作的话,则可以用 ssl_trusted_certificate 补上用于签下 client3 证书的 CA 证书 client-x-ca.crt
 1server {
 2    listen 443 ssl;
 3    server_name server.local;
 4    ssl_certificate /certs/server.crt;
 5    ssl_certificate_key /certs/server.key;
 6
 7    ssl_verify_client on;
 8
 9    ssl_client_certificate /certs/client-ca.crt;
10    ssl_trusted_certificate /certs/client-x-ca.crt;
11}

再有更多 CA  签发的客户端证书,字面上的光靠 ssl_client_certificate 和  ssl_trusted_certificate 似乎无能为力了,因为它们都是不可重复的属性。

但是一个 *.crt 文件中可以包含多个证书的内容啊,因此我们需要把多个 CA 的证书合并成一个证书文件,即要让 ssl_trusted_certificate 指定的文件中含多个 CA 证书的内容。下面是简单的合并证书内容的命令
cat client_a-ca.crt client_b-ca.crt client_c-ca.crt > other-clients-ca.crt
合并后,other-clients-ca.crt 中的内容就是下面的格式
1-----BEGIN CERTIFICATE-----
2<client1-ca-content>
3-----END CERTIFICATE-----
4-----BEGIN CERTIFICATE-----
5<client2-ca-content>
6-----END CERTIFICATE-----
7-----BEGIN CERTIFICATE-----
8<client3-ca-content>
9-----END CERTIFICATE-----

再用 ssl_trusted_certificate 指定为该含有多个 CA 证书的文件
1    ssl_trusted_certificate /certs/other-clients-ca.crt;

该做法实质用 ssl_trusted_certificate 配置的是一个客户端证书信任链,服务端就能会信任 ssl_client_certificate 和 ssl_trusted_certificate 包含的所有 CA 签发的客户端证书。

附:如何让 curl 信任证书

默认是 curl 也是只信任权威的证书,所以对于自签发证书配置的配置端必须用 -k 来强自任凭,没有 -k 就是

1curl --cert client1.crt --key client1.key https://server.local
2curl: (60) SSL certificate problem: unable to get local issuer certificate
3More details here: https://curl.se/docs/sslcerts.html
4
5curl failed to verify the legitimacy of the server and therefore could not
6establish a secure connection to it. To learn more about this situation and
7how to fix it, please visit the web page mentioned above.

那么如何不用 -k  参数还能获取正常响应结果呢?有以下几种办法

--cacert 参数,相当于浏览器导入了第三方可信任证书
1curl --cert client1.crt --key client1.key --cacert server.crt https://server.local
2Ok
3
4curl --cert client1.crt --key client1.key --cacert server-ca.crt https://server.local
5Ok

即信任服务端 server.crt 证书或用于签发 server.crt 的 CA 证书。信任某个证书的话也必须信任由此证书签发的其他证书,即信任老子就要无条件信任小子。

通过环境变量替换 --cacert 参数
1export CURL_CA_BUNDLE=server.crt
2curl --cert client1.crt --key client1.key  https://server.local
3Ok
4
5export CURL_CA_BUNDLE=server-ca.crt
6curl --cert client1.crt --key client1.key  https://server.local
7Ok

与前面等效。还有一个  --capath  指定目录,用起来稍显复杂,不作深入了。

还可以用 Trust Store, 如 macOS 的 Keychain Access, Windows 的 Certificates, Debian/Ubuntu 的 /etc/ssl/certs/ca-certificates.crt, 和 RHEL/CentOS  的文件 /etc/pki/tls/certs/ca-bundle.crt。

以 Ubuntu 为例,把欲信任的证书内容附加到 /etc/ssl/certs/ca-certificates.crt 后就可以免去 -k, 操作
1# cat server-ca.crt >> /etc/ssl/certs/ca-certificates.crt
2# curl --cert client1.crt --key client1.key https://server.local
3Ok

假设 server-ca.crt 在当前目录中。

对于 Java 程序或很多地方也都有 Trust Store 这个概念,可以顺着这个思路,进行信任某个 CA 所签发的证书。 永久链接 https://yanbin.blog/client-certs-with-different-ca-nginx-mtls-config/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。