使用 cert-manager 管理 K8S TLS 憑證

smalltown
smalltown
May 12, 2020 · 15 min read
圖片來源 Exabytes

Background

在這個對資訊安全越來越要求的時代,想要架設一個線上服務時,透過 HTTPS 協定來傳輸資料是再正常不過的了!而一般通常會有哪幾種作法呢?

  1. 自我簽發 TLS 憑證:某些內部服務其實並不需要被公開存取,所希望達到的是資料在點對點傳輸的過程中必須要是加密的,所以可以使用自己組織簽發的 TLS 憑證,Client 去存取的時候自己帶上 CA 憑證去驗證即可,例如 HashiCorp Vault, AWS RDS TLS 連線…等

自己一開始是使用 CertBot 的 DNS-01 Challenge 的方式來申請 Let’s Encrypt 服務,申請起來是很方便啦,但是當你申請的憑證有越來越多張,而且每一張都只有三個月的時間就會過期,常常一直手動 Renew 這些憑證其實也滿惱人的,常言道懶惰是發明的動力,所以有了 cert-manager 的誕生!

cert-manager

cert-manager 是基於 Kubernetes 所開發的憑證管理工具,它可以可以幫忙發出來自各家的 TLS 憑證,例如上面所提到的 ACME (Let’s Encrypt), HashiCorp Vault, Venafi 或是自己簽發的憑證,而且它還可以確保 TLS 憑證一直維持在有效期限內,因此想要透過這篇文章簡單的示範一下如何透過 cert-manager 來自動申請且 Renew Let’s Entrypt 的 TLS 憑證

Prerequisite

在此篇文章中有實際操作的部分,所以先把需要安裝的東西寫在這個章節內,想要自己動手試試看的人記得要先設定一下

Kubernetes

因為 cert-manager 會安裝在 Kubernetes 中,所以必須有一個 K8S 的環境才行,這邊的範例使用 minikube (官方安裝教學)

~$ minikube version
minikube version: v1.9.2
commit: 93af9c1e43cab9618e301bc9fa720c63d5efa393
~$ minikube start
...
🐳 Preparing Kubernetes v1.18.0 on Docker 19.03.8 ...
🌟 Enabling addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube"
# 簡單檢查一下可以存取 Kubernetes Cluster
~$ kubectl get node
NAME STATUS ROLES AGE VERSION
minikube Ready master 43s v1.18.0
...
# 如下失敗無法取得 Node 資訊:
The connection to the server xxxxx was refused - did you specify the right host or port?
...

Helm

因為會使用到 Helm 來安裝 cert-manager,因此要先在本機端將 Helm 給安裝完畢,這邊推薦可以使用 kubenvz 來安裝和管理 Helm 的版本

$ brew tap nutellinoit/kubenvz
$ brew install kubenvz
~$ kubenvz helm install v3.2.1
Downloading helm v3.2.1 from https://get.helm.sh/helm-v3.2.1-darwin-amd64.tar.gz
~$ helm version
version.BuildInfo{Version:"v3.2.1", GitCommit:"fe51cd1e31e6a202cba7dead9552a6d418ded79a", GitTreeState:"clean", GoVersion:"go1.13.10"}

AWS IAM User

