配置客户端与 APISIX 之间的双向认证(mTLS)

mTLS 是一种双向身份认证的方式。如果在你的网络环境中,要求只有受信任的客户端才可以访问服务端,那么可以启用 mTLS 来验证客户端的身份,保证服务端 API 的安全。本文主要介绍了如何配置客户端与 Apache APISIX 之间的双向认证(mTLS)。

配置

本示例包含以下过程:

  1. 生成证书;
  2. 在 APISIX 中配置证书;
  3. 在 APISIX 中创建并配置路由;
  4. 测试验证。

为了使测试结果更加清晰,本文提到的示例会向上游传递一些有关客户端证书的信息,其中包括:serialfingerprintcommon name

生成证书

我们需要生成三个测试证书,分别是根证书、服务器证书、客户端证书。只需通过以下命令,就可以通过 OpenSSL 生成我们需要的测试证书。

  1. # 根证书
  2. openssl genrsa -out ca.key 2048
  3. openssl req -new -sha256 -key ca.key -out ca.csr -subj "/CN=ROOTCA"
  4. openssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.cer
  5. # 服务器证书
  6. openssl genrsa -out server.key 2048
  7. # 注意:CN 值中的 `test.com` 为我们要测试的域名/主机名。
  8. openssl req -new -sha256 -key server.key -out server.csr -subj "/CN=test.com"
  9. openssl x509 -req -days 36500 -sha256 -extensions v3_req -CA ca.cer -CAkey ca.key -CAserial ca.srl -CAcreateserial -in server.csr -out server.cer
  10. # 客户端证书
  11. openssl genrsa -out client.key 2048
  12. openssl req -new -sha256 -key client.key -out client.csr -subj "/CN=CLIENT"
  13. openssl x509 -req -days 36500 -sha256 -extensions v3_req -CA ca.cer -CAkey ca.key -CAserial ca.srl -CAcreateserial -in client.csr -out client.cer
  14. # 将客户端证书转换为 pkcs12 供 Windows 使用(可选)
  15. openssl pkcs12 -export -clcerts -in client.cer -inkey client.key -out client.p12

在 APISIX 中配置证书

使用 curl 命令请求 APISIX Admin API 创建一个 SSL 资源并指定 SNI。

配置客户端与 APISIX 之间的双向认证(mTLS) - 图1注意

证书中的换行需要替换为其转义字符 \n

  1. curl -X PUT 'http://127.0.0.1:9180/apisix/admin/ssls/1' \
  2. --header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  3. --header 'Content-Type: application/json' \
  4. --data-raw '{
  5. "sni": "test.com",
  6. "cert": "<服务器证书>",
  7. "key": "<服务器证书私钥>",
  8. "client": {
  9. "ca": "<客户端证书公钥>"
  10. }
  11. }'
  • sni:指定证书的域名(CN),当客户端尝试通过 TLS 与 APISIX 握手时,APISIX 会将 ClientHello 中的 SNI 数据与该字段进行匹配,找到对应的服务器证书进行握手。
  • cert:服务器证书。
  • key:服务器证书的私钥。
  • client.ca:用来验证客户端证书的 CA 文件。为了演示方便,这里使用了同一个 CA

配置测试路由

使用 curl 命令请求 APISIX Admin API 创建一个路由。

  1. curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/1' \
  2. --header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  3. --header 'Content-Type: application/json' \
  4. --data-raw '{
  5. "uri": "/anything",
  6. "plugins": {
  7. "proxy-rewrite": {
  8. "headers": {
  9. "X-Ssl-Client-Fingerprint": "$ssl_client_fingerprint",
  10. "X-Ssl-Client-Serial": "$ssl_client_serial",
  11. "X-Ssl-Client-S-DN": "$ssl_client_s_dn"
  12. }
  13. }
  14. },
  15. "upstream": {
  16. "nodes": {
  17. "httpbin.org":1
  18. },
  19. "type":"roundrobin"
  20. }
  21. }'

