cert-managerをWorkload Identityに対応させる

MF KESSAIでは、一部社内で、cert-managerを利用しており、利用中のGKE(Google Kubernetes Engine)でWorkload Identity化をすすめるにあたってぶつかった壁があり暫定的ではあるものの解決したため、その方法を残しておきたいと思います。

社内で記事を温めてる間に、Documentを更新するPRが生まれてマージされました。 そのため公開するか悩みましたが日本語記事として読んでもらえればと思います。

この記事の背景

cert-managerのDNS01 Challengeを使ってGKEで証明書を生成するには、現時点では以下の通り設定する必要があります 。

apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: example-issuer
spec:
  acme:
    ...
    solvers:
    - dns01:
        clouddns:
          # The ID of the GCP project
          project: $PROJECT_ID
          # This is the secret used to access the service account
          serviceAccountSecretRef:
            name: clouddns-dns01-solver-svc-acct
            key: key.json
  • spec.acme.solvers[0].dns01.clouddns.project にはDNSが存在するGCP ProjectID
  • spec.acme.solvers[0].dns01.clouddns.serviceAccountSecretRef にはDNSを操作する権限を持ったGSA(GCP Service Account)の鍵情報を登録したk8s secretとkey名

しかし現在のGKEでは、Workload Identityという仕組みを使うことで、k8s secretを介する事なくGSAの認証情報をPodが使えるようにする仕組みが推奨となっています。

対応方法

実は同様のIssueが存在しており、解決方法が提示されていたので、そちらの説明をしていく事になります。

Step1: KSA(Kubernetes Service Account)を作成or特定する

[email protected]:~ (hoge)$ kubectl get deployment cert-manager -n cert-manager -o yaml | grep serviceAccount
      serviceAccount: cert-manager
      serviceAccountName: cert-manager

すでにcertmanagerには cert-manager というKSAが作成され設定されています。

Step2: GSAの作成or更新する

弊社ではすでにk8s secret経由で設定する従来の方法で利用していたため、すでにGSAは作成済みでしたので弊社では更新のみ行いました。 ここでは手順として残しておきます。 https://github.com/jetstack/cert-manager/issues/3009#issuecomment-656663316 に書かれている内容と同じになります。

$ export PROJECT_ID=myproject-id
$ export $GOOGLE_SERVICE_ACCOUNT_NAME=myserviceaccount-name

Create service account
$ gcloud iam service-accounts create $GOOGLE_SERVICE_ACCOUNT_NAME --display-name "$GOOGLE_SERVICE_ACCOUNT_NAME"

DNS role
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
   --member serviceAccount:[email protected]$PROJECT_ID.iam.gserviceaccount.com \
   --role roles/dns.admin

Workload identity
$ gcloud iam service-accounts add-iam-policy-binding \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:$PROJECT_ID.svc.id.goog[cert-manager/cert-manager]" \
  [email protected]$PROJECT_ID.iam.gserviceaccount.com

これでGCP IAM側での作業は完了です。

Step3: 最後のKSAにannotationsを追加する

$ kubectl patch serviceaccount cert-manager -p '{"metadata": {"annotations":{"iam.gke.io/gcp-service-account":"[email protected]$PROJECT_ID.iam.gserviceaccount.com"}}}' -n cert-manager

これで、KSAの cert-manager を利用するcert-manager podが、Step2で作成したGSAの認証を利用してDNSを操作できる形になりました。

Step4: ClusterIssuer or Issuerの設定変更

Step3まででSecretの利用が不要になりましたので、最終的に記述内容は以下の通りです。

apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: example-issuer
spec:
  acme:
    ...
    solvers:
    - dns01:
        clouddns:
          # The ID of the GCP project
          project: $PROJECT_ID
-          # This is the secret used to access the service account
-          serviceAccountSecretRef:
-            name: clouddns-dns01-solver-svc-acct
-            key: key.json

連携が失敗してると

cert-manager/controller/challenges "msg"="re-queuing item  due to error processing" "error"="GoogleCloud API call failed: Get https://www.googleapis.com/dns/v1/projects/dnsproject/managedZones?alt=json\u0026dnsName=example.com.\u0026prettyPrint=false: compute: Received 403 `
Unable to generate token; 
IAM returned 403 Forbidden: The caller does not have permission
This error could be caused by a missing IAM policy binding on the target IAM service account.

You can create the necessary policy binding with:

gcloud iam service-accounts add-iam-policy-binding \\
--role=roles/iam.workloadIdentityUser \\
--member=\"serviceAccount:hoge.svc.id.goog[cert-manager/cert-manager]\" \\
[email protected]\n\nFor more information, refer to the Workload Identity documentation:

https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity`" "key"="hoge-cert-927063497-0" 

この様にGSAに対して、KSAをroles/iam.workloadIdentityUser Roleでmemberに追加してくださいと怒られます。そして対応したら問題なく証明書の作成が始まります。

cert-managerの実装を見てみよう

なぜ設定に必要そうな鍵情報を削除して動くのかコードを見てみましょう。

pkg/issuer/acme/dns/clouddns/clouddns.go#L80

func NewDNSProviderCredentials(project string, dns01Nameservers []string, hostedZoneName string) (*DNSProvider, error) {
..
    client, err := google.DefaultClient(ctx, dns.NdevClouddnsReadwriteScope) |
..
}

CloudDNSのProviderを作成する際に、内部で、google.DefaultClient を呼ばれているためWorkload Identityを利用できています。

最後に

この手順では、helm を利用してcert-managerインストール後にpatchを当てる必要がありました。cert-manager 側が対応するまで待つか、helmを使わずにkustomizeを利用するなどしてpatchまでKSAに当てておくと良いかもしれませんね。