IPv6 only環境でcertbotをpipインストールする (Let's EncryptでSSL証明書を自動更新する)

Let's EncryptでSSL証明書の更新を自動で行いたいため、Rocky Linux 8のIPv6 only環境でcertbotをpipインストールしたときのメモを残します。

TL;DR

公式のsnapからインストールする手順だとIPv6サーバがなく途中で躓いてしまったため、snap経由ではなくpipコマンドでcertbotをインストールしました。 一日一回の自動実行はcrontabではなくsystemctlでcertbot.timerを設定しました。 SELinuxのポリシー設定のあるサーバでnginxの設定に躓いたので回避策を書きました。

  1. Rocky Linux 8 (CentOS8互換ディストリビューション)
  2. snapのサーバがIPv6に対応していない
  3. 仕方なくpipでcertbotをインストールする
  4. certbotを試しに実行してみる
  5. systemctlでcertbot.timerを設定する
  6. SELinuxを無効化せずにnginxを動かす
  7. nginxのVirtualHost設定ファイル例

1. Rocky Linux 8 (CentOS8互換ディストリビューション)

今回はひとまず Rocky Linux 8 (CentOS8互換ディストリビューション) でcertbotを動かしてみます。何も考えずにsnapを使いたい場合はUbuntuを使うのが一番良いです。ただ、簡単インストールできてしまうことの裏返しとして、プログラムの動作構成をあまりよく知らずに使えてしまうので、あえて茨の道を渡ってみるのも良いと思います。ちなみにApline Linuxディストリビューションのシステム構成の問題でそもそもsnapはインストールできません(対応できておらずサポートされていません)。今回試すsnapを利用しないpipからのインストール手順であれば、Apline Linuxでもcertbotを動かすことができると思います。

2. snapのサーバがIPv6対応していない

Rocky Linux 8の環境でcertbotをインストールするために、まず公式で推奨されているsnapをインストールしようとするのですが、、、

sudo dnf install -y epel-release
sudo dnf install -y snapd
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
sudo snap install core

上記コマンドをIPv6 only環境で実行すると、最後のsnapコマンド実行時に下記のようなエラーが発生してしまいます。

$ sudo snap install core
error: cannot install "core": persistent network error: Post
       "https://api.snapcraft.io/v2/snaps/refresh": dial tcp 185.125.188.58:443: connect: network
       is unreachable

これはsnapのパッケージ管理サーバ api.snapcraft.io がAAAAレコードを持たず、サーバとIPv6通信ができないことが原因です。

この件は、2017年からBug #1710022 “Snap store APIs (api.snapcraft.io) are not reachab...” : Bugs : Snap Store Serverというタイトルでバグ報告がされていますが、2022年になってもまだ何も進展がなく、Snap Store ServerにIPv6対応のAAAAレコードが追加されるような気配がありません…。

3. 仕方なくpipでcertbotをインストールする

snapはあきらめて、certbotの最新版をPython3.9のpip経由でインストールすることにします。前準備としてpip3コマンドをインストールします。

sudo dnf -y install python39-pip

pip自体のバージョンを pip3 install --upgrade pip コマンドを実行して最新版にバージョンアップします。

$ pip3 --version
pip 20.2.4 from /usr/lib/python3.9/site-packages/pip (python 3.9)
$ sudo pip3 install --upgrade pip
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Collecting pip
  Downloading pip-22.0.4-py3-none-any.whl (2.1 MB)
Installing collected packages: pip
  WARNING: The scripts pip, pip3 and pip3.9 are installed in '/usr/local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-22.0.4
$ pip3 --version
pip 22.0.4 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)

certbotの動作に必要な依存ライブラリを予めOSパッケージからインストールしておきます。

sudo dnf -y install python39-devel python39-cffi python39-requests python39-urllib3
sudo dnf -y install augeas-libs libffi-devel openssl-libs

certbotをpip経由でインストールします。nginxとapacheプラグインを同時にインストールしています。

sudo pip3 install certbot certbot-nginx certbot-apache

/usr/local/binにcertbotがインストールされるため、/usr/binにシンボリックリンクを貼っておきます。

sudo ln -s /usr/local/bin/certbot /usr/bin/certbot

4. certbotを試しに実行してみる

インストールされたcertbotのバージョンを確認します。

$ certbot --version
certbot 1.27.0

--dry-runオプションを指定して、試しにcertbot renewを実行してみます。

$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
No simulated renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

何も前回の設定がなければ、certbotコマンドが無事動いているようなので、ひとまずこれでokです。おめでとうございます。

5. systemctlでcertbot.timerを設定する

certbot renewalを実行するサービスの定義certbot.serviceを作成します。ExecStartの行で実行するcertbot renewのコマンドを指定するのですが、--post-hookの引数をつけることで、Webサーバの設定を再起動してSSL証明書を再読み込みするようにしています。