APISIX 会根据 SNI 和上一步创建的 SSL 资源自动处理 TLS 握手,所以我们不需要在路由中指定主机名(但也可以显式地指定主机名)。

另外,上面 curl 命令中,我们启用了 proxy-rewrite 插件,它将动态地更新请求头的信息,示例中变量值的来源是 NGINX 变量,你可以在这里找到它们:http://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables。

测试验证

由于我们使用域名 test.com 作为测试域名,在开始验证之前,我们必须先将测试域名添加到你的 DNS 或者本地的 hosts 文件中。

  1. 如果我们不使用 hosts,只是想测试一下结果,那么你可以使用下面的命令直接进行测试:
  1. curl --resolve "test.com:9443:127.0.0.1" https://test.com:9443/anything -k --cert ./client.cer --key ./client.key
  1. 如果你需要修改 hosts,请阅读下面示例(以 Ubuntu 为例):
  • 修改 /etc/hosts 文件

    1. # 127.0.0.1 localhost
    2. 127.0.0.1 test.com
  • 验证测试域名是否生效

    1. ping test.com
    2. PING test.com (127.0.0.1) 56(84) bytes of data.
    3. 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=64 time=0.028 ms
    4. 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=64 time=0.037 ms
    5. 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=3 ttl=64 time=0.036 ms
    6. 64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=4 ttl=64 time=0.031 ms
    7. ^C
    8. --- test.com ping statistics ---
    9. 4 packets transmitted, 4 received, 0% packet loss, time 3080ms
    10. rtt min/avg/max/mdev = 0.028/0.033/0.037/0.003 ms
  • 测试

    1. curl https://test.com:9443/anything -k --cert ./client.cer --key ./client.key

    然后你将收到下面的响应体:

    1. {
    2. "args": {},
    3. "data": "",
    4. "files": {},
    5. "form": {},
    6. "headers": {
    7. "Accept": "*/*",
    8. "Host": "test.com",
    9. "User-Agent": "curl/7.81.0",
    10. "X-Amzn-Trace-Id": "Root=1-63256343-17e870ca1d8f72dc40b2c5a9",
    11. "X-Forwarded-Host": "test.com",
    12. "X-Ssl-Client-Fingerprint": "c1626ce3bca723f187d04e3757f1d000ca62d651",
    13. "X-Ssl-Client-S-Dn": "CN=CLIENT",
    14. "X-Ssl-Client-Serial": "5141CC6F5E2B4BA31746D7DBFE9BA81F069CF970"
    15. },
    16. "json": null,
    17. "method": "GET",
    18. "origin": "127.0.0.1",
    19. "url": "http://test.com/anything"
    20. }

由于我们在示例中配置了 proxy-rewrite 插件,我们可以看到响应体中包含上游收到的请求体,包含了正确数据。

基于对 URI 正则表达式匹配,绕过 MTLS

APISIX 允许配置 URI 白名单以便绕过 MTLS。如果请求的 URI 在白名单内,客户端证书将不被检查。注意,如果针对白名单外的 URI 发请求,而该请求缺乏客户端证书或者提供了非法客户端证书,会得到 HTTP 400 响应,而不是在 SSL 握手阶段被拒绝。

时序图

skip mtls

