AWS EC2 に Docker Compose で nginx + certbot (Let's Encript) + Redmine + MySQL 入れてみた（3/5 Nginx, Certbot 導入と SSL 証明書の取得・更新）

__↓ 1/5 はじめに__

https://qiita.com/ax_kazz/items/59e15728139a6768bbdf

__↓ 2/5 Docker, Docker Compose など基盤構築__

https://qiita.com/ax_kazz/items/816fe535aa3cf402d004

[前回記事](https://qiita.com/ax_kazz/items/816fe535aa3cf402d004 "2/5 Docker, Docker Compose")の続きです。
この先使用する Compose ファイル（docker-compose.yml）については、[前記事の最後](https://qiita.com/ax_kazz/items/816fe535aa3cf402d004#docker-composeyml-compose-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E8%A8%AD%E7%BD%AE "2/5 Docker, Docker Compose など基盤構築 - docker-compose.yml （Compose ファイル）の設置")を参照してください。

そして、ここから面倒な作業が続きます。
以下の順番で設定・操作を nginx, certbot を起動・再起動を繰り返すことで、SSL 環境を作ります。

1. DHE ciphers 利用のための準備（https 対応の際に必要）
1. Nginx 起動 ⇒ Webサイトの設定ファイル設置 ⇒ 再起動（http のみ）
1. certbot 起動（SSL証明書取得）
1. Compose ファイル、Nginx 設定ファイルの差替（https 対応）
1. nginx 再起動（https 対応）
1. certbot 再起動（SSL証明書更新用）

# DHE ciphers 利用のための準備（https 対応の際に必要）

SSL で利用される暗号化方式には種類がいくつかあり、その１つに DHE ciphers があります。
SSL 対応版のconf に差し替える前に、DHE ciphers が使えるようにしておく必要があるため、必要なファイルをここで準備しておきます[^1] [^2]。

[^1]: nginx 1.11 以降で DHE ciphers を使えるようにしたい場合、自分で DH パラメータファイルを準備する必要がある。そのため、 dhparam.pem をここで生成する。ちなみにDHE ciphers が使えない場合は、別の暗号化方式が利用できれば事足りると思う。しかしながら DHE ciphers を利用した通信ができなくなるぶん、利用可能な暗号化の選択肢も狭まることに繋がる。その悪影響を考慮すると、適切に設定しておくに越したことはないと思う。

[^2]: このファイルは、暗号化通信のセキュリティ観点から、ホスト毎に設定すべき性質のファイル。コンテナ（ボリューム）内には格納しないのが適切かと思う。

```shell-session
$ mkdir -p data/ssl/private
$ openssl dhparam 2048 -out data/ssl/private/dhparam.pem
```

この dhparam.pem は SSL有効化の際に差し替える nginx の conf ファイル内、ssl_dhparam パラメータで紐付けています。
そのためこのファイルは、本来この時点（SSL証明書取得前の時点）では必要ありません。
しかしながら Compose ファイルの中でこのファイルをマウントしている関係上、先にここで準備する必要があります[^3]。

[^3]: あとで Compose ファイルを書き換えるのもあり。

https://qiita.com/r-ytakada/items/7ac9ce32c1ed4d01d505

# Nginx 起動 ⇒ Webサイトの設定ファイル設置 ⇒ 再起動（http のみ）

Nginx 設定ファイルの保存場所（ボリューム）を作成・構築するため、最初は何も考えずに Nginx を起動します。

```shell-session
$ docker-compose up -d nginx
```

次に Certbot による SSL 証明書取得からはじめます。
そのため Nginx も、http だけ応答する状態から始めるので、そのための設定を準備します。

次に Webドメイン用の Nginx 設定ファイル「vhost-www\.example.jp.conf」を作成し、サーバにアップロードします[^4]。

```nginx:vhost-www.example.jp.conf
server {
    listen 80;
    server_name www.example.jp;

    root /usr/share/nginx/html;
}
```

[^4]: Nginx コンテナ運用のベストプラクティスに沿って行う場合、(1) Nginx 設定テンプレートファイル（vhost-www\.example.jp.conf.tmplate）を作成し、(2) バインドマウントした templates に設置、（3）Nginx を起動すると設置したテンプレートファイルから自動的に設定ファイル（vhost-www\.example.jp.conf）が構築される、という流れに沿って行うべき。でも今回は、テンプレート用に一時的なバインドマウントを施す手間を省略するため、このような手順とした。

記載の通り、まずは 80ポートのみで一旦アップロード。
SSL 証明書取得後に 443 ポートに関する記述を追加した設定ファイルに差し替えます。

ちなみにドキュメントルートの場所についても注意してください[^5]。

[^5]: ドキュメントルートは /var/www/html が割と有名ですが、nginx のドキュメントルートは、公式も含めて /usr/share/nginx/html が一般的な模様。

次に、この設定ファイルをホストからコンテナにコピーします。
（「docker-compose cp」コマンドはありません。コピーは「docker cp」コマンドを使います。）

```shell-session
$ docker cp vhost-www.example.jp.conf nginx:/etc/nginx/conf.d/vhost-www.example.jp.conf
```

この状態で Nginx 最初の再起動し、設定ファイルの内容を反映します。

```shell-session
$ docker-compose stop nginx
$ docker-compose up -d nginx
```

一旦ブラウザからアクセスして、Nginx のブランクページが表示されるか確認します。

```
http://www.example.jp
```

:::note warn
最近のブラウザは、URL だけで確認しようとすると https:// 優先（SSL 有）でアクセスします。
この時点では SSL はまだ設定していないので、必ず http:// （SSL 無）で確認してください。
:::

# certbot 起動（SSL証明書取得）

次に certbot を起動し、証明書を取得します。
その際、最初は Compose ファイルに記述されている ENTRYPOINT を無視させる必要があります。
そのため、以下の通り docker-compose コマンドに --entrypoint オプションを指定して「空白」を記述します。

```shell-session
$ docker-compose run --rm --entrypoint '' certbot certbot certonly --webroot -w /usr/share/nginx/html -d www.example.jp --agree-tos -m foobar@example.jp --test-cert
```

これによって ENTRYPOINT の動作が上書き（オーバーライド）されます。
さらに替わりとして、SSL 証明書を取得するために「certbot certonly」コマンドを記述しています[^9]。

## 補足：certbot コマンドの非対話オプションについて

ここで使用した certbot コマンドは、-d, --agree-tos, -m などのオプションオプションを用いることで、オプション未使用の場合は表示される「対話形式」を極力省略しています。
しかしながら、、、

```shell-session
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing 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: 
```

…という対話形式だけは、省略する方法（オプション）が分かりませんでした。
よって、ここだけは対応形式で応答する必要があります。

:::note info
要約すると「Certbot 開発団体である EFF に、コマンド内で登録するメールアドレスを共有してもよいか」という確認メッセージです。
「Y」(Yes) とすると、コマンド内で登録したメールアドレスが EFF に共有され、EFF からニュースやキャンペーンなどお知らせ等を配信してくれる模様。
各自の判断で Y/N いずれかを選択してよいと思いますが、個人的には必要ないので、「N」(No) を入力して Enter。
:::

:::note info
本当はこの対話もコマンドオプションで省略したかったのですが、対応するオプション等がよく分からなかった。
判明した時点で、記述を訂正する予定です。
:::

## Compose ファイル、Nginx 設定ファイルの差替（https 対応）

前項の certbot コマンドに問題がなければ、SSL 証明書が取得できているはずです。
（正常かエラーかは、メッセージの内容を確認してください。）

問題なければ https（SSL）対応の記述をCompose ファイル、Nginx 設定ファイルそれぞれに反映します。

```yaml:docker-compose.yml（抜粋）:
    :<snip>
  nginx:
      :<snip>
    ports:
      - "80:80"
      - "443:443" # 443 ポートが EXPOSE されてないので、ports で開ける
          :<snip>
```
（docker-compose.yml は、443 ポート開放のコメント解除のみ。）


```nginx:vhost-www.example.conf
server {
    listen        80;
    server_name   www.example.jp;

    # certbot renew （SSL証明書の更新用）
    location /.well-known/acme-challenge/ {
        root /usr/share/nginx/html;
    }

    # それ以外は https に一律強制リダイレクト
    return 302    https://www.example.jp$request_uri;
}
server {
    listen        443 ssl;
    server_name   www.example.jp;

    ssl_certificate          /etc/letsencrypt/live/www.example.jp/fullchain.pem;
    ssl_certificate_key      /etc/letsencrypt/live/www.example.jp/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    ssl_trusted_certificate  /etc/letsencrypt/live/www.example.jp/chain.pem;
    ssl_dhparam              /ssl/private/dhparam.pem;

#    add_header Strict-Transport-Security "max-age=63072000" always;

    ssl_session_cache shared:SSL:10m;

    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    location / {
        root /usr/share/nginx/html;
    }
}
```

（conf のほうは、80ポートの「certbot renew」用の記述と、https 強制リダイレクト、443 ポートに関する記述を追加。）

Nginx 設定ファイルについては、コンテナ（ボリューム）内に再設置します。

```shell-session
$ docker cp vhost-www.example.jp.conf nginx:/etc/nginx/conf.d/vhost-www.example.jp.conf
```

:::note warn
http ⇒ https 強制リダイレクトは 302（一時的）リダイレクトを使用していますが、環境構築が終了し、運用が安定した時点で 301（恒久的）リダイレクトに変更したほうが良いと思います。
302 の場合は「ブラウザ ⇒ サーバ」に都度アクセスしたうえでリダイレクトを判断されますが、301 の場合、ブラウザ内にキャッシュが残っているかぎり、サーバにはアクセスせず、キャッシュの内容からリダイレクトを取得し遷移します。これは無用なアクセストラフィックの軽減に繋がります。

ただしこれは、すべてのブラウザがこの挙動をするとは限りません。
また、誤って 301 リダイレクトを指定した場合、ブラウザ側のキャッシュを削除しないかぎり、正しい遷移ができなくなる可能性があるので、注意が必要です。
:::

:::note warn
「add_header Strict-Transport-Security」は、HSTS（HTTP Strict Transport Security）に関する設定です。
こちらもセキュリティ上は、設定することが望ましいです。

しかしながら誤って設定した場合、こちらもブラウザ側で設定を解除する必要が生じる可能性があります。
そのため環境構築が終了し、運用が安定した時点でコメント状態を解除したほうが良いと思います。
:::

## nginx 再起動（https 対応）

Nginx を再起動して SSL を有効化します。

```shell-session
$ docker-compose stop nginx
$ docker-compose up -d nginx
$ docker-compose logs nginx
```

上記３つめのコマンドは、Nginx のログ確認のため。エラーが出てなければ OK。
この状態で https, http 両方で Nginx のブランクページが表示されるかを確認します。

```
http://www.example.jp
https://www.example.jp
```
さらに https の場合は、証明書が適切に入っているか（鍵マークになっているか）と、証明書の中身も確認しましょう。

:::note info
ここで一旦、EC2 のスナップショットを取得。（２番目）
この先は Redmine, MySQL の環境構築を行うので、破壊時の戻り先にしておく。
:::

## certbot 再起動（SSL証明書更新用）

certbot も docker-compose.yml の entrypoint の内容を反映させるため、再起動します。

```shell-session
$ docker-compose stop certbot
$ docker-compose up -d certbot
$ docker-compose logs certbot
```

これによって、Composer ファイル内の記述されている COMMAND が実行され、定期的に「certbot renew」が実行されるようになります。

:::note info
ちなみに３つめのコマンドは、certbot renew のログ確認のためです。
エラーが出てなければ問題ありませんが、nginx, certbot ともに、設定作業後しばらく（初回の SSL 更新が行われるまで）は、「docker-compose logs」コマンドで定期的に動作を確認したほうが良いと思います。
:::

[次の記事](https://qiita.com/ax_kazz/items/1285534b46aeb307d7e0 "4/5 MySQL, Redmine の構築")に続きます。

__↓ 4/5 MySQL, Redmine の構築__

https://qiita.com/ax_kazz/items/1285534b46aeb307d7e0

__↓ 5/5 引用・謝辞__

https://qiita.com/ax_kazz/items/be1e491910b9546bc8c1

# 参考・引用

http://kikei.github.io/server/2020/07/11/redmine.html

https://www.nginx.co.jp/blog/deploying-nginx-nginx-plus-docker/