cat<<EOF>/etc/systemd/system/certbot.service
[Unit]
Description=Let's Encrypt certbot renewal

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --agree-tos --post-hook "systemctl reload nginx.service"
EOF

ここではcrontabではなく、certbot.timerを定義して一日一回dailyで定時実行されるようにします。

cat<<EOF>/etc/systemd/system/certbot.timer
[Unit]
Description=Let's Encrypt certbot renewal (daily)

[Timer]
OnCalendar=0/12:00:00
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target
EOF

このファイルの中でRandomizedDelaySec=1hを指定することで実行時間を1時間の誤差をわざと設けて、全世界で同じ時刻にLet's Encryptのサーバにアクセスして過負荷になることを防いでおきます。このテクニックはArch Linuxcertbot設定ファイルを参考にしました。

systemctlコマンドでcertbot.timerを起動して、再起動時でも有効になるように設定します。

sudo systemctl start certbot.timer
sudo systemctl enable certbot.timer

ファイルの内容を書き換えた場合は、systemctl daemon-reloadコマンドを実行して設定ファイルを再読み込みします。 そして、systemctl list-timersコマンドを実行して、次回起動される.serviceコマンドの一覧を出力します。

sudo systemctl daemon-reload
sudo systemctl list-timers

実行結果の中にcertbot.timer certbot.serviceの行があればokです。

$ sudo systemctl list-timers
NEXT                         LEFT          LAST                         PASSED    UNIT                         ACTIVATES
Sat 2022-05-07 08:09:58 UTC  32min left    Sat 2022-05-07 07:06:44 UTC  30min ago dnf-makecache.timer          dnf-makecache.service
Sat 2022-05-07 11:13:44 UTC  3h 36min left Fri 2022-05-06 11:13:44 UTC  20h ago   systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sat 2022-05-07 12:19:07 UTC  4h 41min left Sat 2022-05-07 07:37:05 UTC  11s ago   certbot.timer                certbot.service
Sun 2022-05-08 00:00:00 UTC  16h left      Sat 2022-05-07 00:00:44 UTC  7h ago    unbound-anchor.timer         unbound-anchor.service

4 timers listed.
Pass --all to see loaded but inactive timers, too.

もしも、certbotが勝手に自動起動されると困る場合は、以下のコマンドを実行して、certbot.timerを停止して無効にします。

sudo systemctl stop certbot.timer
sudo systemctl disable certbot.timer

確認のためにsystemctl list-timers --allを実行して、certbot.timerの行が n/a (not available) になっていることを確認します。

$ sudo systemctl list-timers --all
NEXT                         LEFT          LAST                         PASSED      UNIT                         ACTIVATES
Sat 2022-05-07 08:09:58 UTC  29min left    Sat 2022-05-07 07:06:44 UTC  33min ago   dnf-makecache.timer          dnf-makecache.service
Sat 2022-05-07 11:13:44 UTC  3h 33min left Fri 2022-05-06 11:13:44 UTC  20h ago     systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sun 2022-05-08 00:00:00 UTC  16h left      Sat 2022-05-07 00:00:44 UTC  7h ago      unbound-anchor.timer         unbound-anchor.service
n/a                          n/a           Sat 2022-05-07 07:37:05 UTC  3min 8s ago certbot.timer                certbot.service

4 timers listed.

6. SELinuxを無効化せずにnginxを動かす

まだnginxをインストールしていない場合はOSパッケージからインストールします。

sudo dnf -y install nginx

nginxのサービスを起動して、再起動時にも自動起動を有効にします。

$ sudo systemctl start nginx
$ sudo systemctl enable nginx
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.

certbot certonly --nginxで証明書を取得します。(certonlyオプションを省略するとnginxの設定ファイルを書き換えてくれます)

$ sudo certbot certonly --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): 自分のメールアドレスを入力する

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: 規約を読んで同意するならyを入力する

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: 特にメールで案内が欲しくなければnを入力する
Account registered.
Please enter the domain name(s) you would like on your certificate (comma and/or
space separated) (Enter 'c' to cancel): 自分のサーバのFQDNを入力する(例:ssl.example.com)
Requesting a certificate for ssl.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/ssl.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/ssl.example.com/privkey.pem
This certificate expires on 2022-08-07.
These files will be updated when the certificate renews.

NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

もしも以下のようなエラーが出る場合は途中のacme-challengeに失敗してしまっています。nginxがうまく動いていないようです。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/ssl.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for ssl.example.com

Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
  Domain: ssl.example.com
  Type:   unauthorized
  Detail: XXXX:XXXX:100:XXX:XX:cafe:4e:1: Invalid response from https://ssl.example.com/.well-known/acme-challenge/hlbtXXXXeDKRQI9f0lNFBSLY6v4PyRfwoFY8GwOHAKo: 404

