標籤:DNS

使用 cert-manager 透過 ACME 從 FreeIPA 簽發憑證

本篇 TL;DR 因包含腳本及 K8s yaml 內容較長,有興趣直接閱讀全文的讀者可以按此跳到正文

TL;DR

分成 FreeIPA 與 cert-manager 兩個部分設定:

FreeIPA

# ipa-acme-manage enable
# tsig-keygen -a hmac-sha512 cert-manager-acme | tee -a /etc/named/ipa-ext.conf
# systemctl restart named-pkcs11.service || systemctl restart named.service
# ipa dnszone-mod ${IPA_DNS_ZONE} --dynamic-update=True \
    --update-policy='grant cert-manager-acme wildcard * TXT;'

順便記下印出來的 TSIG Key Secret。

cert-manager

---
apiVersion: v1
kind: Secret
type: Opaque
metadata:
  namespace: cert-manager
  name: ipa-tsig
data:
  cert-manager-acme: ${TSIG_SECRET_IN_BASE64}
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: ipa
spec:
  acme:
    caBundle: ${IPA_CA_CERT_IN_BASE64}
    privateKeySecretRef:
      name: ipa-acme-account-key
    server: https://${IPA_CA_SERVER}/acme/directory
    solvers:
    - dns01:
        rfc2136:
          nameserver: ${IPA_NS_IP}
          tsigAlgorithm: HMACSHA512
          tsigKeyName: cert-manager-acme
          tsigSecretSecretRef:
            key: cert-manager-acme
            name: ipa-tsig
      selector:
        dnsZones:
        - ${IPA_DNS_ZONE}

完成。如果要從 Ingress 自動發憑證的話要記得加上 cert-manager.io/cluster-issuer: ipa 的 annotation。

前言

我們在管理 Kubernetes[1] 的時候常常會需要提供一個 TLS Ingress[2],在沒有自動化工具的協助下,我們會需要手動將憑證及金鑰塞進 Cluster 裡。

此時 cert-manager[3] 就能幫我們處理自動簽發憑證的問題,cert-manager 支援透過 ACME[4] 來完成自動化簽發憑證。通常,我們可能會拿來簽發 Let's Encrypt[5] 的憑證,但如何讓 Let's Encrypt 通過驗證就會是個問題,cert-manager 同時支援了 HTTP-01[6] 及 DNS-01[7] 兩種方式。

