FreeBSD 12 で自宅サーバ作成　Let's Encrypt編

## はじめに
FreeBSD 12を使用した自宅サーバの構築を行った。FreeBSD 12はリリースされて間もない状態ですが、カーネルに標準でVIMAGEが取り込まれDNSの外向きと、内向きを別々のシステム（jail+VIMAGE）が同一ホストで構築できることから採用することにしました。その時の構築手順を備忘録の意味も含め、具体的にいくつかに渡り説明する。

* [基本システム編その１](https://qiita.com/Chun3/items/2f891e5993c91b58ae5c)
	- 背景
	- 基本のインストール
	- 日本語環境の設定
* [基本システム編その２](https://qiita.com/Chun3/items/1e8fe5071c4a78d37e66)
	- セキュリティ
	- ftpサーバ
* [基本システム編その３](https://qiita.com/Chun3/items/e5cc6a69542153649dc0)
	- jail
	- 基本システム編おわり
* [jail内基本システム編](https://qiita.com/Chun3/items/4b11b9acdd080ecdab6c)
	- 基本の設定（jail1・jail2共通）
* [DNS編](https://qiita.com/Chun3/items/95ad3113202d111ee53c)
 	- DNSの構築におけるこれまでの問題
 	- bind9インストール
 	- NTPサーバ
* [Let's Encrypt編](https://qiita.com/Chun3/items/4814c25c09756690133f)（この記事）
 	- Let's Encryptについて
 	- certbotのインストール
 	- 各種設定
 	- 証明書の取得
* [Webサーバ編](https://qiita.com/Chun3/items/286e567952cd9faf279f)
 	- インストール（apache2.4・webalizer・php7.3・postgresql11.2・mysql8.0）
 	- 設定
* [メールサーバ編](https://qiita.com/Chun3/items/dc0e176c9de4f6b4bfb8)
 	- インストール（cyrus-sasl・postfix・dovecot・procmail・pflogsumm・policyd-spf）
 	- 設定・起動
 	- spfレコード送信設定
* [ファイルサーバ編](https://qiita.com/Chun3/items/356b7fadd7aa5280ec65)
 	- sambaの選択
 	- samba3のインストール
* [メールサーバ編その2](https://qiita.com/Chun3/items/4c15ee889b052df67bbd)
 	- 概要
 	- OpenDKIM
 	- OpenDMARC
 	- ClamAV (clamav-milter)
 	- Spamassasin (spamass-milter)

## Let's Encryptについて
これまで、証明書の取得は個人で実施するには金銭的・手続き的にハードルが高くできないものと決めつけていました。しかし、無償で手続きもネットで簡単に行えそうであることを最近知り、情報を集め実行しました。手続き的にはdnsやhttpなどいくつかの方法がありましたが、以下のサイトを参照しdnsを使って証明書を取得することができました。

- [CertbotとBINDの組み合わせでLet's Encryptのワイルドカード証明書を取得・更新する](https://qiita.com/yasuhirokimura/items/3a95e169f806b3772e06)
- [Let's Encrypt 総合ポータル (非公式解説サイト)](https://free-ssl.jp/)

dnsをviewにより外向きと内向きを同一システムで運用していたことで問題があり今回の自宅サーバ再構築のきっかけとなりました。
なお、上記サイトは分かりやすく丁寧に説明されていますので詳細はそちらを参照していただき、以下は具体的にやった手順をまとめる。
また、外部dnsと連携して認証するためjail1（外部用）にインストールし作業を行う。
加えて、この作業の性質上１からやり直し確認しづらく、これまでにやったことをまとめたものである。jailの中からはまだ実施していないため問題が出る可能性があり、最終的にシステム移行時に確認予定。

**結果報告（2019/9/23）**
最初に自動更新が確認されたときに追記する予定が、随分とサボってしまった。現在自動更新を無事に4回実施できています。証明書ファイルの添字も”５”になりファイルの日付を見ると約２ヶ月毎に更新されているのが確認できた。また、運用始めてから今までに問題も発生していないことを追加報告します。

**問題発生（2019/10/19）**
pkgをアップデート後自動更新が行われていないことに気づき、土曜日にcronで更新プログラムが起動するので、その結果を確認した。
すると以下のメッセージが

```
/usr/local/sbin/renew-letsencript: certbot: not found
```

ソフトがなくなっている？
確認すると

```
/usr/local/bin# ls -l cert*
-rwxr-xr-x  1 root  wheel     393 Sep 28 10:23 certbot-2.7*
-rwxr-xr-x  1 root  wheel  326216 Oct  3 10:19 certtool*
```

プログラム名にpythonのバージョンが入ったようだ（python3用も出来たのか？）
とりあえずリンクを張って更新できるようになった。

```
/usr/local/bin# ln -s certbot-2.7 certbot
```

また、有効期限まで１９日であったことから先週には、最初の自動更新失敗のメールが来ているはずであるが、気づかずに廃棄したようだ（よく確認しないと）。しかし、本日Let's Encrypt Expiry Bot <expiry@letsencrypt.org>からLet's Encrypt certificate expiration notice for domain "*.example.jp"のメールを受け取りありがたく思っている。


```
Hello,

Your certificate (or certificates) for the names listed below will expire in 19 days (on 07 Nov 19 15:11 +0000).
Please make sure to renew your certificate before then, or visitors to your website will encounter errors.
（以下省略）
```

## certbotのインストール
certbotのオプションを選択することにより認証方式を選ぶことができる。今回はdnsを使用するためcertbot-dns-rfc2136をインストールする。プログラム自身はpythonで書かれているため以下のようにたくさんのモジュールがインストールされる。

```
jail1 /root # pkg install security/py-certbot-dns-rfc2136
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
All repositories are up to date.
The following 63 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        py27-certbot-dns-rfc2136: 0.31.0
        py36-certbot-dns-rfc2136: 0.31.0
        py27-certbot: 0.31.0,1
        py27-openssl: 18.0.0
        py27-cryptography: 2.3
        py27-ipaddress: 1.0.22
        py27-idna: 2.7
        py27-six: 1.12.0
        py27-enum34: 1.1.6
        py27-cffi: 1.11.5
        py27-pycparser: 2.18
        py27-asn1crypto: 0.22.0
        py27-josepy: 1.1.0
        py27-acme: 0.31.0,1
        py27-requests-toolbelt: 0.8.0
        py27-requests: 2.21.0
        py27-chardet: 3.0.4
        py27-certifi: 2018.11.29
        py27-urllib3: 1.22,1
        py27-pysocks: 1.6.8
        py27-pytz: 2018.9,1
        py27-pyrfc3339: 1.1
        py27-zope.interface: 4.6.0
        py27-zope.component: 4.2.2
        py27-zope.event: 4.1.0
        py27-parsedatetime: 2.4_1
        py27-configobj: 5.0.6_1
        py27-configargparse: 0.14.0
        py27-dnspython: 1.15.0
        py27-mock: 2.0.0
        py27-pbr: 3.1.1
        py27-pip: 9.0.3
        py27-funcsigs: 1.0.2
        py36-certbot: 0.31.0,1
        py36-openssl: 18.0.0
        py36-cryptography: 2.3
        python36: 3.6.8
        py36-idna: 2.7
        py36-setuptools: 40.8.0
        py36-six: 1.12.0
        py36-cffi: 1.11.5
        py36-pycparser: 2.18
        py36-asn1crypto: 0.22.0
        py36-josepy: 1.1.0
        py36-acme: 0.31.0,1
        py36-requests-toolbelt: 0.8.0
        py36-requests: 2.21.0
        py36-chardet: 3.0.4
        py36-certifi: 2018.11.29
        py36-urllib3: 1.22,1
        py36-pysocks: 1.6.8
        py36-pytz: 2018.9,1
        py36-pyrfc3339: 1.1
        py36-zope.interface: 4.6.0
        py36-zope.component: 4.2.2
        py36-zope.event: 4.1.0
        py36-parsedatetime: 2.4_1
        py36-configobj: 5.0.6_1
        py36-configargparse: 0.14.0
        py36-dnspython: 1.15.0
        py36-mock: 2.0.0
        py36-pbr: 3.1.1
        py36-pip: 9.0.3

Number of packages to be installed: 63

The process will require 207 MiB more space.
42 MiB to be downloaded.
（以下省略）
```

## 各種設定
### dns更新用パスワード作成
証明書を取得する際、要求元が確かに要求されたドメインの管理者であることを証明するためにISRG（証明書発行元）から送られてきた認証コードをdnsのTXTレコードとしてISRGに応答することで証明書が発行される（詳細は上記サイトを参照）。
このことから、ダイナミックにTXTレコードを書き換える必要があり、certbotからdnsをアクセスするためのパスワードを作成する。

```
jail1 /root # tsig-keygen certbot-key > /usr/local/etc/namedb/certbot-key.key
```

```/usr/local/etc/namedb/certbot-key.key
key "certbot-key" {
        algorithm hmac-sha256;
        secret "/9a5f/hobiMY9BuycJOehKDNB1QzDMPliFbnSBecZZM=";
};
```

### bindの設定
前節の設定に追加し以下のようにする。

```/usr/local/etc/namedb/named.conf
include "/usr/local/etc/namedb/certbot-key.key"; // 認証鍵の読み込み
include "/usr/local/etc/namedb/rndc_key";
controls {
        inet 127.0.0.1 port 953 allow {127.0.0.1;} keys {"rndc-key";};
};

options {
        directory       "/usr/local/etc/namedb/working";
        pid-file        "/var/run/named/pid";
        dump-file       "/var/dump/named_dump.db";
        statistics-file "/var/stats/named.stats";
        listen-on-v6    { none; };
        allow-transfer  { 192.168.1.0/24; };
};

// wan側の設定
// 自ドメイン正引き
zone "example.jp" {
        type master;
        file "/usr/local/etc/namedb/master/example.jp.zone";
        check-names ignore;
};

zone "_acme-challenge.example.jp" {
        type master;
        file "/usr/local/etc/namedb/dynamic/_acme-challenge.example.jp.zone";
        check-names ignore;
        update-policy {
                grant certbot-key. name _acme-challenge.example.jp. TXT;
        };
};
```

example.jpのゾーンファイルに最後の行を追加する

```/usr/local/etc/namedb/master/example.jp.zone
$TTL    3600
@       IN      SOA     ns1.example.jp.    root.example.jp.  (
                        20190020101      ; Serial
                        3600            ; Refresh
                        900             ; Retry
                        3600000         ; Expire
                        3600            ; Minimum
                                )
                        IN      NS      ns1.example.jp.
                        IN      NS      ns2.example.jp.
                        IN      MX  10  mail.example.jp.
                        IN      A       111.222.333.100
ns1                     IN      A       111.222.333.100
ns2                     IN      A       111.222.333.200
mail                    IN      A       111.222.333.100
www                     IN      CNAME   ns1.example.jp.
www2                    IN      CNAME   ns2.example.jp.
ftp                     IN      CNAME   ns1.example.jp.
_acme-challenge         IN      NS      ns1.example.jp.
```

_acme-challengeサブドメインのゾーンファイルをdynamicディレクトリーに作成

```/usr/local/etc/namedb/dynamic/_acme-challenge.example.jp.zone
$TTL    3600
@       IN      SOA     ns1.example.jp.    root.example.jp.  (
                        20190020101      ; Serial
                        3600            ; Refresh
                        900             ; Retry
                        3600000         ; Expire
                        3600            ; Minimum
                                )
        IN      NS      ns1.example.jp.
```

パーミッションの設定

```
jail1 /usr/local/etc/namedb/dynamic # chmod 644 _acme-challenge.example.jp.zone
jail1 /usr/local/etc/namedb/dynamic # chown bind:bind _acme-challenge.example.jp.zone
```

### certbotの設定

```sh:/usr/local/etc/letsencrypt/dns-rfc2136.ini
# _acme-challenge.example.jpゾーンのmasterサーバのアドレス
dns_rfc2136_server = 111.222.333.100
# アクセスするポート番号
dns_rfc2136_port = 53
# 認証鍵の名前
dns_rfc2136_name = certbot-key.
# 認証鍵の値
dns_rfc2136_secret = /9a5f/hobiMY9BuycJOehKDNB1QzDMPliFbnSBecZZM=
# 鍵の生成に用いたアルゴリズム
dns_rfc2136_algorithm = HMAC-SHA256
```

## 証明書の取得
### アカウントの作成
以下は連絡用のメールをシェアする場合でありしない場合は--eff-emailを--no-eff-emailとすれば良い

```
jail1 /root # certbot register --email admin@example.jp --agree-tos --eff-email
Saving debug log to /var/log/letsencrypt/letsencrypt.log

IMPORTANT NOTES:
 - Your account credentials have been saved in your Certbot
   configuration directory at /usr/local/etc/letsencrypt. You should
   make a secure backup of this folder now. This configuration
   directory will also contain certificates and private keys obtained
   by Certbot so making regular backups of this folder is ideal.
```

### 証明書の取得

```
jail1 /root # certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /usr/local/etc/letsencrypt/dns-rfc2136.ini -d '*.example.jp'
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-rfc2136, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for example.jp
Waiting 60 seconds for DNS changes to propagate
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /usr/local/etc/letsencrypt/live/example.jp/fullchain.pem
   Your key file has been saved at:
   /usr/local/etc/letsencrypt/live/example.jp/privkey.pem
   Your cert will expire on 2019-04-27. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
```

取得した証明書は以下のように実態のソフトリンクになっている。更新していくとリンク先のファイル名の添字が変わっていく（例cert1.pem → cert2.pem）。

```
jail1 /root # ls -l /usr/local/etc/letsencrypt/live/example.jp/
total 4
-rw-r--r--  1 root  wheel  692 Jan 27 15:16 README
lrwxr-xr-x  1 root  wheel   31 Jan 27 15:16 cert.pem@ -> ../../archive/example.jp/cert1.pem
lrwxr-xr-x  1 root  wheel   32 Jan 27 15:16 chain.pem@ -> ../../archive/example.jp/chain1.pem
lrwxr-xr-x  1 root  wheel   36 Jan 27 15:16 fullchain.pem@ -> ../../archive/example.jp/fullchain1.pem
lrwxr-xr-x  1 root  wheel   34 Jan 27 15:16 privkey.pem@ -> ../../archive/example.jp/privkey1.pem
```

### 証明書の更新

初期認証条件などが記録されているため、更新は以下のコマンドで簡単に更新できる。

```
jail1 /root # certbot renew
```

証明書を使用しているアプリケーションに対し更新したことを通知する必要があり以下のスクリプトをクロンで呼び出すことで自動更新ができる（上記参考サイトに公開されているスクリプトをそのまま使用）。

```sh:/usr/local/sbin/renew-letsencrypt
#!/bin/sh

LANG=C
PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/sbin:/usr/sbin
letsencrypt_dir=/usr/local/etc/letsencrypt/
services="apache24 postfix dovecot"

check_update() {
    dir=$1

    [ $(find ${dir} -mtime -1 -print | wc -l) -gt 1 ]
}

if [ $(id -u) != 0 ]
then
    echo "This command requires root previlege." 1>&2
    exit 1
fi

certbot renew
if check_update ${letsencrypt_dir}
then
    for service in ${services}
    do
        service ${service} reload || exit $?
    done
fi
exit 0
```

crontabに以下を追加する。

```sh:/etc/crontab
45 3 * * 6 /usr/local/sbin/renew-letsencrypt
```

これで、毎週土曜日に更新要求される。有効期間が30日未満になると実際に更新され、使用しているアプリケーションにreload指示が発行される。