home / vps / certbot Let's Encrypt

サービス選定〜OSのインストール
サーバー類のインストール
nginx
さくら de Ubuntu
サーバーの引越
バックアップ
>>> Let's Encrypt

 Let's Encrypt自体はだいぶ前から知っていたのですが、2018年の3月にワイルドカード証明書を使えるようになったのでいよいよ導入することにしました。

home / vps / certbot 概略

 詳しくは他のサイトに任せますが、ワイルドカード証明書を取得したいので、認証は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の設定をして終わり。

home / vps / certbot DNSが2台以上ある場合

 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です。

home / vps / certbot 認証の自動化

 ここまでできればダイナミック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以下のディレクトリに記録されています。 フックを相対パスで指定した場合は実行するディレクトリに気をつけないといけません。

home / vps / certbot デプロイ

 キーを更新したホスト以外の場所で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。

home / vps / certbot nginxの設定変更

 SSLの基本的な設定(ssl_certificateなど)以外にやっておくといい設定。

httpsへのリダイレクト
server {
    listen 80 default_server;
    ...
    return 301 https://$host$request_uri;
}
すべてのリソースをhttpsで返す

 基本的には相対パスでリンクを書いてますが、アイコンの指定だけは古い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は最終更新日付をオリジナルのまま保持するかどうかの指定。

canonicalリンクの指定

ぐーぐる先生がうるさいので。

server {
    ...
    add_header Link '<https://$host$uri>; rel="canonical"';
}

add_headerserverでも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 $