Hint: The Certificate Authority failed to download the challenge files from the temporary standalone webserver started by Certbot on port 80. Ensure that the listed domains point to this machine and that it can accept inbound connections from the internet.

Failed to renew certificate ssl.example.com with error: Some challenges have failed.

ファイルの権限設定が正しいのにnginxでエラーが発生してしまう場合(webのrootディレクトリも読み取れない場合)は、getenforceコマンドを実行してSELinuxの設定の有無を確認してみましょう。

$ getenforce
Disabled

もしもDisabledだったら、SELinuxは無効になっているので、ファイルの権限設定を見直して、Webのルートディレクトリがnginxの動作権限で読み込みができるかどうか、上位のディレクトリも含めて再確認してください。

$ getenforce
Enforcing

もしもEnforcingだったら、SELinuxが有効になっていて監査にひっかかっているので、適当な作業ディレクトリでSELinuxのポリシーでどこにひっかかっているかを確認します。

$ sudo grep nginx /var/log/audit/audit.log | audit2allow -m nginx
module nginx 1.0;

require {
        type var_t;
        type httpd_t;
        class file { getattr read };
}

#============= httpd_t ==============
allow httpd_t var_t:file { getattr read };

httpd_tコンテキスト(Apacheやnginxの動作権限)に対して、var_tファイルの読み取り権限がないようなので、許可するポリシーファイルを作成します。

$ sudo grep nginx /var/log/audit/audit.log | audit2allow -M nginx
******************** IMPORTANT ***********************
To make this policy package active, execute:

sudo semodule -i nginx.pp

少し時間がかかりますが、作成したポリシーファイルnginx.ppを適用します。

$ sudo semodule -i nginx.pp

もう一つ別の方法として、ポリシーを変更せずに、/var/www/html以下のファイルに対してrestoreconコマンドを実行してhttpd_sys_content_tのラベルを追加してあげるという方法もあります。ファイルのラベルを確認するにはls -Zオプションで、ファイルのSELinuxコンテキストを変更するには、chcon、semanage fcontext、restoreconコマンドが使えます。

$ cd /var/www/html
$ ls -Z
unconfined_u:object_r:var_t:s0 index.html
$ sudo restorecon *.html
$ ls -Z
unconfined_u:object_r:httpd_sys_content_t:s0 index.html

これでもうまくいかない場合は、セキュリティは弱くなりますが、httpd_tコンテキストに対してpermissiveにして全許可します。

$ sudo semanage permissive -a httpd_t

それでもうまくいかない場合は、SELinuxをオフにする、というのが長らくの定説ですが、ちょっと悲しいですね。

$ setenforce 0
$ getenforce
Permissive

一時的にSELinuxを無効化して、SELinuxが原因かどうかを確認して、サーバ内外のファイアウォールTCP/80,TCP/443が外から通信できないとか、どうやらSELinuxは冤罪で別のことが原因でありそうであれば元通りに設定を戻しておいてあげます。

$ setenforce 1
$ getenforce
Enforcing

以下のURLにSELinux上でnginxを動かす方法が載っていますので、SELinuxのポリシー設定は一度慣れておくと良いでしょう。

7. nginxのVirtualHost設定ファイル例

ちなみに、私が使っているnginxのVirtualHost設定ファイル例を以下に掲載します。IPv6 onlyの設定になっています。 シェル変数DOMAINに設定したいVirtualHostのFQDNを指定してVirtualHost毎に.confファイルを作成しています。

DOMAIN="ssl.example.com"
cat<<EOF| sudo tee /etc/nginx/conf.d/$DOMAIN.conf
map \$http_upgrade \$connection_upgrade {
    default upgrade;
    '' close;
}
server {
    ## listen 80; # IPv4
    listen [::]:80; # IPv6
    server_name $DOMAIN;
    return 301 https://\$host\$request_uri;
}
server {
    ## listen 443 ssl http2; # IPv4
    listen [::]:443 ssl http2; # IPv6
    server_name $DOMAIN;
    access_log /var/log/nginx/$DOMAIN.access.log;
    location / {
      root /var/www/$DOMAIN;
      index index.html;
    }
    ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; # managed by Certbot

    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
EOF

複数の.confファイルを作成することによって、SSL証明書FQDN個別に生成して1台のnginxサーバで複数のVirtualHostを運用できます。

nginxの設定ファイルの文法チェックを-tコマンドで実施した後、systemctlで設定ファイルを再読み込みします。

sudo nginx -t
sudo systemctl reload nginx

エラーが出なければokです。それでは、良いLet's Encryptライフをお過ごしください!

参考文献