例子

  1. 配置路由和证书
  1. curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  2. -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
  3. {
  4. "uri": "/*",
  5. "upstream": {
  6. "nodes": {
  7. "httpbin.org": 1
  8. }
  9. }
  10. }'
  11. curl http://127.0.0.1:9180/apisix/admin/ssls/1 \
  12. -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
  13. {
  14. "cert": "'"$(<t/certs/mtls_server.crt)"'",
  15. "key": "'"$(<t/certs/mtls_server.key)"'",
  16. "snis": [
  17. "*.apisix.dev"
  18. ],
  19. "client": {
  20. "ca": "'"$(<t/certs/mtls_ca.crt)"'",
  21. "depth": 10,
  22. "skip_mtls_uri_regex": [
  23. "/anything.*"
  24. ]
  25. }
  26. }'
  27. 2. 如果没提供客户端证书,而 URI 又不在白名单内,会得到 HTTP 400 响应。
  28. ```bash
  29. curl https://admin.apisix.dev:9443/uuid -v \
  30. --resolve 'admin.apisix.dev:9443:127.0.0.1' --cacert t/certs/mtls_ca.crt
  31. * Added admin.apisix.dev:9443:127.0.0.1 to DNS cache
  32. * Hostname admin.apisix.dev was found in DNS cache
  33. * Trying 127.0.0.1:9443...
  34. * TCP_NODELAY set
  35. * Connected to admin.apisix.dev (127.0.0.1) port 9443 (#0)
  36. * ALPN, offering h2
  37. * ALPN, offering http/1.1
  38. * successfully set certificate verify locations:
  39. * CAfile: t/certs/mtls_ca.crt
  40. CApath: /etc/ssl/certs
  41. * TLSv1.3 (OUT), TLS handshake, Client hello (1):
  42. * TLSv1.3 (IN), TLS handshake, Server hello (2):
  43. * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
  44. * TLSv1.3 (IN), TLS handshake, Request CERT (13):
  45. * TLSv1.3 (IN), TLS handshake, Certificate (11):
  46. * TLSv1.3 (IN), TLS handshake, CERT verify (15):
  47. * TLSv1.3 (IN), TLS handshake, Finished (20):
  48. * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
  49. * TLSv1.3 (OUT), TLS handshake, Certificate (11):
  50. * TLSv1.3 (OUT), TLS handshake, Finished (20):
  51. * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
  52. * ALPN, server accepted to use h2
  53. * Server certificate:
  54. * subject: C=cn; ST=GuangDong; L=ZhuHai; CN=admin.apisix.dev; OU=ops
  55. * start date: Dec 1 10:17:24 2022 GMT
  56. * expire date: Aug 18 10:17:24 2042 GMT
  57. * subjectAltName: host "admin.apisix.dev" matched cert's "admin.apisix.dev"
  58. * issuer: C=cn; ST=GuangDong; L=ZhuHai; CN=ca.apisix.dev; OU=ops
  59. * SSL certificate verify ok.
  60. * Using HTTP2, server supports multi-use
  61. * Connection state changed (HTTP/2 confirmed)
  62. * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
  63. * Using Stream ID: 1 (easy handle 0x56246de24e30)
  64. > GET /uuid HTTP/2
  65. > Host: admin.apisix.dev:9443
  66. > user-agent: curl/7.68.0
  67. > accept: */*
  68. >
  69. * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
  70. * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
  71. * old SSL session ID is stale, removing
  72. * Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
  73. < HTTP/2 400
  74. < date: Fri, 21 Apr 2023 07:53:23 GMT
  75. < content-type: text/html; charset=utf-8
  76. < content-length: 229
  77. < server: APISIX/3.2.0
  78. <
  79. <html>
  80. <head><title>400 Bad Request</title></head>
  81. <body>
  82. <center><h1>400 Bad Request</h1></center>
  83. <hr><center>openresty</center>
  84. <p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body>
  85. </html>
  86. * Connection #0 to host admin.apisix.dev left intact
  87. 3. 虽然没提供客户端证书,但是 URI 在白名单内,请求会被成功处理和响应。
  88. ```bash
  89. curl https://admin.apisix.dev:9443/anything/foobar -i \
  90. --resolve 'admin.apisix.dev:9443:127.0.0.1' --cacert t/certs/mtls_ca.crt
  91. HTTP/2 200
  92. content-type: application/json
  93. content-length: 416
  94. date: Fri, 21 Apr 2023 07:58:28 GMT
  95. access-control-allow-origin: *
  96. access-control-allow-credentials: true
  97. server: APISIX/3.2.0
  98. ...

总结

想了解更多有关 Apache APISIX 的 mTLS 功能介绍,可以阅读:TLS 双向认证