今天想要來跟大家聊聊,如果想透過 cert-manager 自動簽發憑證的話,我們可以怎麼設定。本篇以想要讓 FreeIPA[8] 作為憑證簽發者,而且透過 DNS-01 通過驗證當作目標來進行(適用於用 FreeIPA 管理 DNS zone 的場景,所以你想用 Let's Encrypt 簽發憑證也可以):

FreeIPA DNS

由於我們打算採用 DNS-01 作為 ACME 的認證手段,我們得先來搞定如何讓 cert-manager 自動來修改 FreeIPA 的 DNS 記錄。

FreeIPA 使用 BIND9[9] 作為其 DNS 的解決方案,由於 BIND 與 cert-manager 都支援[10] RFC-2136[11],因此我們可以透過 UPDATE 指令來更新 RR[12]。而為了讓 FreeIPA 的 BIND 確認這個更新的指令是從被授權的 cert-manager 來的,我們還必須透過 TSIG[13] RR 來進行簽名認證。

TSIG Secret Key

說到簽名,就必須要有一個可以拿來簽名[14]的 Secret Key,於是讓我們從建立這把金鑰開始:

# export TSIG_KEY_NAME=cert-manager-acme
# tsig-keygen -a hmac-sha512 ${TSIG_KEY_NAME} | tee -a /etc/named/ipa-ext.conf
key "cert-manager-acme" {
        algorithm hmac-sha512;
        secret "<...base64 encoded secret here...>";
};
# systemctl restart named.service || systemctl restart named-pkcs11.service
#

請自行將 ${TSIG_KEY_NAME} 替換成想要的金鑰名稱,像筆者是使用容易識別的 cert-manager-acme,表示這把金鑰是給 cert-manager 作為 ACME 用的金鑰。

另外,輸出的 secret 內容請記下來,此為 TSIG Secret Key,後面會用 ${TSIG_SECRET} 表示,在設定 cert-manager 時還會用到。我們在這裡建立了一把使用 HMAC-SHA512[15] 作為簽名演算法的金鑰。

上面的指令除了建立 TSIG Secret Key 以外,還會將這個設定寫入 BIND9 的設定檔中,並重新啓動 BIND9[16],讓 BIND9 認識這把金鑰的存在。

設定更新策略

光是讓 BIND9 認得 TSIG 金鑰是不夠的,我們還必須設定更新策略以讓 BIND9 可以按照允許規則來放行該金鑰的更新指令:

透過 FreeIPA WebUI

我們可以透過 WebUI 來進行更新策略的設定,在登入 WebUI 後點選 "Network Service" 選擇要用來簽發憑證的 DNS Zone 後,來到 Settings 並按照下方截圖說明設定:(如果你的 WebUI 不是英文版本,可參考截圖內的相對位置進行設定)

  • 啓用 "Dynamic update"
  • 在 "BIND update policy"[17] 中加入規則:(截圖中反白內容)
    • grant cert-manager-acme wildcard *.k8s.davy.home TXT;
    • 請將 cert-manager-acme 替換成剛剛設定的 ${TSIG_KEY_NAME}
    • 請將 *.k8s.davy.home 替換成讀者想要簽發憑證的 Domain name,筆者在此想讓 cert-manager 可以簽發 k8s.davy.home. 下的所有 Subdomain[18],因而如此設定

儲存後即可完成更新策略的設定。

透過 ipa CLI

如果不想要再花時間登入 WebUI 的話,也有透過 ipa 指令設定的方式:

# export IPA_DNS_ZONE=davy.home
# ipa dnszone-mod ${IPA_DNS_ZONE} --dynamic-update=True \
    --update-policy='grant cert-manager-acme wildcard *.k8s.davy.home TXT;'
#

請讀者自行依照實際情形替換 ${IPA_DNS_ZONE} 為對應的 DNS Zone,以及將 cert-manager-acme*.k8s.davy.home 替換成對應的內容。

cert-manager ClusterIssuer/Issuer

在 cert-manager 這側,我們需要做的事情就相對簡單了,只需要建立兩個資源 —— SecretClusterIssuer/Issuer

首先我們必須先決定這個 Issuer 是 Cluster-wide 的還是 Namespace-wide[^namespace] 的,如果你打算讓整個 Cluster 內的資源都能使用這個 Issuer 的話就選擇建立 ClusterIssuer,反之,就選擇建立 Issuer

本文將以 ClusterIssuer 作為範例,最大差別在於同一個 Issuer 的相關資源(例如 Secret 等)必須放在同一個 namespace 下;而 ClusterIssuer 則是將相關資源放在 cert-manager 指定的 namespace(預設與 cert-manager 同一個 namespace[19]),自己本身並沒有 namespace 的屬性。

TSIG Secret Key

首先,我們將建立一個存放 TSIG Secret Key 的 Secret 資源:

$ kubectl -n cert-manager create secret generic ipa-tsig \
    --from-literal=secret="${TSIG_SECRET}"
secret/ipa-tsig created
$

此處的 ${TSIG_SECRET} 在前面〈FreeIPA DNS → TSIG Secret Key〉一節中有提到過,如果還沒有取得的讀者請先往前翻閱。

如果讀者想建立的是 Issuer 資源,請在這一步驟中將 Secret 資源建立在對應的 Namespace 中。

建立 ClusterIssuer

有了存放 TSIG Secret Key 的資源後,我們就可以來建立 ClusterIssuer 資源了:

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: ipa
spec:
  acme:
    caBundle: ${IPA_CA_CERT_IN_BASE64}
    privateKeySecretRef:
      name: ipa-acme-account-key
    server: https://${IPA_CA_SERVER}/acme/directory
    solvers:
    - dns01:
        rfc2136:
          nameserver: ${IPA_NS_IP}
          tsigAlgorithm: HMACSHA512
          tsigKeyName: ${TSIG_KEY_NAME}
          tsigSecretSecretRef:
            name: ipa-tsig
            key: secret
      selector:
        dnsZones:
        - ${IPA_DNS_ZONE}

其中有幾個值需要讀者根據實際情況替換:

  • ${IPA_CA_CERT_IN_BASE64}
    • FreeIPA 的 CA 憑證(以 base64 編碼的 PEM 格式表示)
    • 可以從 FreeIPA 主機的 /etc/ipa/ca.crt 取得
  • ${IPA_CA_SERVER}
    • 通常會是 ipa-ca.<IPA REALM DOMAIN>,以筆者的環境來說就是 ipa-ca.davy.home
  • ${IPA_NS_IP}
    • FreeIPA 的 DNS Primary IP,通常就是 FreeIPA 的 Primary Server IP
  • ${TSIG_KEY_NAME}
  • ${IPA_DNS_ZONE}
    • 你想要簽發的 DNS Zone 或 Subdomain,以筆者的案例來說就是 k8s.davy.home
  • ipa-acme-account-key
    • 這裡會自動建立用於存放 ACME 註冊資訊的 Secret,讀者可自由更換其名稱
  • server: https://${IPA_CA_SERVER}/acme/directory

如果讀者想建立的是 Issuer 資源,請在這一步驟中改建立 Issuer 資源,並將此建立在對應的 Namespace 中。

接下來我們可以透過 kubectl describe 來觀察 ClusterIssuer 與我們 FreeIPA ACME 服務的註冊情形:

$ kubectl describe clusterissuer ipa
...
Status:
  Acme:
    Last Private Key Hash:  BbkPF6ZPgulgisJ80ISEIq10JXHIB19M9I2eYOHIFAQ=
    Uri:                    https://ipa.davy.home/acme/rest/acct/Mt72_fSdmemIOK8cj89x6AZhwujqenQ5MyIxqCAnC1Y
  Conditions:
    Last Transition Time:  2024-07-18T15:18:40Z
    Message:               The ACME account was registered with the ACME server
    Observed Generation:   2
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
$

如果可以看到 Reason: ACMEAccountRegistered,表示我們的 FreeIPA 已經受理我們的 ACME 註冊手續了。但這還並不表示我們設定的 TSIG Secret Key 已經正確被驗證了,這會等到我們真的進行 DNS-01 驗證時,要修改 TXT RR 的時候才會去驗證。

TLS Ingress

到目前為止,我們已經大致上完成了 FreeIPA 及 cert-manager 兩側的串接了,我們可以透過新增一個測試用的 TLS Ingress 來測試是否真的成功了:

---
kind: Namespace
metadata:
  name: cert-manager-acme-test
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: cert-manager-acme-test
  name: hello-deployment
spec:
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  namespace: cert-manager-acme-test
  name: hello-service
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: hello
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: ipa
  namespace: cert-manager-acme-test
  name: hello-ingress
spec:
  rules:
  - host: hello.k8s.davy.home
    http:
      paths:
      - backend:
          service:
            name: hello-service
            port:
              name: http
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - hello.k8s.davy.home
    secretName: hello-tls

上面的 yaml 會部署一組 DeploymentServiceIngress 到一個新的 Namespace cert-manager-acme-test 中。

注意我們在 Ingress 的地方除了設定 TLS 相關的內容以外,還多了一個 Annotation cert-manager.io/cluster-issuer,cert-manager 會自行過濾帶有此 Annotation 的 Ingress 並以其值對應的 ClusterIssuer 來簽發憑證[20]

如果想使用 Issuer 的話,此處換成 cert-manager.io/issuer 即可。

上方的 Ingress 在設定 TLS 時一併設定了一個 secretName,此為存放 TLS 憑證及金鑰用的 Secret 資源的名稱,如果不存在的話會自動建立。而我們亦可以透過觀察這個 Secret 及其對應的 cert-manager.io/v1/CertificateRequest 資源來看到憑證簽發的狀態如何:

$ kubectl -n cert-manager-acme-test get secret hello-tls -o yaml
apiVersion: v1
data:
  tls.crt: ...
  tls.key: ...
kind: Secret
metadata:
  annotations:
    cert-manager.io/alt-names: hello.k8s.davy.home
    cert-manager.io/certificate-name: hello-tls
    cert-manager.io/common-name: hello.k8s.davy.home
    cert-manager.io/ip-sans: ""
    cert-manager.io/issuer-group: cert-manager.io
    cert-manager.io/issuer-kind: ClusterIssuer
    cert-manager.io/issuer-name: ipa
    cert-manager.io/uri-sans: ""
  labels:
    controller.cert-manager.io/fao: "true"
  name: hello-tls
  namespace: cert-manager-acme-test
type: kubernetes.io/tls
$ kubectl -n cert-manager-acme-test get CertificateRequest \
  -o custom-columns="NAME:.metadata.name,Secret:.metadata.annotations.cert-manager\.io/certificate-name,Issuer:.spec.issuerRef.name,Issued:.status.conditions[?(@.type=='Ready')].status"
NAME          Secret      Issuer   Issued
hello-tls-1   hello-tls   ipa      True
$ kubectl -n cert-manager-acme-test get CertificateRequest hello-tls-1 -o yaml
apiVersion: cert-manager.io/v1
kind: CertificateRequest
metadata:
  annotations:
    cert-manager.io/certificate-name: hello-tls
    cert-manager.io/certificate-revision: "1"
    cert-manager.io/private-key-secret-name: hello-tls-7nrlt
  name: hello-tls-1
  namespace: cert-manager-acme-test
  ownerReferences:
  - apiVersion: cert-manager.io/v1
    blockOwnerDeletion: true
    controller: true
    kind: Certificate
    name: hello-tls
spec:
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: ipa
  request: ...
status:
  certificate: ...
  conditions:
  - lastTransitionTime: "2024-07-18T14:58:21Z"
    message: Certificate request has been approved by cert-manager.io
    reason: cert-manager.io
    status: "True"
    type: Approved
  - lastTransitionTime: "2024-07-18T15:19:48Z"
    message: Certificate fetched from issuer successfully
    reason: Issued
    status: "True"
    type: Ready
$

Secret 及對應的 CertificateRequest 中可以看出來,cert-manager 已經幫我們完成了一條龍 —— 從設定 TLS Ingress 到設定 TXT RR 到通過 DNS-01 驗證再回到設定 Ingress 憑證。

我們也可以打開對應的 Ingress URL 來看到結果:

Certifiacte Information

Certificate Chain

後記

原本以為可以在兩天內連續產出的這篇文章結果又拖了兩天才終於寫完,準備環境明明只花了一個晚上,結果大部分的時間都花在找對應的 Reference。 Orz

總之,這個技巧除了讓 FreeIPA 簽發憑證外,只要是透過 FreeIPA 或甚至純粹地用 BIND9 管理 DNS 的人都能使用這個方式自動化設定 TXT RR 來自動完成 DNS-01 驗證。

或是也可以延伸出其他的用法,其實並不限於 ACME 的場景而已,如果能夠理解背後的設定的意義的話就會有更多有趣的應用可以發掘。 :)


  1. Kubernetes 是用於自動部署、擴展和管理「容器化(containerized)應用程式」的開源系統,詳見其官方網站 ↩︎

  2. 此處指 Kubernetes 中的 networking.k8s.io/v1/Ingress 資源,用於從 Cluster 中暴露 HTTP/HTTPS 服務至外界,其詳細概念請見 Kubernetes 文件《Ingress》一章中的解釋 ↩︎

  3. cert-manager 是用於 Kubernetes 中的 X.509 憑證管理器,詳見其官方網站 https://cert-manager.io/ ↩︎

  4. "Automatic Certificate Management Environment",自動化憑證管理環境,定義於 RFC-8555,或可參考 Let's Encrypt 提供的解釋 ↩︎

  5. Let's Encrypt 是由 ISRG (Internet Security Research Group) 提供的非營利 CA 憑證機構,其透過 ACME 證明網域所有權後可免費提供其所有者短期 TLS 憑證,詳見其官網 https://letsencrypt.org/ ↩︎

  6. HTTP-01 為 ACME 定義的驗證方式之一,透過所有者在指定位置(https://<DOMAIN>/.well-known/acme-challenge/<TOKEN>)放置指定內容的方式驗證其網域所有權 ↩︎

  7. DNS-01 為 ACME 定義的驗證方式之一,透過所有者設定指定 TXT RR (_acme-challenge.<DOMAIN>)來驗證其網域所有權 ↩︎

  8. FreeIPA 是免費的開源身份管理系統,IPA 分別代表 Identity、Policy、Audit,同時也是 Red Hat Identity Manager 的上游開源專案,詳見其官方網站 ↩︎

  9. BIND9 是現今網際網路上常見的 DNS 伺服器軟體,目前由網際網路系統協會(ISC, Internet Systems Consortium)負責開發與維護。詳見維基百科《BIND》條目,或其官方網站 ↩︎

  10. 關於在 cert-mananger 中設定 RFC-2136 的詳細方式及說明請參閱使用手冊〈DNS-01 / RFC-2136〉一節 ↩︎

  11. "Dynamic Updates in the Domain Name System (DNS UPDATE)",旨在使得 DNS 可以在線上動態更新記錄,詳見 RFC-2136 內容 ↩︎

  12. Resource Record,RFC-1034 3.6 ↩︎

  13. Transaction Signature,由 RFC-2845 定義的 RR Type,詳見 RFC-2845 或維基百科上的《TSIG》條目 ↩︎

  14. 此處的「簽名」指的是 MAC (Message Authentication Code),用於認證及確認資料完整性,有關 MAC 的說明請參閱維基百科《Message Authentication Code》條目 ↩︎

  15. 在 RFC-2845 中僅定義 TSIG 可採用 HMAC-MD5 作為簽名演算法,但 MD5 在 2024 的現在已不夠安全,因此我們在此選擇使用 RFC-4635 擴充支援的 HMAC-SHA512 演算法。關於 HMAC-SHA 系列演算法的定義請參閱 RFC-4634;SHA 系列演算法的定義請參閱 FIPS180-4 ↩︎

  16. 依據實際情況的不同,FreeIPA 啓用的 BIND9 可能是 named.servernamed-pkcs11.server,這裡筆者偷懶將兩個都重啓試試看,讀者可以視情況自行調整 ↩︎

  17. BIND9 提供了許多更新策略的設定方式,其他規則寫法可參見筆者常參考的 ZyTrax 網站中關於 update-policy 的說明 ↩︎

  18. Subdomain —— 子域名,為指定域名再多一層的域名,詳見維基百科《Subdomain》條目 ↩︎

  19. 關於 ClusterIssuer/Issuer 相關的 Secret 需放置於何 Namespace 可參見原始碼中的說明,關於如何設定 Cluster resource namespace,如果是使用 Helm 部署 cert-mananger 的讀者可以參閱 clusterResourceNamespace 的設定說明 ↩︎

  20. cert-manager 會透過 webhook 來對 Ingress 做整合,詳見 cert-mananger 文件中〈Requesting Certificates → Ingress〉一節 ↩︎

讓 macOS 根據不同 Domain 選擇 DNS 伺服器

TL;DR

/etc/resolver/ 下建立與目標 Domain 同名的設定檔。

背景

由於在 macOS 設定 DNS resolver 的時候,OS 選擇查詢的 resolver 可能不是按照順序的,所以只要有任何一個 resolver 搶先回 NXDOMAIN 就會讓你的查詢找不到 Domain,這對一些有非公開網域或偷偷自定網域的人們來說十分的困擾。

一般來說,我們可能會想讓 Domain Server 分成內外兩組,把只有自己查得到的 domain 讓內部的 DNS 來解析,剩餘的其他網域則是讓外部的 DNS 來解析就好了,省去讓內部 DNS 做 recursive 的麻煩。

但由於 macOS DNS resolving 的設計(或我們說 BSD 的設計),使得直接將全域 DNS 設定為內部優先,外部次之的這件事情變得無法進行,此時可以利用手動設定的方式來達到將不同 domain 指定到不同 DNS 的目的。

resolver(5)

man 5 resolver[1] 裡面有提到一件事情,/etc/resolv.conf 記錄的是「主要的」DNS resolver 設定[2],但其實所有的設定除了會從 /etc/resolv.conf 讀取以外,還會從 /etc/resolver/ 目錄下面讀取[3]

對於這個目錄裡面的設定檔除了與 resolv.conf 格式一致以外,還有一個限制是檔名必須與要搜尋的 domain 同名,在關於 domain 的這個設定值的說明是這樣說的:

Domain

Domain name associated with this resolver configuration. This option is normally not required by the Mac OS X DNS search system when the resolver configuration is read from a file in the /etc/resolver directory.
In that case the file name is used as the domain name.

However, "domain" must be provided when there are multiple resolver clients for the same domain name, since multiple files may not exist having the same name.

也就是說,我們可以在 /etc/resolver/ 下建立於我們想要額外設定個別網域的 resolver 設定檔。方法也很簡單,我們接下來就來嘗試做點實驗:

/etc/resolver/

假定我們想要個別設定的網域為 davy.home(當然,這個網域實際上並不存在於這個世界上),那麼我們只需要建立 /etc/resolver/davy.home[4] 這個檔案即可,內容如下:

domain davy.home
search davy.home
nameserver 10.10.10.10
nameserver 10.10.10.100

在這個範例中,我設定了兩組 nameserver 分別是 10.10.10.1010.10.10.100,大家可以根據自己的實際情況設定。

雖然文件中寫說 domain 在 macOS 中不是必須的,但我還是按照填寫的規則寫上了 davy.home,各位讀者可以自行嘗試將此欄位移除是否仍然會生效。

接下來我們會清除 macOS 的 DNS cache 並檢查看看設定後的結果是否與我們想像的一樣:

# killall -HUP mDNSResponder
$ scutil --dns
DNS configuration
...
resolver #8
  domain   : davy.home
  search domain[0] : davy.home
  nameserver[0] : 10.10.10.10
  nameserver[1] : 10.10.10.100
  flags    : Request A records
  reach    : 0x00000002 (Reachable)
...
$

透過 scutil[5] 可以確認系統的 resolver 已經特化出給 davy.home 使用的設定了,此時大家就可以在不改變 Global resolver 的情況下來穩定查詢 davy.home 了。

結語

透過獨立 DNS resolver 的設定讓我們可以指定不同網域的查詢方式,也可以讓我們的 Domain Server 減去了不必要的 recursion 的負擔,對於我們這種可能會有一些實驗用的私人網域來說,比起其他系統會循序查詢 resolver 的方式真的是方便了不少。


  1. 此處指的是 macOS 的 resolver(5) 條目,可以直接透過 man 5 resolver 查閱內容,或參考 FreeBSD 收錄的檔案內容 ↩︎

  2. 原文為 "Note that the /etc/resolv.conf file, which contains configuration for the default (or "primary") DNS resolver client, is maintained automatically by Mac OS X and should not be edited manually. Changes to the DNS configuration should be made by using the Network Preferences panel." ↩︎

  3. 原文為 "These are at present located by the system in the /etc/resolv.conf file and in the files found in the /etc/resolver directory." ↩︎

  4. 預設情況下 /etc/resolver/ 這個目錄並不存在,大家自行新增即可 ↩︎

  5. 關於 scutil 的使用說明可以從 man 8 scutilFreeBSD 的收錄的說明查閱 ↩︎