## page was renamed from SSL証明書/Let's EncryptでSSL証明書の新規取得と自動更新(dns-01編) = Let's EncryptでSSL証明書の新規作成と自動更新(dns-01編) = == Let's Encrypt とは == <> == dns-01 とは == * ドメイン所有者を証明するための「チャレンジ・レスポンス」のやりとりをHTTP経由(`http-01`)ではなく、DNS経由(`dns-01`)で行う方法である。 * 以下のような環境では、この方法しか採れないケースもある。 * ワイルドカード証明書を取得する場合。 * イントラネット内のサーバーでインターネットに公開してない場合。 * アプライアンス機器等で、HTTPの直接ランディングが不可能である場合。 * 特定プロトコル(メールサーバー等)ユースでHTTPでのランディングができない/したくない場合など。 * もちろんデメリットもある。 * `http-01` との比較で手順が複雑化する(DNSコンテンツサーバーへの更新が標準化されていないため)。 * `RFC2136`(`Dynamic Updates in the Domain Name System`)で更新手順が標準化されてるじゃないか! * というあなたは [[https://github.com/lukas2511/dehydrated/wiki/Examples-for-DNS-01-hooks|現実]] を見た方がいい。 * DNSコンテンツサーバーとの連携が必須でそちらをなんとかしないとどうしようもない。 * とは言え、一度この味を知ってしまうと`http-01`手順には戻れなくなること請け合いである:-)。 * どれくらい戻れないかというと、Webサーバーに対しても`dns-01`手順を適用したくなってしまうくらい中毒性がある:-)。 == 目次 == <> == 検証環境 == * 以下のソフトウェアの利用を前提に検証を実施した。いずれも最新のリリースということで確認しているが、ある程度古い環境でも問題無いと思われる。 * OS: `FreeBSD 12.1-R` * ACMEクライアント: `dehydrated 0.6.5` ※少なくとも`0.6.2`以降が必須 * DNSダイナミックアップデートクライアント: `BIND 9.14.8`(`nsupdate`コマンド) * DNSコンテンツサーバー: `BIND 9.14.8` * SSL証明書利用サーバーの利用詳細についてはここでは言及しない。 * 上記以外の環境では、以下の点に相違が発生する。必要に応じて読み替えたし。 * インストール方法 * インストールされるディレクトリ * 自動更新のための手続きとその設定 * 逆に以下の点は参考にできる。 * 設定パラメータとその意味 * 運用事例 == 検証作業内容 == * 本例では、`www.example.jp`というサイトに対して証明書を発行するものとする。 * チャレンジ・レスポンスコードをDNSダイナミックアップデートするため、`example.jp`ゾーンに対するDNSコンテンツサーバーへの更新権限があるものとする。 * BINDの場合「DNSダイナミックアップデート」を許可する(`allow-update`)だけでは、許可したアクセス元からの「ゾーン」に対するあらゆるレコードの変更ができてしまうので注意。 * その辺りは`update-policy`機能(`allow-update`機能とは排他)により、ある程度制限することができる。 * しかし`update-policy`では、アクセス元制限ができないという罠もあり多少の妥協は致し方ないところ。 === 想定サーバー・ドメイン === * DNSコンテンツサーバーは`ns.example.jp`とする。 * 実際には複数のDNSコンテンツサーバー(いわゆるセカンダリサーバー)で運用されていると思う。 * それらサーバーへの反映は`ns.example.jp`からの`notify yes;`およびIXFR(`Incremental Zone Transfer`)により、全てのセカンダリサーバーへ、即時かつ遅滞なくデプロイされるものとする。 * それら運用上の詳細については既に設定されているものとして、ここでは取り扱わない。 * SSLサーバーは`www.example.jp`とする。 * 上記サーバー(DNSコンテンツサーバー・SSLサーバー)は同一でもかまわないし、別々でもかまわない。 === DNSコンテンツサーバー側 === * DNSサーバーは [[https://www.freshports.org/dns/bind911|ports/dns/bind911]] をインストールする。 === SSLサーバー側 === * DNSダイナミックアップデートクライアントは [[https://www.freshports.org/dns/bind-tools|ports/dns/bind-tools]] をインストールする。 * 古いFreeBSD標準(9.x等)の`nsupdate`コマンドはTSIGは取り扱えないため、`bind-tools`を別途インストールする必要があるが、もう今更だよね。 * ACMEクライアントは [[https://www.freshports.org/security/dehydrated|ports/security/dehydrated]] をインストールしておく。 == インストール == * いずれも`ports/security/dehydrated`、`ports/dns/bind-utils`よりインストールする。 * オプションの選択によって手順が変わる点は無いため、ここでは明示しない。 = DNSコンテンツサーバー側の設定 = * [[SSL証明書/Let's EncryptでSSL証明書の新規取得と自動更新(http-01編)|Let's EncryptでSSL証明書の新規取得と自動更新(http-01編)]]で実施した「ドメイン所有者確認トークンディレクトリの指定」の代わりの作業となる。 * よってディレクトリ作成作業は不要である。 * しかし設定ファイル(`named.conf`)やゾーンファイル(`example.jp.db`)の設置場所等に工夫が必要になる。 == /usr/local/etc/namedb/named.conf(一部) == {{{ include "/usr/local/etc/namedb/ns-www.key"; zone "example.jp" { type master; file "/usr/local/etc/namedb/dynamic/example.jp.db"; update-policy { grant ns-www. name _acme-challenge.www.example.jp. TXT; }; }; }}} * 「DNSコンテンツサーバー」と「SSLサーバー」とで、TSIG(`Transaction SIGnature`)キーを共有する。 * TSIGキーファイルはキー名と秘密鍵で構成された、`named.conf`の書式に準拠したテキストファイルである。 * このTSIGキーを「`/usr/local/etc/namedb/ns-www.key`」という名前で保存しておく(所有者は`bind:wheel`、パーミッションは`0400`で)。 * また、TSIGキー名に対して、更新許可設定を与える(`update-policy`および`grant`)。 * また変更できるレコード名およびリソースレコードを限定する(`name _acme-challenge.www.example.jp. TXT`)。 * 残念なことに`update-policy`ではアクセス元制限ができないので、キーファイル(`secret`)の取り扱い(流出)については注意すること。 * なお`allow-update`は`update-policy`とは排他であるため、両方設定することはできない。 == /usr/local/etc/namedb/dynamic/example.jp.db(example.jp ゾーンファイル) == {{{ $TTL 300 @ IN SOA ns.example.jp. domain.example.jp. ( 2017032201 ; serial 7200 ; refresh (2 hours) 900 ; retry (15 minutes) 2419200 ; expire (4 weeks) 86400 ; minimum (1 day) ) IN NS ns ns IN A 192.0.2.1 }}} * 本ファイルの設置場所、命名規則はそれぞれのポリシーに従う(本例では ゾーン名.db とした)。 * DNSダイナミックアップデート対象となるゾーンは`/usr/local/etc/namedb/dynamic/`ディレクトリ以下に設置されるものとしている。 == /usr/local/etc/namedb/ns-www.key(TSIGキーファイル) == {{{ key "ns-www." { algorithm hmac-sha256; secret "PfzeGvXiOqtPOwQJY/iNFrvlD3/eKAHRZ0TbyK5GYII="; }; }}} 上記ファイルは以下の手順にて生成することができる。 {{{#!highlight bash tsig-keygen -a hmac-sha256 ns-www. > /usr/local/etc/namedb/ns-www.key chown bind:wheel /usr/local/etc/namedb/ns-www.key chmod 0400 /usr/local/etc/namedb/ns-www.key }}} * もちろん secret の部分は毎回【【ランダム】】に発行される(違うキーで同じシークレットを使ってはいけません)。 * このファイルは`named.conf`でも、(後で説明する)`nsupdate`コマンド(`-k`オプションで)でもそのまま解釈してくれる。 * 本ファイルの設置場所、命名規則については一概に言えることが無く、「ポリシーで」で逃げるには無責任すぎるので、以下に例を出してみる。 === 本例における具体的設定例 === * BIND側に設置する場合は、`/usr/local/etc/namedb/`ディレクトリに設置することとする。 * `/usr/local/etc/namedb/ns-www.key`(`owner:group=bind:wheel`、`mode=0400`) * dehydrated側に設置する場合は、`/usr/local/etc/dehydrated/`ディレクトリに設置することとする。 * `/usr/local/etc/dehydrated/ns-www.key`(`owner:group=root:wheel`、`mode=0400`) * ファイル名についてだが、「キー名.key」とするのが違和感なくていいと思う。 * 肝心のキー名だが、[[https://ftp.isc.org/isc/bind9/cur/9.11/doc/arm/Bv9ARM.ch04.html#tsig|BIND9.11のマニュアル(TSIG)]]によれば「ホスト名1-ホスト名2.」という例がある。 * 本気かどうかわからないが、「DNSコンテンツサーバー-ダイナミックアップデートするサーバー.」というニュアンスになる。 * 本件の場合、`ns.example.jp`と`www.example.jp`であることから「`ns-www.`」とするのが妥当(ほんと?)。 * まぁなんでもいいけど、わかりやすいようにね。 = SSLサーバー側の設定 = * ほとんど[[SSL証明書/Let's EncryptでSSL証明書の新規取得と自動更新(http-01編)|Let's EncryptでSSL証明書の新規取得と自動更新(http-01編)]]で実施した作業と同じになる。 * 明確に違う点は、先のページでは解説してない「`HOOK`」設定となる。 * ここでは一通り作業の意味がわかってる前提で、一通り設定を紹介する。 == /etc/periodic.conf == {{{ weekly_dehydrated_enable="YES" }}} 自動更新設定(YES=自動更新する)。 [[https://www.freebsd.org/cgi/man.cgi?periodic(8)|periodic(8)]]にある通り、毎週土曜日3時に実行される。 なお今回、`weekly_dehydrated_deployscript`は指定しない(後述の HOOK 設定参照のこと)。 == /usr/local/etc/dehydrated/ns-www.key(TSIGキーファイル) == これは先に`tsig-keygen`コマンドで作成されたファイルである。 DNSコンテンツサーバーと同一になるようコピーするなどして設定すること。 その際のオーナー・グループ・パーミッションは以下の通りである。 {{{ chown root:wheel /usr/local/etc/dehydrated/ns-www.key chmod 0400 /usr/local/etc/dehydrated/ns-www.key }}} == /usr/local/etc/dehydrated/config == {{{ CHALLENGETYPE="dns-01" HOOK="${BASEDIR}/hook.sh" RENEW_DAYS="30" KEY_ALGO="rsa" KEYSIZE="2048" #KEY_ALGO="prime256v1" CONTACT_EMAIL="メールアドレス" #テスト発行したい場合、以下の行を有効にすること。 #CA="https://acme-staging-v02.api.letsencrypt.org/directory" }}} * `http-01` との時との大きな違いは`CHALLENGETYPE`と`HOOK`設定にある。 * `HOOK`設定(によって指定されるファイル)については後述する。 * `CHALLENGETYPE`には`dns-01`を指定する。 * `CHALLENGETYPE`は現在`http-01`か`dns-01`、`tls-alpn-01`の3つしか選択肢は無い。 == /usr/local/etc/dehydrated/domains.txt == {{{ www.example.jp }}} 本ファイルの設定については [[/SSL証明書/Let's EncryptでSSL証明書の新規取得と自動更新(http-01編)#A.2BMLMw4jDzMM0w.2FDDgMNUwoTCkMOs-|コモンネームの設定]]に準拠するものとする(例)。 === ワイルドカード証明書 === [[https://github.com/lukas2511/dehydrated/blob/master/docs/domains_txt.md|v0.6.0 以降でワイルドカード証明書を取得したい]]場合の`domains.txt`の書き方は下記の通りです(`star_example_jp` は一例)。 {{{ *.example.jp > star_example_jp }}} * 味噌は「 > 」で上記例では`star_example_jp`というエイリアス設定(サブディレクトリ名として使用)してやる必要があります。 * エイリアス設定しない場合、エイリアス設定でも「`*.`」で始まる場合、エラーとなります(途中に`*`が入ってても問題無い!?)。 * まぁシェルスクリプトで、パス名に`*`が含まれる場合の取り扱いは面倒なので、このような仕組みになってるのだと思います。 * このエイリアスの指定は今までの指定方法でも使用できますが、わざわざ設定する必要はないでしょう。 * 一行中の複数の「`>`」の指定はエラーです(エイリアスは一行に一個だけ)。 * なお二行以上の設定(違うコモンネーム)に対して同一エイリアスを設定した場合の振る舞いについては未調査です:-)。 またベストプラクティクス的にははSANsによる設定がスマートかと思われます(`example.jp`をコモンネームとする)。 {{{ example.jp *.example.jp }}} === RSA/ECDSAハイブリッド証明書 === 同一コモンネームで複数キーアルゴリズムの鍵を取得したい場合は、下記の通りになります。 ハイブリッドといっても、一つの証明書の中に複数のキーアルゴリズムが入ってるわけでなく、複数の証明書で使い分ける話となります。 {{{ example.jp > example.jp.rsa2048 example.jp > example.jp.prime256v1 }}} エイリアス機能により区別ができればいいので、エイリアス名はなんでもいいのですが、 `/usr/local/etc/dehydrated/config` で指定できるキーアルゴリズムは一つしか指定できません。 つまりこのままではどちらも同じキーアルゴリズムで取得することになるので、期待した結果は得られません。 エイリアス単位で「カスタマイズ」するために、`/usr/local/etc/dehydrated/certs/エイリアス名/config` ファイルにてカスタムしたい項目を上書き設定してやります。 {{{#!highlight console # cat /usr/local/etc/dehydrated/certs/example.jp.rsa2048/config KEY_ALGO="rsa" KEYSIZE="2048" # cat /usr/local/etc/dehydrated/certs/example.jp.prime256v1/config KEY_ALGO="prime256v1" }}} セットアップした直後には `/usr/local/etc/dehydrated/certs/エイリアス名` ディレクトリが存在しないため、事前に作成しておきます。 {{{#!highlight bash mkdir -p 0700 /usr/local/etc/dehydrated/certs/エイリアス名 }}} == /usr/local/etc/dehydrated/hook.sh == {{{ #!/usr/local/bin/bash TTL="300" DNSSERVER="ns.example.jp" alias nsupdate="/usr/local/bin/nsupdate -k ${BASEDIR}/ns-www.key" deploy_challenge { local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" printf 'server %s\nupdate add _acme-challenge.%s. %d TXT "%s"\nsend\n' "${DNSSERVER}" "${DOMAIN}" "${TTL}" "${TOKEN_VALUE}" | nsupdate } clean_challenge { local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" printf 'server %s\nupdate delete _acme-challenge.%s. TXT\nsend\n' "${DNSSERVER}" "${DOMAIN}" | nsupdate } deploy_cert { /usr/sbin/service apache24 restart && /usr/local/bin/dehydrated -gc } HANDLER="$1"; shift if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert)$ ]]; then "$HANDLER" "$@" fi }}} * フック(HOOK)ファイルのひな形としては`/usr/local/etc/dehydrated/hook.sh.example`を参照すること。 * またほとんどのフックは「何もしない」ので、バージョンアップによる拡張の影響を考慮して、使用しているフック以外は無視するようプログラムしましょう。 == /usr/local/etc/dehydrated/deploy.sh == * 本ケースでは`deploy.sh`を取り扱わない(無くてもいい)。 * これは`HOOK`で指定するシェルスクリプトの deploy_cert シェル関数で代替するからである。 * HOOK(`/usr/local/etc/dehydrated/config`)と`weekly_dehydrated_deployscript`(`/etc/periodic.conf`)の違いは以下の通りである。 * 証明書取得する単位毎に実行されるのが HOOK、全て取得終って実行されるのが`weekly_dehydrated_deployscript`。 * 証明書取得に成功した・失敗した(または更新なし)がわかるのが HOOK、わからないのが`weekly_dehydrated_deployscript`。 * よって、1枚の証明書を相手にする時はそう違いは無いが、複数枚を取得して分散デプロイしたいなら HOOK しかない。 * ファインチューニング(更新ない時は何もしないなど)したいなら HOOK 一択。 * 逆に HOOK はデプロイだけしたいユースケースにおいて若干ながら大味(ダミーの関数を置く必要がある)である。 * この HOOK はバージョンによって拡張されるので、未知の呼び出しに対してはエラーにならないよう、ケアする必要がある。 = 【付録】ゾーン分割によるダイナミックアップデートの制限 = * 以下のケースのどれか(`and/or`)が該当するなら、一工夫必要になる。 * DNSコンテンツサーバーへの更新権限が無い/得られない場合。 * 本例レベルの「ゾーン」に対して更新を許可するには広すぎる(セキュリティ的・ポリシー的)ので狭くしたい場合。 * なお実現のためには「委任」が必須なので、そこは調整すること。 * 今回`_acme-challenge.www.example.jp`ゾーン(に分けて/委任してもらって)に、更新が新しいゾーン内に閉じるよう制限してみた。 * ここでは敢えて「`example.jp`」ゾーンから「`_acme-challenge.www.example.jp`」を自分自身(DNSコンテンツサーバー)へ委任するものとし、外部のDNSコンテンツサーバーへは向けないものとする。 * ここまでお膳立てが整えられていれば例えば、サービスゾーンのDNSコンテンツサーバーへの更新権限は得られなくても、自前のDNSコンテンツサーバーに委任してもらって、更新できるようにするのは難しくないと思う。 == /usr/local/etc/namedb/named.conf(一部) == {{{ include "/usr/local/etc/namedb/ns-www.key"; zone "example.jp" { type master; file "/usr/local/etc/namedb/master/example.jp.db"; }; zone "_acme-challenge.www.example.jp" { type master; file "/usr/local/etc/namedb/dynamic/_acme-challenge.www.example.jp.db"; update-policy { grant ns-www. name _acme-challenge.www.example.jp. TXT; }; }; }}} == /usr/local/etc/namedb/master/example.jp.db(example.jp ゾーンファイル) == {{{ $TTL 300 @ IN SOA ns.example.jp. domain.example.jp. ( 2017032201 ; serial 7200 ; refresh (2 hours) 900 ; retry (15 minutes) 2419200 ; expire (4 weeks) 86400 ; minimum (1 day) ) IN NS ns _acme-challenge.www IN NS ns ns IN A 192.0.2.1 }}} == /usr/local/etc/namedb/dynamic/_acme-challenge.www.example.jp.db(_acme-challenge.www.example.jp ゾーンファイル) == {{{ $TTL 300 @ IN SOA ns.example.jp. domain.example.jp. ( 2017032201 ; serial 7200 ; refresh (2 hours) 900 ; retry (15 minutes) 2419200 ; expire (4 weeks) 86400 ; minimum (1 day) ) IN NS ns.example.jp. }}} == /usr/local/etc/dehydrated/hook.sh == {{{ : DNSSERVER="ns.example.jp" : }}} 場合によっては`DNSSERVER`設定を変更する(今回の前提では必要ない)。 == 相違点 == * 以下の3ファイルに対する変更以外に相違点は無い。 * 委任先のDNSサーバーが元と違う場合は`nsupdate`コマンドで指定するサーバーを変更する必要がある。 === /usr/local/etc/namedb/named.conf === * `example.jp`ゾーンでのDNSダイナミックアップデートの許可をしない。 * よって設置先ディレクトリも変わる。 * `_acme-challenge.www.example.jp`ゾーンを定義し、そちらでDNSダイナミックアップデートの設定を行う。 === /usr/local/etc/namedb/master/example.jp.db === * `_acme-challenge.www`レコードにて委任の設定を追加する。 * もちろんDNSサーバーが複数ある場合は複数記入すること。 === /usr/local/etc/namedb/dynamic/_acme-challenge.www.example.jp.db === * 新規作成する。 * 中身はほぼ空になるが、SOAとNSレコードの設定は必須である。 * 設置先ディレクトリには注意すること(/usr/local/etc/namedb/dynamic ディレクトリ以下に設置)。 = 参考文献 = * [[https://ftp.isc.org/isc/bind9/cur/9.11/doc/arm/Bv9ARM.html|BIND 9 Administrator Reference Manual]] * [[https://ftp.isc.org/isc/bind9/cur/9.11/doc/arm/Bv9ARM.ch04.html#dynamic_update|Chapter 4. Advanced DNS Features - Dynamic Update]] * [[https://ftp.isc.org/isc/bind9/cur/9.11/doc/arm/Bv9ARM.ch04.html#tsig|Chapter 4. Advanced DNS Features - TSIG]] * [[https://ftp.isc.org/isc/bind9/cur/9.11/doc/arm/Bv9ARM.ch06.html#dynamic_update_policies|Chapter 6. BIND 9 Configuration Reference - Dynamic Update Policies]] * [[https://ftp.isc.org/isc/bind9/cur/9.11/doc/arm/Bv9ARM.ch07.html|Chapter 7. BIND 9 Security Considerations]] == 参考文献について一言 == * BINDに関する情報を検索しても、オリジナルドキュメントが上位に来ないのは問題だと思う。 * 検索して出てきたサイトの情報を精査してみると、微妙な問題が散見しており、オリジナル文書読むと間違いであることに気がつく。 * 特にダイナミックアップデートについては`tsig-keygen`コマンドがあるのに`dnssec-keygen`コマンドで説明しているサイトが多いなど(歴史的理由については考慮するとしても)、涙無しには調査できない。 <>