Let's Encrypt を利用した SSL 証明書の作成と Nginx on Ubuntu 20 の設定

## はじめに

Let's Encrypt で SSL 証明書を作成し、Ubuntu 20 で稼働する Nginx に対して SSL 設定をしてみました。

Azure VM + Nginx へ配置した Web API とやりとりするデータを暗号化することが狙いです。
参考：[Python + Flask + MongoDB を利用した Web API の作成と Azure VM + Nginx への配置（ホロライブの動画配信予定を収集 その３）](https://qiita.com/kerobot/items/bd504b0d787de63c364e)

## 環境

* Ubuntu 20 LTS (Azure VM)
* Nginx 1.18.0
* certbot 1.10.1

## 前提

Nginx が導入され、ポート 80 でアクセス（DNS による名前解決）できること。
念のため、HTTP (ポート 80) で Web API にアクセスできることを確認しておきます。
![img01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283306/bceb3b99-4333-e93c-9306-c7a62353ac30.jpeg)

## 準備

SSL を設定してポート 443 でアクセスするため、ポート 443 への受信も許可しておきます。
今回は、クラウド環境として Azure を利用したため、NSG でポート 443 への受信許可を追加しておきました。

## Certbot を利用した Nginx への SSL 設定

[Certbot](https://certbot.eff.org/) を用いて、Let's Encrypt を利用した SSL 証明書を作成しました。

Ubuntu 20.04 と Nginx の組み合わせの場合は、下記手順で導入します。
[Ubuntu 20.04 と Nginx の公式手順](https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx)

### Certbot とは

ウェブサイトに対して、Let's Encrypt 証明書を使用して HTTPS を有効にするための無料のオープンソース・ソフトウェア・ツールです。

### Let's Encrypt とは

非営利団体である Internet Security Research Group (ISRG）により運営されている証明書認証局で、TLS の X.509 証明書を無料で発行しています。

### SSL 証明書の作成

1. 対象のサーバーに対して、sudo 権限を持つユーザーとして SSH 接続します。

2. snap パッケージをインストールするために、snapd をインストールします。

    ```bash
    $ sudo apt install snapd
    ```

3. snapd のバージョンが最新であることを確認します。

    ```bash
    $ sudo snap install core; sudo snap refresh core
    ```

4. Certbot の既存の OS パッケージをすべて削除します。

    ```bash
    $ sudo apt remove certbot
    ```

5. snapd を利用して、Certbot をインストールします。

    ```bash
    $ sudo snap install --classic certbot
    ```

6. /snap/bin/certbot に対してシンボリックリンクを作成します。

    ```bash
    $ sudo ln -s /snap/bin/certbot /usr/bin/certbot
    ```

7. Certbot を実行して証明書を生成します。（Nginx の自動的な構成変更は指定なし）

    ```bash
    $ cd /etc/letsencrypt/
    $ sudo certbot certonly --webroot -w <公開パス> -d <ドメイン> --renew-by-default --email <メールアドレス>
    ```

    手順通りに進めた際に、.well-known/acme-challenge にファイルが無いよ。的な下記エラーが発生したため、上記のように、証明書を作成すると同時に公開パスを明示的に指定しています。
    念のため、公開パス直下に .well-known/acme-challenge ディレクトリも作成しておきました。

    ```text
    Challenge failed for domain hogehoge.cloudapp.azure.com
    http-01 challenge for hogehoge.cloudapp.azure.com
    Cleaning up challenges
    Some challenges have failed.

    IMPORTANT NOTES:
     - The following errors were reported by the server:

       Domain: hogehoge.cloudapp.azure.com
       Type:   unauthorized
       Detail: Invalid response from
       http://hogehoge.cloudapp.azure.com/.well-known/acme-challenge/...
       [xxx.xxx.xxx.xxx]: "<html>\r\n<head><title>404 Not
       Found</title></head>\r\n<body>\r\n<center><h1>404 Not
       Found</h1></center>\r\n<hr><center>nginx/1.18.0 (Ub"

       To fix these errors, please make sure that your domain name was
       entered correctly and the DNS A/AAAA record(s) for that domain
       contain(s) the right IP address.
    ```

    また、何回か試行している際に、下記のようなエラーが発生するようになりました。

    ```bash
    Another instance of Certbot is already running.
    ```

    プロセスを探して強制的に停止し、

    ```bash
    # プロセスを探して強制的に停止
    $ ps waux | grep cert
    root      461580  0.0  0.1  11276  4984 pts/0    T    15:38   0:00 sudo certbot --nginx
    root      461581  0.0  1.1  68672 46380 pts/0    T    15:38   0:00 /snap/certbot/793/bin/python3 /snap/certbot/793/bin/certbot --nginx
    mcuser    462206  0.0  0.0   8160   672 pts/0    S+   15:52   0:00 grep --color=auto cert

    $ sudo kill -9 461580
    [1]+  Killed                  sudo certbot --nginx  (wd: ~)
    (wd now: /etc/letsencrypt)

    $ ps waux | grep cert
    mcuser    462221  0.0  0.0   8160   740 pts/0    S+   15:53   0:00 grep --color=auto cert
    ```

    .certbot.lock ファイルを探して削除することで解決しました。

    ```bash
    # .certbot.lock ファイルを探して削除
    $ sudo find / -type f -name ".certbot.lock"
    /var/lib/letsencrypt/.certbot.lock
    /var/log/letsencrypt/.certbot.lock
    /etc/nginx/.certbot.lock
    /etc/letsencrypt/.certbot.lock

    $ sudo rm /var/lib/letsencrypt/.certbot.lock
    $ sudo rm /var/log/letsencrypt/.certbot.lock
    $ sudo rm /etc/nginx/.certbot.lock
    $ sudo rm /etc/letsencrypt/.certbot.lock
    ```

8. 証明書の生成が完了すると、ログの場所や証明書やキーの情報が表示されます。

    ```text
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Plugins selected: Authenticator webroot, Installer None
    Requesting a certificate for hogehoge.cloudapp.azure.com

    IMPORTANT NOTES:
     - Congratulations! Your certificate and chain have been saved at:
       /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem
       Your key file has been saved at:
       /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/privkey.pem
       Your cert will expire on 2021-03-21. 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
    ```

9. 証明書が生成されたことを確認します。

    ```text
    $ sudo -s
    # ll /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/
    ```

    ```text
    README
    cert.pem -> ../../archive/hogehoge.cloudapp.azure.com/cert1.pem
    chain.pem -> ../../archive/hogehoge.cloudapp.azure.com/chain1.pem
    fullchain.pem -> ../../archive/hogehoge.cloudapp.azure.com/fullchain1.pem
    privkey.pem -> ../../archive/hogehoge.cloudapp.azure.com/privkey1.pem
    ```

### Nginx の SSL 設定

1. Nginx の設定ファイルを編集します。

    ```bash
    # Web API 用に作成した Nginx の設定ファイルを編集します。
    $ vi ~/holoapi/config/nginx.conf
    ```

    ```bash
    # Nginx の既定の設定ファイルであればこんな感じ
    $ sudo vi /etc/nginx/sites-available/default
    ```

2. SSL を有効化し、ssl_certificate に fullchain.pem（証明書）、ssl_certificate_key に privkey.pem（キー） を指定します。

    ```bash
    ...
    server {
        listen  80;

        # 443ポートでSSL
        listen 443 ssl;
        # SSL証明書
        ssl_certificate /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem;
        # SSLキー
        ssl_certificate_key /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/privkey.pem;
        # サーバー名（ドメイン）
        server_name hogehoge.cloudapp.azure.com;
    ...
    ```

3. Nginx の設定ファイルを検証します。

    ```bash
    $ sudo nginx -t -c ~/holoapi/config/nginx.conf
    nginx: the configuration file /home/mcuser/holoapi/config/nginx.conf syntax is ok
    nginx: configuration file /home/mcuser/holoapi/config/nginx.conf test is successful
    ```

4. Nginx のプロセスを一旦停止して起動しなおしました。

    ```bash
    # プロセスの停止
    $ ps ax | grep nginx
     468229 ?        Ss     0:00 nginx: master process nginx -c /home/mcuser/holoapi/config/nginx.conf
     468230 ?        S      0:00 nginx: worker process
    $ cat /var/run/nginx.pid
    468229
    $ sudo kill -QUIT $( cat /var/run/nginx.pid )
    # 設定ファイルを指定して起動
    $ sudo nginx -c ~/holoapi/config/nginx.conf
    # 参考：設定ファイルの再読み込み
    $ sudo nginx -s reload
    ```

    ```bash
    # daemon プロセスの再起動であればこんな感じ
    $ sudo systemctl restart nginx
    # daemon プロセスの状態確認
    $ sudo systemctl status nginx
    # 参考：設定ファイルの再読み込み
    $ sudo systemctl reload nginx
    ```

5. 利用中のポートを確認してみると、ポート 443 が LISTEN となっています。

    ```bash
    $ ss -antu
    Netid     State          Recv-Q     Send-Q          Local Address:Port               Peer Address:Port      Process
    tcp       LISTEN         0          511                   0.0.0.0:443                     0.0.0.0:*
    ```

### SSL 接続確認

HTTPS (ポート 443) でアクセスできました。このサイトとの接続は安心とのことです。
![img02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283306/78c3b639-e11e-5299-012b-be4b8c1badbd.jpeg)

## Certbot を利用した SSL 証明書の削除

参考までに、作成した SSL 証明書の削除は、下記のように delete を指定して Certbot を実行します。

```bash
$ sudo certbot delete

Saving debug log to /var/log/letsencrypt/letsencrypt.log

Which certificate(s) would you like to delete?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: hogehoge.cloudapp.azure.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificate(s) are selected for deletion:

  * hogehoge.cloudapp.azure.com

Are you sure you want to delete the above certificate(s)?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
Deleted all files relating to certificate hogehoge.cloudapp.azure.com.
```

これだけだと、Nginx の設定ファイルなどはそのままとなってしまうため、手動で設定ファイルを修正して再起動（または再読み込み）する必要があります。

## Certbot を利用した SSL 証明書の自動更新確認

これまでの手順で導入した Certbot は、定期的に証明書の有効期限の自動更新を行いますが、renew --dry-run を指定して Certbot を実行することで、更新をテストできます。

ただし、今回もこれではエラーになってしまったため、

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

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/hogehoge.cloudapp.azure.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator nginx, Installer nginx
Simulating renewal of an existing certificate for hogehoge.cloudapp.azure.com
Performing the following challenges:
http-01 challenge for hogehoge.cloudapp.azure.com
Using default addresses 80 and [::]:80 ipv6only=on for authentication.
Waiting for verification...
Challenge failed for domain hogehoge.cloudapp.azure.com
http-01 challenge for hogehoge.cloudapp.azure.com
Cleaning up challenges
Attempting to renew cert (hogehoge.cloudapp.azure.com) from /etc/letsencrypt/renewal/hogehoge.cloudapp.azure.com.conf produced an unexpected error: Some challenges have failed.. Skipping.
All renewal attempts failed. The following certs could not be renewed:
  /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem (failure)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

All renewal attempts failed. The following certs could not be renewed:
  /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem (failure)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 renew failure(s), 0 parse failure(s)

IMPORTANT NOTES:
 - The following errors were reported by the server:

   Domain: hogehoge.cloudapp.azure.com
   Type:   unauthorized
   Detail: Invalid response from
   http://hogehoge.cloudapp.azure.com/.well-known/acme-challenge/6uWFQz67cHvdXqevhQhIayObcJAgp-4uS3nQmwktOf8
   [20.46.183.35]: "<html>\r\n<head><title>404 Not
   Found</title></head>\r\n<body>\r\n<center><h1>404 Not
   Found</h1></center>\r\n<hr><center>nginx/1.18.0 (Ub"

   To fix these errors, please make sure that your domain name was
   entered correctly and the DNS A/AAAA record(s) for that domain
   contain(s) the right IP address.
```

このように、公開パスを明示的に指定して実行しました。

```bash
$ sudo certbot renew --dry-run --webroot -w <公開パス>
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/hogehoge.cloudapp.azure.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator webroot, Installer None
Simulating renewal of an existing certificate for hogehoge.cloudapp.azure.com
Performing the following challenges:
http-01 challenge for hogehoge.cloudapp.azure.com
Using the webroot path <公開パス> for all unmatched domains.
Waiting for verification...
Cleaning up challenges

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```

## Certbot を利用した SSL 証明書の更新

SSL 証明書の更新を実際に行う場合は下記コマンドを実行します。

```bash
$ sudo certbot renew
```

更新する必要がない場合はスキップされます。

```bash
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/hogehoge.cloudapp.azure.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem expires on 2021-03-22 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```

/etc/letsencrypt/renewal/hogehoge.cloudapp.azure.com.conf の設定に従って更新を行うため、ポート 80 でアクセスする公開パスを意図的に指定したい場合などは設定を変更しておきます。

```bash
# renew_before_expiry = 30 days
version = 1.10.1
archive_dir = /etc/letsencrypt/archive/hogehoge.cloudapp.azure.com
cert = /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/cert.pem
privkey = /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/privkey.pem
chain = /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/chain.pem
fullchain = /etc/letsencrypt/live/hogehoge.cloudapp.azure.com/fullchain.pem

# Options used in the renewal process
[renewalparams]
account = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
authenticator = webroot
manual_public_ip_logging_ok = None
webroot_path = <公開パス>,
server = https://...
[[webroot_map]]
```

今回は証明書の生成や自動更新確認の時点で公開パスを指定していたため、すでに設定されていました。


## おわりに

Azure VM + Nginx へ配置した Web API とやりとりするデータを暗号化することができました。
Nginx はほとんど利用したことがないですが、やりたいことを実現するための設定がシンプルでわかりやすいですね。

Let's Encrypt は、無料で SSL 証明書が利用できるため助かりました。
メリット・デメリットもいろいろあるようですが、一般的に販売されている SSL 証明書と、暗号強度などがほぼ同じということが驚きです。

SSL 証明書の有効期間が 90 日間ということで短いですが、自動更新の仕組みがあるため、通常利用に問題はなさそうです。
このまま利用して更新などを確認してこうと思います。