证书

APISIX 支持通过 TLS 扩展 SNI 实现加载特定的 SSL 证书以实现对 https 的支持。

SNI(Server Name Indication)是用来改善 SSL 和 TLS 的一项特性,它允许客户端在服务器端向其发送证书之前向服务器端发送请求的域名,服务器端根据客户端请求的域名选择合适的 SSL 证书发送给客户端。

单一域名指定

通常情况下一个 SSL 证书只包含一个静态域名,配置一个 ssl 参数对象,它包括 certkeysni三个属性,详细如下:

  • cert:SSL 密钥对的公钥,pem 格式
  • key:SSL 密钥对的私钥,pem 格式
  • snis:SSL 证书所指定的一个或多个域名,注意在设置这个参数之前,你需要确保这个证书对应的私钥是有效的。

为了简化示例,我们会使用下面的 Python 脚本:

create-ssl.py

  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. import sys
  4. # sudo pip install requests
  5. import requests
  6. if len(sys.argv) <= 3:
  7. print("bad argument")
  8. sys.exit(1)
  9. with open(sys.argv[1]) as f:
  10. cert = f.read()
  11. with open(sys.argv[2]) as f:
  12. key = f.read()
  13. sni = sys.argv[3]
  14. api_key = "edd1c9f034335f136f87ad84b625c8f1"
  15. resp = requests.put("http://127.0.0.1:9180/apisix/admin/ssls/1", json={
  16. "cert": cert,
  17. "key": key,
  18. "snis": [sni],
  19. }, headers={
  20. "X-API-KEY": api_key,
  21. })
  22. print(resp.status_code)
  23. print(resp.text)
  1. # 创建 SSL 对象
  2. ./create-ssl.py t.crt t.key test.com
  3. # 创建 Router 对象
  4. curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
  5. {
  6. "uri": "/hello",
  7. "hosts": ["test.com"],
  8. "methods": ["GET"],
  9. "upstream": {
  10. "type": "roundrobin",
  11. "nodes": {
  12. "127.0.0.1:1980": 1
  13. }
  14. }
  15. }'
  16. # 测试一下
  17. curl --resolve 'test.com:9443:127.0.0.1' https://test.com:9443/hello -vvv
  18. * Added test.com:9443:127.0.0.1 to DNS cache
  19. * About to connect() to test.com port 9443 (#0)
  20. * Trying 127.0.0.1...
  21. * Connected to test.com (127.0.0.1) port 9443 (#0)
  22. * Initializing NSS with certpath: sql:/etc/pki/nssdb
  23. * skipping SSL peer certificate verification
  24. * SSL connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  25. * Server certificate:
  26. * subject: CN=test.com,O=iresty,L=ZhuHai,ST=GuangDong,C=CN
  27. * start date: Jun 24 22:18:05 2019 GMT
  28. * expire date: May 31 22:18:05 2119 GMT
  29. * common name: test.com
  30. * issuer: CN=test.com,O=iresty,L=ZhuHai,ST=GuangDong,C=CN
  31. > GET /hello HTTP/1.1
  32. > User-Agent: curl/7.29.0
  33. > Host: test.com:9443
  34. > Accept: */*

泛域名

一个 SSL 证书的域名也可能包含泛域名,如 *.test.com,它代表所有以 test.com 结尾的域名都可以使用该证书。 比如 *.test.com,可以匹配 www.test.commail.test.com

看下面这个例子,请注意我们把 *.test.com 作为 sni 传递进来:

  1. ./create-ssl.py t.crt t.key '*.test.com'
  2. curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
  3. {
  4. "uri": "/hello",
  5. "hosts": ["*.test.com"],
  6. "methods": ["GET"],
  7. "upstream": {
  8. "type": "roundrobin",
  9. "nodes": {
  10. "127.0.0.1:1980": 1
  11. }
  12. }
  13. }'
  14. # 测试一下
  15. curl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/hello -vvv
  16. * Added test.com:9443:127.0.0.1 to DNS cache
  17. * About to connect() to test.com port 9443 (#0)
  18. * Trying 127.0.0.1...
  19. * Connected to test.com (127.0.0.1) port 9443 (#0)
  20. * Initializing NSS with certpath: sql:/etc/pki/nssdb
  21. * skipping SSL peer certificate verification
  22. * SSL connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  23. * Server certificate:
  24. * subject: CN=test.com,O=iresty,L=ZhuHai,ST=GuangDong,C=CN
  25. * start date: Jun 24 22:18:05 2019 GMT
  26. * expire date: May 31 22:18:05 2119 GMT
  27. * common name: test.com
  28. * issuer: CN=test.com,O=iresty,L=ZhuHai,ST=GuangDong,C=CN
  29. > GET /hello HTTP/1.1
  30. > User-Agent: curl/7.29.0
  31. > Host: test.com:9443
  32. > Accept: */*

多域名的情况

如果一个 SSL 证书包含多个独立域名,比如 www.test.commail.test.com, 你可以把它们都放入 snis 数组中,就像这样:

  1. {
  2. "snis": ["www.test.com", "mail.test.com"]
  3. }

单域名多证书的情况

如果你期望为一个域名配置多张证书,例如以此来同时支持使用 ECC 和 RSA 的密钥交换算法,那么你可以将额外的证书和私钥(第一张证书和其私钥依然使用 certkey)配置在 certskeys 中。

  • certs:PEM 格式的 SSL 证书列表
  • keys:PEM 格式的 SSL 证书私钥列表

APISIX 会将相同下标的证书和私钥配对使用,因此 certskeys 列表的长度必须一致。

设置多个 CA 证书

APISIX 目前支持在多处设置 CA 证书,比如 保护 Admin API 等。

在这些地方,使用 ssl_trusted_certificatetrusted_ca_cert 来配置 CA 证书,但是这些配置最终将转化为 OpenResty 的 lua_ssl_trusted_certificate 指令。

如果你需要在不同的地方指定不同的 CA 证书,你可以将这些 CA 证书制作成一个 CA bundle 文件,在需要用到 CA 证书的地方将配置指向这个文件。这样可以避免生成的 lua_ssl_trusted_certificate 存在多处并且互相覆盖的问题。

下面用一个完整的例子来展示如何在 APISIX 设置多个 CA 证书。

假设让 client 与 APISIX Admin API,APISIX 与 ETCD 之间都使用 mTLS 协议进行通信,目前有两张 CA 证书,分别是 foo_ca.crtbar_ca.crt,用这两张 CA 证书各自签发 client 与 server 证书对,foo_ca.crt 及其签发的证书对用于保护 Admin API,bar_ca.crt 及其签发的证书对用于保护 ETCD。

下表详细列出这个示例所涉及到的配置及其作用:

配置类型用途
foo_ca.crtCA 证书签发客户端与 APISIX Admin API 进行 mTLS 通信所需的次级证书。
foo_client.crt证书foo_ca.crt 签发,客户端使用,访问 APISIX Admin API 时证明自身身份的证书。
foo_client.key密钥文件foo_ca.crt 签发,客户端使用,访问 APISIX Admin API 所需的密钥文件。
foo_server.crt证书foo_ca.crt 签发,APISIX 使用,对应 admin_api_mtls.admin_ssl_cert 配置项。
foo_server.key密钥文件foo_ca.crt 签发,APISIX 使用,对应 admin_api_mtls.admin_ssl_cert_key 配置项。
admin.apisix.dev域名签发 foo_server.crt 证书时使用的 Common Name,客户端通过该域名访问 APISIX Admin API
bar_ca.crtCA 证书签发 APISIX 与 ETCD 进行 mTLS 通信所需的次级证书。
bar_etcd.crt证书bar_ca.crt 签发,ETCD 使用,对应 ETCD 启动命令中的 —cert-file 选项。
bar_etcd.key密钥文件bar_ca.crt 签发,ETCD 使用,对应 ETCD 启动命令中的 —key-file 选项。
bar_apisix.crt证书bar_ca.crt 签发,APISIX 使用,对应 etcd.tls.cert 配置项。
bar_apisix.key密钥文件bar_ca.crt 签发,APISIX 使用,对应 etcd.tls.key 配置项。
etcd.cluster.dev域名签发 bar_etcd.crt 证书时使用的 Common Name,APISIX 与 ETCD 进行 mTLS 通信时,使用该域名作为 SNI。对应 etcd.tls.sni 配置项。
apisix.ca-bundleCA bundlefoo_ca.crtbar_ca.crt 合并而成,替代 foo_ca.crtbar_ca.crt
  1. 制作 CA bundle 文件
  1. cat /path/to/foo_ca.crt /path/to/bar_ca.crt > apisix.ca-bundle
  1. 启动 ETCD 集群,并开启客户端验证

先编写 goreman 配置,命名为 Procfile-single-enable-mtls,内容如下:

  1. # 运行 `go get github.com/mattn/goreman` 安装 goreman,用 goreman 执行以下命令:
  2. etcd1: etcd --name infra1 --listen-client-urls https://127.0.0.1:12379 --advertise-client-urls https://127.0.0.1:12379 --listen-peer-urls http://127.0.0.1:12380 --initial-advertise-peer-urls http://127.0.0.1:12380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle
  3. etcd2: etcd --name infra2 --listen-client-urls https://127.0.0.1:22379 --advertise-client-urls https://127.0.0.1:22379 --listen-peer-urls http://127.0.0.1:22380 --initial-advertise-peer-urls http://127.0.0.1:22380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle
  4. etcd3: etcd --name infra3 --listen-client-urls https://127.0.0.1:32379 --advertise-client-urls https://127.0.0.1:32379 --listen-peer-urls http://127.0.0.1:32380 --initial-advertise-peer-urls http://127.0.0.1:32380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle

使用 goreman 来启动 ETCD 集群:

  1. goreman -f Procfile-single-enable-mtls start > goreman.log 2>&1 &
  1. 更新 config.yaml

conf/config.yaml

  1. deployment:
  2. admin:
  3. admin_key
  4. - name: admin
  5. key: edd1c9f034335f136f87ad84b625c8f1
  6. role: admin
  7. admin_listen:
  8. ip: 127.0.0.1
  9. port: 9180
  10. https_admin: true
  11. admin_api_mtls:
  12. admin_ssl_ca_cert: /path/to/apisix.ca-bundle
  13. admin_ssl_cert: /path/to/foo_server.crt
  14. admin_ssl_cert_key: /path/to/foo_server.key
  15. apisix:
  16. ssl:
  17. ssl_trusted_certificate: /path/to/apisix.ca-bundle
  18. deployment:
  19. role: traditional
  20. role_traditional:
  21. config_provider: etcd
  22. etcd:
  23. host:
  24. - "https://127.0.0.1:12379"
  25. - "https://127.0.0.1:22379"
  26. - "https://127.0.0.1:32379"
  27. tls:
  28. cert: /path/to/bar_apisix.crt
  29. key: /path/to/bar_apisix.key
  30. sni: etcd.cluster.dev
  1. 测试 Admin API

启动 APISIX,如果 APISIX 启动成功,logs/error.log 中没有异常输出,表示 APISIX 与 ETCD 之间进行 mTLS 通信正常。

用 curl 模拟客户端,与 APISIX Admin API 进行 mTLS 通信,并创建一条路由:

  1. curl -vvv \
  2. --resolve 'admin.apisix.dev:9180:127.0.0.1' https://admin.apisix.dev:9180/apisix/admin/routes/1 \
  3. --cert /path/to/foo_client.crt \
  4. --key /path/to/foo_client.key \
  5. --cacert /path/to/apisix.ca-bundle \
  6. -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
  7. {
  8. "uri": "/get",
  9. "upstream": {
  10. "type": "roundrobin",
  11. "nodes": {
  12. "httpbin.org:80": 1
  13. }
  14. }
  15. }'

如果输出以下 SSL 握手过程,表示 curl 与 APISIX Admin API 之间 mTLS 通信成功:

  1. * TLSv1.3 (OUT), TLS handshake, Client hello (1):
  2. * TLSv1.3 (IN), TLS handshake, Server hello (2):
  3. * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
  4. * TLSv1.3 (IN), TLS handshake, Request CERT (13):
  5. * TLSv1.3 (IN), TLS handshake, Certificate (11):
  6. * TLSv1.3 (IN), TLS handshake, CERT verify (15):
  7. * TLSv1.3 (IN), TLS handshake, Finished (20):
  8. * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
  9. * TLSv1.3 (OUT), TLS handshake, Certificate (11):
  10. * TLSv1.3 (OUT), TLS handshake, CERT verify (15):
  11. * TLSv1.3 (OUT), TLS handshake, Finished (20):
  12. * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
  1. 验证 APISIX 代理
  1. curl http://127.0.0.1:9080/get -i
  2. HTTP/1.1 200 OK
  3. Content-Type: application/json
  4. Content-Length: 298
  5. Connection: keep-alive
  6. Date: Tue, 26 Jul 2022 16:31:00 GMT
  7. Access-Control-Allow-Origin: *
  8. Access-Control-Allow-Credentials: true
  9. Server: APISIX/2.14.1
  10. ……

APISIX 将请求代理到了上游 httpbin.org/get 路径,并返回了 HTTP/1.1 200 OK。整个过程使用 CA bundle 替代 CA 证书是正常可用的。