こんにちは。基盤チームのakaimoです。
現在リネットではTLS証明書にLet's Encryptを使っていて、その管理にcert-managerを導入しています。
先日、Let's Encryptのインシデントにより証明書の強制失効が行われました。
幸いリネットは対象外でしたが、cert-managerで管理している証明書をcert-managerが設定した更新のスケジュールが来る前に、ダウンタイムを発生させずに任意のタイミングで更新する方法を知らないで運用するのはリスクだと感じたため、やり方を調べました。
Let's Encryptのインシデントの対象者チェックツール
公式のドキュメントを読んだところ、手動で更新をかけられるような記載は見当たりませんでした。
しかし、今回のインシデントの対象者かチェックするツールをcert-managerの開発者が公開しており、このツールを使って対象者であることが判明した場合は更新をかけることができるようです。
このツール自体の使い方は簡単で、cert-managerにアクセスする権限のあるkubectlが存在するサーバーに、バイナリとインシデントの対象者一覧をダウンロードして実行するだけです。
$ wget -O letsencrypt-caa-bug-checker https://github.com/jetstack/letsencrypt-caa-bug-checker/releases/download/v0.0.2/letsencrypt-caa-bug-checker-linux-amd64 $ chmod 755 letsencrypt-caa-bug-checker $ wget -c https://d4twhgtvn0ff5.cloudfront.net/caa-rechecking-incident-affected-serials.txt.gz $ zcat < caa-rechecking-incident-affected-serials.txt.gz > serials.txt $ ./letsencrypt-caa-bug-checker --affected-serials-file serials.txt
renewオプション
--renew
オプションをつけて実行すれば更新も行うとドキュメントに記載されていたため、その処理を再現すれば任意のタイミングで更新をかけられそうなので調べてみます。
チェックツール自体はmain.go
だけのシンプルな構成で、--renew
オプションによるフラグがあり、そのフラグのチェック後に実行される部分にrenewCertificate
という関数があります。
そこで更新の処理を行っているようです。
renewCertificate関数
renewCertificate
はk8sのクライアントとcert-managerが管理する証明書の構造体を引数として受け取っています。
func renewCertificate(ctx context.Context, cl client.Client, cert capi.Certificate) error
https://github.com/jetstack/letsencrypt-caa-bug-checker/blob/v0.0.2/main.go#L148
まず、更新する証明書のCertificateRequest
を取得し、処理中でなければ削除しています。
var requests capi.CertificateRequestList if err := cl.List(ctx, &requests, client.InNamespace(cert.Namespace)); err != nil { return err } for _, req := range requests.Items { // If any existing CertificateRequest resources exist and are complete, // we delete them to avoid a re-issuance of the same certificate. if !metav1.IsControlledBy(&req, &cert) { continue } // This indicates an issuance is currently in progress if len(req.Status.Certificate) == 0 { log.Printf("Found existing CertificateRequest %s/%s for Certificate - skipping triggering a renewal...", req.Namespace, req.Name) return nil } if err := cl.Delete(ctx, &req); err != nil { log.Printf("Failed to delete old CertificateRequest %s/%s for Certificate", req.Namespace, req.Name) return err } log.Printf("Deleted old CertificateRequest %s/%s for Certificate", req.Namespace, req.Name) }
https://github.com/jetstack/letsencrypt-caa-bug-checker/blob/v0.0.2/main.go#L150
次に証明書が入ったSecret
を取得しIssuerNameAnnotationKey
というannotationをアップデートしています。
コメントを読む限り、これで証明書の更新が行われるようです。
// Fetch an up to date copy of the Secret resource for this Certificate var secret core.Secret if err := cl.Get(ctx, client.ObjectKey{Namespace: cert.Namespace, Name: cert.Spec.SecretName}, &secret); err != nil { log.Printf("Failed to retrieve up-to-date copy of existing Secret resource for Certificate: %v", err) return err } // Manually override/set the IssuerNameAnnotationKey - this will cause cert-manager // to assume that we have changed the 'issuerRef' specified on the Certificate and // trigger a one-time renewal. if secret.Annotations == nil { secret.Annotations = make(map[string]string) } secret.Annotations[capi.IssuerNameAnnotationKey] = "force-renewal-triggered" if err := cl.Update(ctx, &secret); err != nil { log.Printf("Failed to update Secret resource for Certificate: %v", err) return err }
https://github.com/jetstack/letsencrypt-caa-bug-checker/blob/v0.0.2/main.go#L176
更新処理が行われると、はじめに消したCertificateRequest
が作成され、これをもって更新処理が始まったとしているようです。
log.Printf("Triggered renewal of Certificate - waiting for new CertificateRequest resource to be created...") // Wait for a CertificateRequest resource to be created err := wait.Poll(time.Second, time.Minute, func() (bool, error) { var requests capi.CertificateRequestList if err := cl.List(ctx, &requests, client.InNamespace(cert.Namespace)); err != nil { return false, err } // Wait for a CertificateRequest owned by this Certificate to exist for _, req := range requests.Items { if metav1.IsControlledBy(&req, &cert) { log.Printf("CertificateRequest %s/%s found, renewal in progress!", req.Namespace, req.Name) return true, nil } } return false, nil })
https://github.com/jetstack/letsencrypt-caa-bug-checker/blob/v0.0.2/main.go#L197
実際にやってみる
開発用のAPIに向けて実際に手動でやってみたいと思います。
手順としては以下のようになります。
- 更新したい証明書の
CertificateRequest
を削除 - 更新したい証明書の
Secret
のAnnotationcert-manager.io/issuer-name
をforce-renewal-triggered
に書き換え
実行すると多数のログが出力されますが、以下のようなログがでていれば更新が行われています。
I0310 09:44:42.874306 1 sync.go:367] cert-manager/controller/certificates "msg"="no existing CertificateRequest resource exists, creating new request..." I0310 09:44:42.920794 1 sync.go:379] cert-manager/controller/certificates "msg"="created certificate request" I0310 09:44:46.881652 1 logger.go:75] Calling GetAuthorization I0310 09:44:47.673297 1 logger.go:100] Calling DNS01ChallengeRecord
更新されたSecretの中にある証明書の期限を見てみると以下のようになっていました。
無事に更新されています。
終わりに
無事に手動で更新ができ、似たような事態に遭遇した場合でもダウンタイムなしに対応できそうです。
また、今回のやり方を開発者はhacks
と言っており、今後のリリースで正式な手段で再取得する方法をリリースすると言っています。
as part of #2402 we intend to introduce a mechanism that allows users or external controllers to trigger a renewal/reissuance in a well reasoned/sane way (unlike some of the recent 'hacks' we've had to resort to up until now).