底下的示範會使用到 DNS-01 Challenge 的方式來進行 Let’s Encrypt TLS 憑證的申請,而使用到的是 AWS 的 DNS 託管服務 Route53;所以我們在這邊要先建立好一個 AWS IAM User 並且給予它以下的權限,然後再透過這個 IAM User 建立一把 AK/SK 給 cert-manager 使用

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/*"
},
{
"Effect": "Allow",
"Action": "route53:ListHostedZonesByName",
"Resource": "*"
}
]
}

Demonstration

上一個章節已經將所需要的東西都準備完畢了,緊接著來安裝今天的主角 cert-manager

Installation

# 首先建立好 cert-manager 所需要的 namespace
~$ kubectl create namespace cert-manager
namespace/cert-manager created
# 加入 cert-manager 的 helm repo 並且更新它
~$ helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
~$ helm repo update
...
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈ Happy Helming!⎈
~$ helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v0.15.0 --set installCRDs=true
...
NAME: cert-manager
LAST DEPLOYED: Mon May 11 21:43:20 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager has been deployed successfully!
...
# 最後驗證安裝是否成功,差不多等個一分多鐘就可以看到有底下三個 Pod 呈現 Running 的狀態
~$ kubectl get pods --namespace cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-7cb75cf6b4-95z5n 1/1 Running 0 6m57s
cert-manager-cainjector-759496659c-zc6g5 1/1 Running 0 6m57s
cert-manager-webhook-7c75b89bf6-tsvd5 1/1 Running 0 6m57s

Creating an Issuer/ClusterIssuer

安裝好 cert-manager 之後,我們要來設定簽發 TLS 憑證的組態,而這邊有兩種類別,我們這個範例使用 ClusterIssuer

  • Issuer:作用範圍只在某個 K8S Namespace 內

這個設定主要是要讓 cert-manager 知道所要使用的 CA 是什麼 (像這邊就是使用 Let’s Encrypt),還有要管理的域名以及驗證的方法 (這邊的域名是 smalltown.dev, 驗證方法則是採取 DNS-01),設定好這些之後,下一個步驟要申請 Certificate 的時候就可以直接引用它

# 請填入一開始建立 AWS IAM User 所產生的 AK/SK,而這裡使用 AK/SK 只是為了方便 Demo, 事實上可以參考官方網站的設定,可以使用更安全的 AWS IAM Role
~$ export accessKeyID=xxxxx
~$ export secretAccessKey=xxxxx
~$ export secretAccessKeyBase64=$(echo ${secretAccessKey} | base64)
# 輸入要申請憑證的域名
~$ export domain=smalltown.dev
# 產生要建立 cert manager clusterissuer 的 yaml 檔案
~$ cat <<EOF > cert-manager-resources.yaml
apiVersion: v1
kind: Secret
metadata:
name: cert-manager-ak-sk
namespace: cert-manager
type: Opaque
data:
secretAccessKey: ${secretAccessKeyBase64}
---
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
namespace: cert-manager
spec:
acme:
server:
https://acme-v02.api.letsencrypt.org/directory
email: cert-manager@smalltown.dev
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- selector:
dnsZones:
- "${domain}"
dns01:
route53:
region: us-east-1
accessKeyID: ${accessKeyID}
secretAccessKeySecretRef:
name: cert-manager-ak-sk
key:
secretAccessKey
EOF
# 建立 clusterissuer
~$ kubectl apply -f cert-manager-resources.yaml
secret/cert-manager-ak-sk created
clusterissuer.cert-manager.io/letsencrypt-prod created
# 然後驗證看看有沒有建立成功
~$ kubectl describe clusterissuers.cert-manager.io letsencrypt-prod -n cert-manager
...
Status:
Acme:
Last Registered Email: cert-manager@smalltown.dev
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/85855018
Conditions:
Last Transition Time: 2020-05-11T14:57:13Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>

Creating Certificate

上一個步驟已經建立好 ClusterIssuer 了,緊接著進行最後一個步驟,也就是透過這個 ClusterIssuer 來申請 TLS Certificate

# 建立要申請 Certificate 的 yaml 檔案,這邊要注意的是在 issuerRef 的部分,假如是使用 clusterissuer 的話,記得要把 kind 給加進去,不然申請 certificate 的時候會卡住~$ cat <<EOF > certificate-resources.yaml
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: default.smalltown.dev
namespace: default
spec:
dnsNames:
- default.smalltown.dev
secretName: default-smalltown-dev-tls
issuerRef:
name: letsencrypt-prod
kind
: ClusterIssuer
EOF
# 建立開 certificate
~$ kubectl apply -f certificate-resources.yaml
certificate.cert-manager.io/default.smalltown.dev created
# 驗證看看 Certificate 有沒有被建立成功 (等個五分鐘)
~$ kubectl describe certificate default.smalltown.dev -n default
...
Status:
Conditions:
Last Transition Time: 2020-05-11T15:57:50Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-08-09T14:57:49Z
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal GeneratedKey 2m9s cert-manager Generated a new private key
Normal Requested 2m9s cert-manager Created new CertificateRequest resource "default.smalltown.dev-3254411236"
Normal Issued 29s cert-manager Certificate issued successfully
# 接著可以看到 certificate 已經產生定義在 yaml 檔案中的 secretName
~$ kubectl describe secrets default-smalltown-dev-tls
Name: default-smalltown-dev-tls
Namespace: default
Labels: <none>
Annotations: cert-manager.io/alt-names: default.smalltown.dev
cert-manager.io/certificate-name: default.smalltown.dev
cert-manager.io/common-name: default.smalltown.dev
cert-manager.io/ip-sans:
cert-manager.io/issuer-kind: ClusterIssuer
cert-manager.io/issuer-name: letsencrypt-prod
cert-manager.io/uri-sans:
Type: kubernetes.io/tlsData
====
ca.crt: 0 bytes
tls.crt: 3574 bytes
tls.key: 1675 bytes

可以看到 Certificate 已經被自動申請完成,而且 cert-manager 會在過期前 30 天就幫忙 renew 好,在 K8S 裡面的服務就可以自動享受到 TLS Certificate 自動被 Renew 好的便利

Conclusion

有了 cert-manager 之後,在 K8S 內的服務端點,假如有使用 HTTPS 加密傳輸協定的話,就不用擔心過期的問題,因為 cert-manager 會自動幫忙搞定;不過在 K8S 外面的服務就沒有這麼幸福了 T_T

而文章一開始也有提到 cert-manager 不只支援 ACME (Let’s Encrypt),而且驗證方式也不只有 DNS-01 而已,還有 HTTP-01,有興趣或是需求的人可以自己試試看,

在試驗的過程中,其實也有遇到小卡關,這時候建議可以看看 cert-manager 三個 Pod 所產生的 Log,或是去 kubectl describe certificaterequest (由 certificate 資源所產生) ,最後再加上 Google 就可以獲得不少有幫助的資訊來排除問題

Starbugs Weekly 星巴哥技術專欄

一群技術人想要寫出一些好文章所建立的技術專欄。每週二一篇原創文章、一封電子報,歡迎大家訂閱!主網站: https://weekly.starbugs.dev/。

Starbugs Weekly 星巴哥技術專欄

一群技術人想要寫出一些好文章所建立的技術專欄。每週二一篇原創文章、一封電子報,歡迎大家訂閱!主網站: https://weekly.starbugs.dev/。

smalltown

Written by

smalltown

原來只是一介草 QA,但開始研究自動化維運雲端服務後,便一頭栽進 DevOps 的世界裏,熱愛鑽研各種可以提升雲端服務品質及增進團隊開發效率的開源技術

Starbugs Weekly 星巴哥技術專欄

一群技術人想要寫出一些好文章所建立的技術專欄。每週二一篇原創文章、一封電子報,歡迎大家訂閱!主網站: https://weekly.starbugs.dev/。