有效的 SSL/TLS 证书是现代应用环境的核心要求。但遗憾的是,在部署应用时,证书的更新管理很容易被忽略。证书的有效期有限且时长不等,DigiCert 证书的有效期在 13 个月左右,Let’s Encrypt 证书的有效期为 90 天。为了确保安全访问,这些证书需要在到期前更新/重新颁发。由于大多数运维团队的工作任务繁重,证书更新有时会被搁置,因此在证书临近(或更糟糕)超过到期日时,手忙脚乱地管理证书的现象时常发生。
其实,情况大可不必如此。只需做好一些规划和准备,就可以简化并实现证书管理的自动化。在这里,我们将介绍一款解决方案,面向使用以下三种技术的 Kubernetes 环境:
- Jetstack 的 cert-manager
- Let’s Encrypt
- NGINX Ingress Controller
通过本文,您将了解如何通过向端点提供自动更新的证书来简化证书管理。
Kubernetes 环境中的证书
在讨论技术细节之前,我们需要先定义一些术语。“TLS 证书”是指在我们的 Ingress controller(Ingress 控制器)上启用 HTTPS 连接所需的两个组件:
- 证书
- 私钥
证书和私钥都由 Let’s Encrypt 颁发。关于 TLS 证书如何工作的详细说明,请参阅 DigiCert 文章《TLS/SSL 证书如何工作》。
在 Kubernetes 中,这两个组件被存储为 Secrets。Kubernetes 工作负载,例如 NGINX Ingress Controller 和 cert-manager 可以写入和读取这些 Secret,也可以由有权访问 Kubernetes 安装的用户进行管理。
cert-manager 简介
cert-manager 项目是一个可以与 Kubernetes 和 OpenShift 协同工作的证书控制器。当部署在 Kubernetes 中时,cert-manager 将自动颁发 Ingress controller 所需的证书,并确保它们是有效和最新的。此外,它将跟踪证书的到期日,并按照配置的时间间隔尝试更新。虽然它与大量公共和私有证书颁发机构合作,但在这里我们将只介绍它与 Let’s Encrypt 的集成。
两种 challenge 类型
当使用 Let’s Encrypt 时,所有证书管理都可以自动进行。虽然这提供了极大的便利,但也带来了一个问题:这项服务如何确保您拥有完全限定域名 (FQDN)?
这个问题可使用 challenge 来解决,challenge 会要求您回答一个仅有权访问特定域名的 DNS 记录的人员才能发起的验证请求。challenge 采取以下两种形式中的一种:
- HTTP-01:可通过为要将证书颁发给的 FQDN 创建 DNS 记录来解决该 challenge。例如,如果您的服务器位于 IP www.xxx.yyy.zzz,并且您的 FQDN 是 cert.example.com,那么该 challenge 机制将在位于 www.xxx.yyy.zzz 的服务器上暴露一个令牌,Let’s Encrypt 服务器将试图通过 cert.example.com 访问该令牌。如果成功,challenge 通过并颁发证书。
HTTP-01 是生成证书的最简单方法,因为它不需要直接访问 DNS 提供商。这种类型的 challenge 总是通过 HTTP 80 支持的DNS 端口 进行。注意,当使用 HTTP-01 challenge 时,cert-manager 将利用 Ingress controller 来提供 challenge 令牌。
- DNS-01:这个 challenge 使用令牌创建 DNS TXT 记录,然后由颁发机构进行验证。如果令牌被认可,则证明您拥有对该域名的所有权,并且可以立即为其记录颁发证书。与 HTTP-01 challenge 不同的是,使用 DNS-01 challenge 时,FQDN 不需要解析服务器的 IP 地址(甚至不存在)。此外,当 80 端口被屏蔽时,可以使用 DNS-01。不方便的是需要通过访问 cert-manager 安装的 API 令牌提供对 DNS 基础设施的访问权限。
Ingress Controllers
Ingress controller 是 Kubernetes 的一种 service,它被专门用于从集群外部引入流量,将流量负载均衡到内部 Pods(一组单个或多个容器),并管理 egress 出向流量。此外,Ingress controller 通过 Kubernetes API 进行控制,当添加、移除 Pods 或 Pods 出现故障时,它将监控并更新负载均衡配置。
有关 Ingress controller 的更多信息,请参阅以下博客:
在下面的示例中,我们将使用由 F5 NGINX 开发和维护的 NGINX Ingress Controller。
证书管理 示例
这些示例假定您拥有一个可供测试使用的有效 Kubernetes 安装,并且该安装可以分配一个外部 IP 地址(Kubernetes LoadBalancer 对象)。此外,它还假定您可以在 80 和443端口(如果使用 HTTP-01 challenge)或只在 443 端口(如果使用 DNS-01 challenge)上接收流量。这些示例都使用 Mac OS X 进行演示,但也可以使用 Linux 或 WSL。
您还需要一个 DNS 提供商以及允许调整 A 记录的 FQDN。如果您使用 HTTP-01 challenge,则只需要添加一个 A 记录(或让人代您添加一个)。如果您使用 DNS-01 challenge,则需要能够对支持的 DNS 提供商或支持的 webhook 提供商进行 API 访问。
部署 NGINX Ingress Controller
最简单的方法是通过 Helm 进行部署。这种部署允许您同时使用 Kubernetes Ingress 和 NGINX Virtual Server CRD。
- 添加 NGINX 仓库。
- 更新仓库。
- 部署 Ingress controller。
- 检查部署,并检索面向 Ingress controller 的 egress 的 IP 地址。注意,如果没有有效的 IP 地址,则无法继续。
$ helm repo add nginx-stable https://helm.nginx.com/stable
"nginx-stable" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "nginx-stable" chart repository
Update Complete. ⎈Happy Helming!⎈
$ helm install nginx-kic nginx-stable/nginx-ingress \
--namespace nginx-ingress --set controller.enableCustomResources=true \
--create-namespace --set controller.enableCertManager=true
NAME: nginx-kic
LAST DEPLOYED: Thu Sep 1 15:58:15 2022
NAMESPACE: nginx-ingress
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The NGINX Ingress Controller has been installed.
$ kubectl get deployments --namespace nginx-ingress
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-kic-nginx-ingress 1/1 1 1 23s
$ kubectl get services --namespace nginx-ingress
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-kic-nginx-ingress LoadBalancer 10.128.60.190 www.xxx.yyy.zzz 80:31526/TCP,443:32058/TCP 30s
添加 DNS A 记录
具体流程将视您的 DNS 提供商而定。这个 DNS 名称需要能够从 Let’s Encrypt 服务器进行解析,在此之前,您可能需要等待记录传播。有关这方面的更多信息,请参阅 SiteGround 文章《什么是 DNS 传播,为什么需要这么长时间?》
如果能够解析所选择的 FQDN,则可以进入下一步。
$ host cert.example.com
cert.example.com has address www.xxx.yyy.zzz
部署 cert-manager
下一步是部署最新版本的 cert-manager。同样,我们将使用 Helm 进行部署。
- 添加 Helm 仓库。
- 更新仓库。
- 部署 cert-manager。
- 验证部署。
$ helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "nginx-stable" chart repository
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈Happy Helming!⎈
$ helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.9.1 --set installCRDs=true
NAME: cert-manager
LAST DEPLOYED: Thu Sep 1 16:01:52 2022
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.9.1 has been deployed successfully!
In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).
More information on the different types of issuers and how to configure them
can be found in our documentation:
https://cert-manager.io/docs/configuration/
For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:
https://cert-manager.io/docs/usage/ingress/
$ kubectl get deployments --namespace cert-manager
NAME READY UP-TO-DATE AVAILABLE AGE
cert-manager 1/1 1 1 4m30s
cert-manager-cainjector 1/1 1 1 4m30s
cert-manager-webhook 1/1 1 1 4m30s
部署 NGINX Cafe 示例
我们将使用 NGINX Cafe 示例来提供后端部署和 services。这个示例在 NGINX 提供的文档中十分常见。在此过程中,我们不会部署 Ingress。
- 克隆 NGINX Ingress GitHub 项目。
- 进入到示例目录。这个目录中包含几个示例,演示了 Ingress controller 的各种配置。我们使用的是 complete-example 目录下提供的示例。
- 部署 NGINX Cafe 示例。
- 使用
kubectl
get 命令验证部署和 services。您需要确保 Pods 显示为READY
,而 service 显示为running
。下面的示例为您提供了一个代表性样本。注意,kubernetes
服务是系统服务,与 NGINX Cafe 示例在同一个命名空间(默认)中运行。
$ git clone https://github.com/nginxinc/kubernetes-ingress.git
Cloning into 'kubernetes-ingress'...
remote: Enumerating objects: 44979, done.
remote: Counting objects: 100% (172/172), done.
remote: Compressing objects: 100% (108/108), done.
remote: Total 44979 (delta 87), reused 120 (delta 63), pack-reused 44807
Receiving objects: 100% (44979/44979), 60.27 MiB | 27.33 MiB/s, done.
Resolving deltas: 100% (26508/26508), done.
$ cd ./kubernetes-ingress/examples/ingress-resources/complete-example
$ kubectl apply -f ./cafe.yaml
deployment.apps/coffee created
service/coffee-svc created
deployment.apps/tea created
service/tea-svc created
$ kubectl get deployments,services --namespace default
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/coffee 2/2 2 2 69s
deployment.apps/tea 3/3 3 3 68s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/coffee-svc ClusterIP 10.128.154.225 <none> 80/TCP 68s
service/kubernetes ClusterIP 10.128.0.1 <none> 443/TCP 29m
service/tea-svc ClusterIP 10.128.96.145 <none> 80/TCP 68s
部署 ClusterIssuer
在 cert-manager 中,ClusterIssuer 可被用来颁发证书。这是一个集群范围对象,可以被任何命名空间引用,也可以被向指定证书颁发机构发起的任何证书请求所使用。在这个示例中,任何对 Let’s Encrypt 证书的请求都可以由这个 ClusterIssuer 来处理。
为选择的 challenge 类型部署 ClusterIssuer。虽然这不在本文的讨论范围内,但简单提一下,您可以通过高级配置选项在 ClusterIssuer 中指定多个解析器(根据选择器字段选择)。
ACME challenge 基础知识
可使用自动证书管理环境 (ACME) 协议来确定您是否拥有某个域名,进而确定能否被颁发 Let’s Encrypt 证书。对于这个 challenge,需要传递以下参数:
- metadata.name:ClusterIssuer 名称,在 Kubernetes 安装中需要是唯一的。这个名称将在后面的证书颁发示例中用到。
- spec.acme.email:这是您为生成证书而使用 Let’s Encrypt 注册的电子邮件地址。这应该是您的电子邮件。
- spec.acme.privateKeySecretRef:这是您将用来存储私钥的 Kubernetes secret 的名称。
- spec.acme.solvers:这应保持不变——它指出了您所使用的 challenge 类型(或者 ACME 所说的解析程序)(HTTP-01 或 DNS-01),以及它应该应用于哪种 Ingress 类型(在这种情况下,将是 nginx)。
使用 HTTP-01
这个示例显示了如何设置 ClusterIssuer 以使用 HTTP-01 challenge 来证明域名所有权和接收证书。
- 创建 ClusterIssuer,使用 HTTP-01 challenge。
- 验证该 ClusterIssuer(应该显示为 Ready)。
$ cat << EOF | kubectl apply -f
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: prod-issuer
spec:
acme:
email: example@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: prod-issuer-account-key
solvers:
- http01:
ingress:
class: nginx
EOF
clusterissuer.cert-manager.io/prod-issuer created
$ kubectl get clusterissuer
NAME READY AGE
prod-issuer True 34s
使用 DNS-01
这个示例显示了如何设置 ClusterIssuer 以使用 DNS-01 challenge 来验证域名所有权。根据具体的 DNS 提供商,您可能需要使用 Kubernetes Secret 来存储令牌。这个示例使用的是 Cloudflare。注意命名空间的使用。部署到 cert-manager 命名空间中的 cert-manager 应用需要访问 Secret。
在这个示例中,您需要一个 Cloudflare API 令牌,您可以从账户中创建该令牌。这将需要放在下面的 <API Token> 行中。如果您使用的不是 Cloudflare,则需要遵循提供商的文档。
- 为 API 令牌创建 Secret。
- 创建颁发机构,使用 DNS-01 challenge。
- 验证颁发机构(它应显示为 ready)。
$ cat << EOF | kubectl apply -f
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: <API Token>
EOF
$ cat << EOF | kubectl apply -f
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: prod-issuer
spec:
acme:
email: example@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: prod-issuer-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
EOF
$ kubectl get clusterissuer
NAME READY AGE
prod-issuer True 31m
部署 Ingress
完成上述步骤后,即可为应用部署 Ingress 资源。这将把流量路由到我们先前部署的 NGINX Cafe 应用中。
使用 Kubernetes Ingress
如果您使用标准的 Kubernetes Ingress 资源,则要使用以下部署 YAML 来配置 Ingress 并请求证书。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress
annotations:
cert-manager.io/cluster-issuer: prod-issuer
acme.cert-manager.io/http01-edit-in-place: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- cert.example.com
secretName: cafe-secret
rules:
- host: cert.example.com
http:
paths:
- path: /tea
pathType: Prefix
backend:
service:
name: tea-svc
port:
number: 80
- path: /coffee
pathType: Prefix
backend:
service:
name: coffee-svc
port:
number: 80
需要检查清单的一些关键部分:
- 被调用的 API 是标准 Kubernetes Ingress。
- 该配置的一个关键部分位于
metadata.annotations
下,在这里我们可以把acme.cert-manager.io/http01-edit-in-place
设置为 “true”。这个值是必填项,可以调整 challenge 的解决方式。如欲了解更多信息,请参阅支持的注释文档。这也可以通过使用 master/minion setup 来处理。 spec.ingressClassName
是指我们已安装并将使用的 NGINX Ingress Controller。spec.tls.secret
Kubernetes Secret 资源存储 Let’s Encrypt 颁发证书时所返回的证书密钥。- 我们的主机名
cert.example.com
是为spec.tls.hosts
和spec.rules.host
指定的。我们的 ClusterIssuer 就为这个主机名颁发证书。 spec.rules.http
部分定义了路径以及满足这些路径上的请求的后端 Services。例如,到/tea
的流量将被引导到tea-svc
上的 80 端口。
- 根据您的安装修改上述清单。至少需要更改
spec.rules.host
和spec.tls.hosts
的值,但建议您审查配置中的所有参数。 - 应用清单。
- 等待证书颁发,READY 字段的值会变为 “True”。
$ kubectl apply -f ./cafe-virtual-server.yaml
virtualserver.k8s.nginx.org/cafe created
$ kubectl get certificates
NAME READY SECRET AGE
certificate.cert-manager.io/cafe-secret True cafe-secret 37m
使用 NGINX 虚拟服务器/虚拟路由
如果您使用 NGINX CRD,则需要使用以下部署 YAML 来配置 Ingress。
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: cafe
spec:
host: cert.example.com
tls:
secret: cafe-secret
cert-manager:
cluster-issuer: prod-issuer
upstreams:
- name: tea
service: tea-svc
port: 80
- name: coffee
service: coffee-svc
port: 80
routes:
- path: /tea
action:
pass: tea
- path: /coffee
action:
pass: coffee
需要再次检查的一些关键部分。
- 被调用的 API 是面向 VirtualServer 资源的 NGINX 特定的 k8s.nginx.org/v1。
spec.tls.secret
Kubernetes Secret 资源存储 Let’s Encrypt 颁发证书时所返回的证书密钥。- 我们的主机名
cert.example.com
是为spec.host
指定的。我们的 ClusterIssuer 就为这个主机名颁发证书。 spec.upstreams
的值指向我们的后端服务,包括端口。spec.routes
定义了路由以及路由匹配时采取的操作。
- 根据您的安装修改上述清单。至少需要更改
spec.host
的值,但建议您审查配置中的所有参数。 - 应用清单。
- 等待证书颁发。您应该看到状态为 “Valid”。
$ kubectl apply -f ./cafe-virtual-server.yaml
virtualserver.k8s.nginx.org/cafe created
$ kubectl get VirtualServers
NAME STATE HOST IP PORTS AGE
cafe Valid cert.example.com www.xxx.yyy.zzz [80,443] 51m
查看证书
您可以通过 Kubernetes API 查看证书。这将显示有关该证书的细节,包括其大小和关联私钥。
$ kubectl describe secret cafe-secret
Name: cafe-secret
Namespace: default
Labels: <none>
Annotations: cert-manager.io/alt-names: cert.example.com
cert-manager.io/certificate-name: cafe-secret
cert-manager.io/common-name: cert.example.com
cert-manager.io/ip-sans:
cert-manager.io/issuer-group:
cert-manager.io/issuer-kind: ClusterIssuer
cert-manager.io/issuer-name: prod-issuer
cert-manager.io/uri-sans:Type: kubernetes.io/tlsData
====
tls.crt: 5607 bytes
tls.key: 1675 bytes
如要查看实际的证书和密钥,则可以运行以下命令。(注意:这揭露了 Kubernetes Secrets 的一个弱点。也就是说,它们可以被任何具有必要访问权限的人员读取)。
$ kubectl get secret cafe-secret -o yaml
测试 Ingress
测试证书。在这里您可以使用任何想用的方法。下面的示例使用了 cURL。如果成功,则会出现一个类似于前面所示的代码,其中包括服务器名称、服务器的内部地址、日期、选择的 URI(路由)(coffee 或 tea)以及请求 ID。如果失败,则会生成 HTTP 错误代码,最可能是 400 或 301。
$ curl https://cert.example.com/tea
Server address: 10.2.0.6:8080
Server name: tea-5c457db9-l4pvq
Date: 02/Sep/2022:15:21:06 +0000
URI: /tea
Request ID: d736db9f696423c6212ffc70cd7ebecf
$ curl https://cert.example.com/coffee
Server address: 10.2.2.6:8080
Server name: coffee-7c86d7d67c-kjddk
Date: 02/Sep/2022:15:21:10 +0000
URI: /coffee
Request ID: 4ea3aa1c87d2f1d80a706dde91f31d54
证书更新
刚开始,我们就提到这种方法将消除管理证书更新的需要。然而,我们尚未解释如何做到这一点。为什么呢?因为这是 cert-manager 的一个核心内置部分。在这个自动化过程中,当 cert-manager 意识到证书不存在、已过期、即将在 15 天内过期时,或者当用户通过 CLI 请求新的证书时,它将自动请求新的证书。简单至极。
常见问题
关于 NGINX Plus?
如果您是 NGINX Plus 用户,那么唯一的区别就是要安装 NGINX Ingress Controller。请参阅 NGINX 文档中的安装 Helm 部分,了解如何修改上述 Helm 命令以完成安装。
我应该使用哪种 challenge 类型?
这主要取决于您的用例。
HTTP-01 challenge 方法要求80 端口对互联网开放,并且 DNS A 记录已根据 Ingress controller 的 IP 地址正确配置。除了创建 A 记录以外,这种方法不要求访问 DNS 提供商。
当无法将 80 端口暴露在互联网上时,可以使用 DNS-01 challenge 方法,它只要求 cert-manager 对 DNS 提供商具有 egress 访问权。但这种方法要求您能够访问 DNS 提供商的 API,所需的访问级别因具体提供商而异。
如何进行故障排除?
由于 Kubernetes 错综复杂,因此很难提供有针对性的故障排除信息。当您遇到问题时,请在 NGINX 社区的官方微信群中向我们提问(NGINX Plus 用户可以使用他们的普通支持选项)。
立即行动
立即申请 NGINX Ingress Controller 30 天免费试用(带有 NGINX App Protect WAF 和 DoS),并下载始终免费的 NGINX Service Mesh。