内向きDNSサーバの影響下にあるkubernetes環境でcert-manager(DNS01)使ったら苦労した話

# お忙しい人向け
* cert-managerのpod内でdig使って、DNSチャレンジトークンが本当に取得できるのかを確認する
* DNSチャレンジトークンが取得できない場合、cert-managerに```--set podDnsPolicy=None```や```--set podDnsConfig.nameservers[0]=8.8.8.8```の設定を行うことで改善する場合がある。

# 経緯
とある日、Let's Encryptからメールが届く、
「お前の証明書、もうすぐ死ぬで」
証明書の維持はcert-managerに任せていたはずなのに...！？
彼（彼女？）の無事を確かめるべく、私は調査を開始した...
<br>
<br>
と、ポエムで始まったところで、以下が調査の記録と対応内容になります。

# 調査内容
## リソースの状態の確認
まずは、cert-managerが証明書更新を行うために自動生成する以下のリソースの状態を調査しました。

* CertificateRequest
* Order
* Challenge

すると、Challengeリソースのイベントログに
```Waiting for dns-01 challenge propagation: DNS record for “[domain-name]” not yet propagated```
が表示され続けていることに気づきます。いくら待ってもこの状態から変わりません。
ここで「cert-managerからDNSチャレンジトークンが見えてないのでは...？」と気づきました。

<details><summary>Challengeリソース、DNSチャレンジトークンとは</summary><div>
Challengeリソースとは、ACMEプロトコル[^1]によって証明書を作成する一連の流れの状態をcert-managerに格納するリソースになります（~~大雑把~~）。
証明書更新が必要な際にcert-managerによって自動的に生成され（CertificateRequest -> Order -> Challengeの順）、証明書作成完了後に時間が経つと削除されます。
ACMEプロトコル[^1]では、以下のようにドメインの所有権を確認するための方式が2つあります。

* チャレンジトークンファイルが対象ドメインのWEBサーバに存在するかチェックすることで行う方式（HTTP01）
* 対象ドメインのDNSレコードにチャレンジトークン（TXTレコード）が存在するかチェックすることで行う方式(DNS01)

DNSチャレンジトークンは、DNS01でドメインの所有を確認するために用いられる文字列のことを指します。

[^1]: ACMEプロトコル<br>![acme-protocol.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63086/7b84b7f0-cafe-9110-c666-10cd96cc3e17.png)
</div></details>

## cert-managerのpod内でdig使って、DNSチャレンジトークンが本当に伝搬されているのかを確認する
```dig @8.8.8.8 -t TXT _acme-challenge.[domain-name]```
を実行すると、チャレンジトークンが取得できるのに、
```dig -t TXT _acme-challenge.[domain-name]```
を実行してもチャレンジトークンが取得できないことを確認しました。
これはもしや、LAN内でのDNS解決に問題がある...？

## 内向きDNSサーバの設定を確認
現環境ではdnsmasqを使用して内向きDNSサーバを構築していたのですが、これに以下の設定をしていました。
```domain-needed```
さらにkubernetes内部のcorednsにも、[domain-name]のリクエストは内向きDNSサーバに流れるように設定していたため、cert-managerが_acme-challenge.[domain-name]のリクエストをした際には以下のような流れになってしまい、外部にあるDNSサーバに問い合わせることができず、いつまでもチャレンジトークンの確認が完了しない、ということになっていたようでした。

1. cert-managerが_acme-challenge.[domain-name]をリクエスト
2. corednsの設定によって、内向きDNSサーバに流される
3. 内向きDNSサーバでは[domain-name]ドメインの名前解決の際、```domain-needed```によって外部への問い合わせは行わない
4. _acme-challenge.[domain-name]をリクエスト結果はエラーになる

# 考察
ACMEプロトコル[^1]のフローだけを見ると_acme-challenge.[domain-name]の確認はACMEサーバだけができればよいように見えますが、おそらくACMEサーバのリソースを無駄にしないため、ACMEサーバへチャレンジトークンの確認要求を出す前に、cert-managerが_acme-challenge.[domain-name]の確認を済ませるようにしているのだと推測されます。
これまでの調査によって内向きDNSサーバの設定により、cert-managerが_acme-challenge.[domain-name]の確認を完了することができないことが分かっているため、この状態だとACMEサーバにチャレンジトークンの確認要求を出すことができず、ACMEプロトコルが終了しないので、証明書の発行ができないのだと思われます。

# 対策
cert-managerが_acme-challenge.[domain-name]の確認を行えるようにすることが肝要なので、以下のような対策が考えられます。

* 内向きDNSサーバやcorednsの設定により、_acme-challenge.[domain-name]のリクエストを外に飛ばせるようにする。
* cert-managerのpod設定について```podDnsPolicy=None```、```podDnsConfig.nameservers[0]=8.8.8.8```を設定し、cert-mamnagerが名前解決を行う際に強制的に外に飛ばすようにする。

私の場合は、cert-managerをhelmにてインストールする際に```--set podDnsPolicy=None```、```--set podDnsConfig.nameservers[0]=8.8.8.8```を指定して、cert-mamnagerが名前解決を行う際に強制的に外に飛ばすようにすることで無事に証明書発行が行えるようになりました。もし同じような問題で困っている方がいらっしゃったら、この記事が役に立つことを願っています。

# オチ
cert-managerで、さくらのクラウドのDNSを利用したDNS01方式の証明書発行ができるようになる非公式cert-manager-webhook実装 [cert-manager-webhook-sacloud](https://github.com/snmagn/cert-manager-webhook-sacloud/) を公開しています。
興味のある方はお試しください！（ダイマ）