本文章介紹如何在 KIC 設定 ACME plugin, 快速達到使用 nip.io+let’s encrypt 提供 SSL/TLS; 最比較一下和 Cert-Manager 使用上的差別.
安裝 KIC, Kong Ingress Controller
Kong 有兩種運作模式
- Kong API Gateway: 或 standalone 模式, 安裝完畢後可以搭配 konga (非 kong 團隊產品), 使用 GUI 來管理 kong 的設定; 這裡指的設定是包含管理 Service, Route, Plugins, Consumers 等, 可以視為入門 kong 最好的途徑
- Kong Ingress Controller: 簡稱 KIC, 為本文使用的模式; 在 k8s 的設計中, 必需要額外安裝 Ingress Controller (例如 HAProxy, Kong, NGINX, Istio), 來實現 Ingress 的功能; 所以想當然, 各家 Ingress Controller 會有各家的長處或特色, 也就有對應的 CRD, 例如 Kong 就有 KongIngress, KongPlugin, KongConsumer (這 Kong 獨有的) 等. 下圖為 KIC 運作模式的示意圖.
無論是 Kong API Gateway, 或是 Kong Ingress Controller, 又同時都有兩種儲存模式, DB-less mode 及 DB mode
- DB-less mode: 就名稱上看來, 可以知道當 kong 重啟後, 設定在記憶體中的資料便會消失; 但如果系統是固定的結構 (例如固定的 Service, Route 關係), 這個模式就很適合, 可以在啟動或佈署 kong 時便把這些關係都建立起來
- DB mode: 背後需要一個 DB (例如 PostgreSQL), 來記錄這些設定, 即使 kong 重新啟動或是主機重開, 都還是會被保存下來; 某些特別的 plugin 在這兩種模式下的設定會不同 (本文中用到的 ACME plugin 就是如此)
官方文件有一些安裝方法, 其中在 minikube 的安裝方法所使用的 kong 為 DB-less; 建議如果需要抓下 kong yaml 自己安裝 (即不是透過 helm) 可以至官方 github 抓對應的 yaml 來修改.
佈署資源
相信會看到這篇文章的人, 以上都是略懂了, 以下就進入正題
1.) 詳讀文件: 因為不讀的話, 你一定設不起來; 不想讀官方文章的話, 照著這篇文章也是可以
2.) 設定 kong 的 Service port: 把 port 80 打開 (不能是 8080), 這是 ACME 運作時的規定, Let’s Encrypt 需要查訪 port 80, 取回簽名後的文件做驗證. 詳細流程可以參考 Let’s Encrypt 官網的說明
3.) 設定 kong 環境變數: 找到下面的 deployment, 加入環境變數 KONG_LUA_SSL_TRUSTED_CERTIFICATE, 並設為 system (只有在 Kong 2.2 以後的版本支援) 如下
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-kong
spec:
template:
spec:
automountServiceAccountToken: false
containers:
- env:
- name: KONG_DATABASE
value: postgres
- name: KONG_PG_HOST
value: postgres
- name: KONG_PG_PASSWORD
value: kong
- name: KONG_PROXY_LISTEN
value: 0.0.0.0:8000, 0.0.0.0:8443 ssl http2
- name: KONG_PORT_MAPS
value: 80:8000, 443:8443
...
- name: KONG_LUA_SSL_TRUSTED_CERTIFICATE
value: system
4.) 創建 ACME plugin: 這是最關鍵的一步; 裡面有幾個設定不要懷疑照著設, 因為我試過了, 包含
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name: acme-kongclusterplugin
annotations:
kubernetes.io/ingress.class: kong
labels:
global: "true"
config:
account_email: markyqj@gmail.com
# api_uri: https://acme-staging-v02.api.letsencrypt.org/directory
tos_accepted: true
storage: kong
domains:
- markyqj-20-222-40-17.nip.io
plugin: acme
如果你想嘗試其他的變化, 下面是我試過的結論, 就不用再浪費時間了, 包含
kind
: 不能使用 KongPluginglobal
: 不能設為 falseapi_uri
: 預設為 Let’s Encrypt production, 也可以切換為 Let’s Encrypt stagingtos_accepted
: 規定要 true, 官方文件有寫, 代表你接受了 tos (雖然不知道 TOS 內容是什麼); 實際上是 ACME plugin 的實作, 會檢查是否接受了 TOS, 可以參考實作
entity_checks = {
{ conditional = {
if_field = "config.api_uri", if_match = { one_of = {
"https://acme-v02.api.letsencrypt.org",
"https://acme-staging-v02.api.letsencrypt.org",
} },
then_field = "config.tos_accepted", then_match = { eq = true },
then_err = "terms of service must be accepted, see https://letsencrypt.org/repository/",
} },
storage
: 設定要把 cert 存放在什麼存儲體上, 這跟上面提到的運作模式 DB 或 DB-less mode 有關; 在指定存儲體後, 而 lua-resty-acme 便會透過對應的方式存取. 要是已經佈署好 kong 了, 要確認自己的 KIC 是以什麼運作模式, 可以觀察下面指令結果, 其中有 postgres (當然你有可能是其他的 DB) 很明顯就是 DB mode 了.
joye@master-node:~$ kubectl -n kong get pods
NAME READY STATUS RESTARTS AGE
ingress-kong-6cc588f6b4-s2tqp 2/2 Running 0 45h
kong-migrations-mv82x 0/1 Completed 0 46h
postgres-0 1/1 Running 0 46h
若為 DB mode 的話, 就可以使用 kong
, 如果為 DB-less mode, 就可以改用 shm
. 實際上這裡的 storage kong
是 kong 實作了一個介面給 lua-resty-acme 用, 透過這個介面 lua-resty-acme 會將取得的 cert 存放至 kong 所設定的 DB (以這裡來說就是 postgreSQL). 如果要與特定的 Vault 整合, 可以透過設定 storage_config 來設定/更改.
5.) 創建 Service: 把目標的 Service 布起來, 例如 my-service
6.) 建立 Ingress (Routing Rule), 把 request 透過 Proxy 導到 Service: 先講講原始的做法, 是要在 Ingress 中掛載對應的 Secret (就是從 Let’s Encrypt 申請到的 SSL/TLS cert), 如下
metadata:
annotations:
konghq.com/protocols: https
konghq.com/https-redirect-status-code: "308"apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: markyqj-ingress
spec:
ingressClassName: kong
tls:
- hosts:
- markyqj-20-222-40-17.nip.io
secretName: markyqj-20-222-40-17.nip.io
rules:
- host: "markyqj-20-222-40-17.nip.io"
http:
paths:
- path: "/api"
...
然而在使用 Kong ACME plugin 後
- TLS/SSL Cert 是由 Kong ACME plugin 管理, 所以不需要再加上 spec.tls
- 由於 ACME 的運作時的規定 (嚴格來說是 ACME http-01), Let’s Encrypt 需要訪問
http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
, 故必需要加上對應的 routing path - 但是, 又為了要為要實作將所有 http request 都轉向到 https, 在 Ingress 中加入了以下的 annotation. 然而這麼做有個問題, ACME 的 challenge 為 http, 如果被被導向到 https 就會失敗
metadata:
annotations:
konghq.com/protocols: https
konghq.com/https-redirect-status-code: "308"
所以最後的解決方法為下, 兩個 Ingress:
1st Ingress: 這裡 upstream (my-service) 其實不重要, 因為實際上並不會把 request 導到該 Service, 所以就隨意填一個 Service, 例如真正要佈署的 my-service 就可以 (again, 即 my-service 不支援這個 request)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: markyqj-ingress-acme
spec:
ingressClassName: kong
rules:
- host: "markyqj-20-222-40-17.nip.io"
http:
paths:
- path: "/.well-known/acme-challenge"
pathType: ImplementationSpecific
backend:
service:
name: my-service
port:
number: 80
2nd Ingress: 這是真正要將 request 導到 my-service 時用的 Ingress. 在這裡加入 http 導向至 https 的 annotation, 但這裡不能加入 annotation konghq.com/plugins: acme-kongclusterplugin;
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: markyqj-ingress
annotations:
konghq.com/protocols: https
konghq.com/https-redirect-status-code: "308"
konghq.com/strip-path: "true"
konghq.com/preserve-host: "true"
konghq.com/request-buffering: "false"
konghq.com/response-buffering: "false"
spec:
ingressClassName: kong
rules:
- host: "markyqj-20-222-40-17.nip.io"
http:
paths:
- path: "/api"
pathType: ImplementationSpecific
backend:
service:
name: my-service
port:
number: 80
因為在 Kong 的定義中, Cluster-wise plugin 不可以與任何的 Route 有關聯; 這裡可以觀察 kong-ingress 的 log 可以發現
time="2022-07-13T07:46:30Z" level=error msg="could not update kong admin" error="1 errors occurred:\n\twhile processing event: {Create} plugin acme for route 69951a8a-cecd-4b63-a084-0a38b88adb3d failed: HTTP status 400 (message: \"schema violation (route: value must be null)\")\n" subsystem=dataplane-synchronizer
至於要怎麼趨動 Kong ACME Plugin 作動呢, 就是隨意打一個 https request 就可以, 要用 curl 打也行. 而由於上面有做 http 導向至 https, 所以 browser 來說, 直接以 http 連線也行; 第一次連線會看到這個
而第二次連線 (就再往下一頁走就會是第二次連線了), 就會發現鎖頭變了, 也就代表憑證已經上去了
所以, 如果要把體驗做的更好, 可以在 Ingress 佈署完後, 下
curl https://markyqj-20–222–40–17.nip.io -k
也可以事先把 cert 申請起來, 使用者打開時不會看到那個 warning
後記
在 Kong 的設計, 這種 “global” 的 Plugin, 也不能有第二個, 例如創建兩個 ACME 的 KongClusterPlugin, 會出現以下的錯誤
level=error msg="multiple KongPlugin definitions found with 'global' label for 'acme', the plugin will not be applied"
也就是反過來說, 如果要增加第二個 domain, 只能透過 patch 的方式去新增, 而 Kong 很歡迎有志者來把這個 feature 設計的更友善一點喔
與 Cert-Manager 使用上的差異
- 與 Kong ACME plugin 相同的部分是, 都需要先創建一個 Resource 來處理/設定 Cert; 以下為 Cert-Manager 的範例
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
namespace: cert-manager
spec:
acme:
email: user@example.com #please change this
privateKeySecretRef:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
...
- 使用 Cert-Manager 需要在 Ingress 中加入 annotation, 知會 Cert-Manager ingress-shim 作動, 以申請及管理 cert
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-example-com
annotations:
kubernetes.io/tls-acme: "true"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: kong
tls:
- secretName: demo-example-com
hosts:
- demo.example.com
rules:
- host: demo.example.com
http:
paths:
- path: /
...
除了設定 (或創建對應的 resource), 兩者最大的不同是
- 作動時機: Cert-Manager 在 apply Ingress 時便會作動; 相對來說, Kong ACME Plugin 是在第一次接收到 https 連線時才會作動
- Cert 管理: Cert-Manager 維護的 cert 是依據 Ingress 中的 spec.tls.secretName, 而 Kong ACME Plugin 需要設定 plugin 中的 domains. 就設定的複雜度而言, 兩者是一樣的. 而 Cert-Manager 在收到 Ingress deleted 後, 會將對應的 Certificate resource 移除, 然而, 預設並不把 cert 移除; 可以在刪除 Ingress 後使用 get secrets 做確認. 參考官方文件的做出以下的修改
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager
spec:
template:
spec:
containers:
- name: cert-manager
image: "quay.io/jetstack/cert-manager-controller:v1.8.2"
imagePullPolicy: IfNotPresent
args:
- --v=2
- --cluster-resource-namespace=$(POD_NAMESPACE)
- --leader-election-namespace=kube-system
- --enable-certificate-owner-ref=true
可以觀察所創建出來的 Secret 便會多出 ownerReferences
kind: Secret
metadata:
creationTimestamp: "2022-07-18T07:46:45Z"
name: summer220718b-20-222-40-17.nip.io
namespace: summer
ownerReferences:
- apiVersion: cert-manager.io/v1
blockOwnerDeletion: true
controller: true
kind: Certificate
name: summer220718b-20-222-40-17.nip.io
uid: 8475902b-c532-42a6-8819-cdf4bf4924a4
uid 是指向對應的 Certificate
kind: Certificate
metadata:
name: summer220718b-20-222-40-17.nip.io
ownerReferences:
- apiVersion: networking.k8s.io/v1
blockOwnerDeletion: true
controller: true
kind: Ingress
name: summer220718b-nucleus-ingress
uid: 073498a3-64db-4ad4-913f-442b209aa79d
resourceVersion: "5618840"
uid: 8475902b-c532-42a6-8819-cdf4bf4924a4
所以當 Ingress 被移除時, Cert-Manager 的 ingress-shim 會移除 Certificate resource, 而應的 Secret 也就被移除了. 也就是, 就管理的複雜度而言, Cert-Manager 是比較簡單的.