Kong ACME plugin, in KIC (KongIngressController)

MarkYQJ
18 min readJul 14, 2022

--

本文章介紹如何在 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 ACME plugin

無論是 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 官網的說明

Let’s Encrypt 指定位址和內容 (Token), 並且由 Admin Software 針對內容由 Let’s Encrypt 所發送的 key 做簽章
Admin Software 做完簽章後, 通知 Let’s Encrypt 驗證, 成功就核發 TLS/SSL cert

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: 不能使用 KongPlugin
  • global: 不能設為 false
  • api_uri: 預設為 Let’s Encrypt production, 也可以切換為 Let’s Encrypt staging
  • tos_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), 兩者最大的不同是

  1. 作動時機: Cert-Manager 在 apply Ingress 時便會作動; 相對來說, Kong ACME Plugin 是在第一次接收到 https 連線時才會作動
  2. 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 是比較簡單的.

--

--