サービス選定〜OSのインストール | |
サーバー類のインストール | |
nginx | |
さくら de Ubuntu | |
サーバーの引越 | |
バックアップ | |
>>> | Let's Encrypt |
Let's Encrypt自体はだいぶ前から知っていたのですが、2018年の3月にワイルドカード証明書を使えるようになったのでいよいよ導入することにしました。
詳しくは他のサイトに任せますが、ワイルドカード証明書を取得したいので、認証はDNS-01チャレンジになります。
古いcertbot
だとDNS-01チャレンジが使えなかったりしますが、Ubuntu 18.04でインストールされるものは大丈夫です。
つなぎに行くホストは明示的に指定する必要があるらしい。
nginxへの設定は手動でやることにします。
certbot certonly --server https://acme-v02.api.letsencrypt.org/directory --manual
指示に従って適当に情報を入力していくと、DNSのTXTレコードに登録すべき内容を指示されるので、ここでDNSのゾーンファイルを更新します。
... the last part of your DNS zone file ... _acme-challenge IN TXT "fugafugahogehoge..."
できたらsystemctl bind9 reload
して、問題なければcertbot
の方を先に進めます。
無事に証明書ができたらnginxの設定をして終わり。
DNS-01でも、DNSが1台ならば自動更新はそんなに面倒ではありません。
問題はスレーブがある場合で、どのDNSを見にくるか分かりませんので、_acme-challenge
のTXTレコードを登録したら、スレーブのゾーンデータも更新しなければなりません。
すべてのDNSを自分で管理している場合は面倒なだけですが、相互運用などで他人が管理しているDNSがあってこちらからゾーン転送のトリガーをかけられない場合、ゾーン転送のリフレッシュ時間だけ待つ必要があり現実的ではありません。
よく考えてみるとDNSが1台ならば問題がないので、_acme-challenge
サブドメインを1台のDNSに委任してしまえばいいことに気づきまして。
certbot
を実行するマシンでDNSを動かせばDNSの更新も含め、このマシンの中で完結します。
certbot
を実行するマシンでマスターのDNSを動かしていても、BIND9の設定ファイルでゾーンを分けてしまえば大丈夫です。
BIND9の設定ファイルはこんな感じ。
... view "external_view" { match-clients { any; }; zone "example.jp" { type master; file "db/example.jp.zone"; }; zone "_acme-challenge.example.jp" { type master; file "db/acme.example.jp.zone"; }; };
db/example.jp.zone
は普通のゾーンファイルですが、中に_acme-challenge.example.jp
への委任情報を書きます。
委任先のDNS名は_acme-challenge
サブドメインの中にある必要はないので、example.jp
ゾーンの名前にしておきます。
そうするとグルーレコードというか、委任先DNSのIPアドレスも同じゾーンファイル中に書いておくことができます。
... snip ... acme-dns01 IN A 192.0.2.53 _acme-challenge IN NS acme-dns01.example.jp.
このゾーンファイルはexample.jp
ゾーンのすべてのDNSで同じになります。
誰に聞いても_acme-challenge
ゾーンはacme-dns01
サーバーを見ろ、と返してくるので、認証のためのDNSアクセスは必ずacme-dns01.example.jp
に対して行われます。
委任を受けたほうはSOAレコード、NSレコードとTXTレコードだけ書いておけばOK。 TTLやネガティブキャッシュ時間は短めにしておきます。
$TTL 600 @ SOA acme-dns01.example.jp. admin.mail.example.jp 20190524 3600 3600 3600 60 IN NS acme-dns01.example.jp. IN TXT "fugafugahogehoge..."
これでsystemctl bind9 restart
して、certbot
の実行を続ければOKです。
ここまでできればダイナミックDNSでなくとも比較的簡単に自動化できます。
--manual-auth-hook
というオプションにスクリプトの名前を指定してやると、認証を行う前にこのスクリプトが実行されます。
このとき、CERTBOT_DOMAIN
に認証するドメインが、CERTBOT_VALIDATION
にTXTレコードに書くべき文字列が入ってくるので、echo
でもhere documentでもいいから、さっきの4行を出力してゾーンデータをリロードしてしまえばOK。
ワイルドカード証明書を取得する場合、たとえば*.example.jp
というドメインに対するワイルドカード証明書を取得する場合、CERTBOT_DOMAIN
にはexample.jp
が設定されてくるので、以下のように書けばOK。
#!/bin/bash cat > /var/named/db/acme.${CERTBOT_DOMAIN}.zone <<EOF $TTL 600 @ SOA acme-dns01.{CERTBOT_DOMAIN}. admin.mail.example.jp `date +%Y%m%d` 3600 3600 3600 60 IN NS acme-dns01.example.jp. IN TXT "${CERTBOT_VALIDATION}" EOF systemctl bind9 reload
シリアル番号はdate
コマンドで生成してみました。
認証が終わると --manual-cleanup-hook
オプションで指定したスクリプトが呼ばれます。
こちらはTXTレコードを抜いておきます。
シリアルを1増やすため、date -d tomorrow +%Y%m%d
としておくといいかも。
certbot
のコマンドラインはこんな感じ。
certbot certonly --manual-public-ip-logging-ok --server https://acme-v02.api.letsencrypt.org/directory --manual --manual-auth-hook /path/to/auth-hook.sh --manual-cleanup-hook /path/to/cleanup-hook.sh -d "*.example.jp"
スクリプトをフルパスにしない場合はPATH
環境変数を使って検索するようなので、カレントディレクトリから実行したい場合は./
で始める必要があるみたい。
あと、--manual-public-ip-logging-ok
というのは、「IPアドレス記録するけどいい?」という問いを出さないようにするためのオプション。
これでコマンドひっぱたくだけですべてが終わります。
一度すべてのフックを指定して実行すれば、次回からは手動認証でもcertbot renew
ですべてやってくれます。
指定したオプションは/etc/letsencrypt/renewal
以下のディレクトリに記録されています。
フックを相対パスで指定した場合は実行するディレクトリに気をつけないといけません。
キーを更新したホスト以外の場所でHTTPサーバーが走っている場合、鍵をHTTPサーバーが走っているホストに転送する必要があります、ちまちまと。
tar
で固めて持っていくのが簡単ですが、
バックアップ
で説明しているrsync
を使ってHTTPサーバーが走っているホストに押し込んでみました。
キーを更新したホストがrsyncクライアントかつ送り側で、root
で実行します(root
でしか読めないファイルがあります)。
HTTPサーバーホストがrsyncサーバーかつ受け側で、パスフレーズなしのrsync専用鍵を使い、バックアップユーザーでログインしてコピーします。
fake superを指定してコピーすることになるので、受け側(HTTPサーバーホスト側)ではすべてのファイルがバックアップユーザーの持ち物になっています。このため、rsync
をローカルで実行して、持ち主を元に戻しながら所定の場所にコピーする必要があります。
このオプションが分かりづらい。
マニュアルの--fake-super
のところに書いてありますが、結論だけ書くとroot
になって
rsync -aH --fake-super -M--super --munge-links /path/from /path/to
でOKらしい。 あとはHTTPサーバーをリロードすればOK。
SSLの基本的な設定(ssl_certificate
など)以外にやっておくといい設定。
server { listen 80 default_server; ... return 301 https://$host$request_uri; }
基本的には相対パスでリンクを書いてますが、アイコンの指定だけは古いAndroid端末で正しく動かないことがある、という情報があったのでスキーム込みの絶対パスになってたりするので。 HTMLの方を書き換えてもいいんですが、そうすると更新日時変わっちゃうし、数が多くて面倒くさいし。
server { ... sub_filter 'http://www.example.jp/' 'https://www.example.jp/'; sub_filter_last_modified on; sub_filter_once off; }
sub_filter_last_modified
は最終更新日付をオリジナルのまま保持するかどうかの指定。
ぐーぐる先生がうるさいので。
server { ... add_header Link '<https://$host$uri>; rel="canonical"'; }
add_header
はserver
でもlocation
でも指定できます。
location
で指定しなければserver
で指定したものが使用されますが、location
で指定するとserver
で指定したものはすべて無視されるので注意(ちゃんとマニュアルに書いてあります)。
たとえば、www.example.jp
はDNSで複数のサーバーに割り振っていて、個々のホストを明示的に指定したい場合はhostname.dmz.example.jp
と指定するような場合。
*.dmz.example.jp
に対しても証明書を取って、これも指定しないといけない。
$ssl_server_name
変数を使う方法もあるけど、パフォーマンスに影響があると書いてあるので、安直にlocation
以下を別のファイルに分けてinclude
してしまいました。
もっといい書き方もありそうだけど。
server { listen 443 ssl; server_name _; ssl_certificate /etc/letsencrypt/live/example.jp/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.jp/privkey.pem; include /etc/nginx/server-body.conf; } server { listen 443 ssl; server_name *.dmz.example.jp; ssl_certificate /etc/letsencrypt/live/dmz.example.jp/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dmz.example.jp/privkey.pem; include /etc/nginx/server-body.conf; }
Copyright (C) 2019-2020 akamoz.jp
$Id: letsencrypt.htm,v 1.4 2020/02/06 16:06:27 you Exp $