docker環境で let's encrypt のssl証明書を取得する

# はじめに

- この記事では、dockerのnginxコンテナを使ったwebサイトのhttps化手順をまとめています。
- dockerコンテナと、let's encryptを使ってサイトのhttps化を目指している方の参考になれば幸いです。
- また、自身の知識の整理のためにssl証明書について冒頭でまとめていますが、既にご存じの方は[3. ssl証明書の取得手順](#3-ssl証明書の取得手順)までスキップしてください。(もし知識の浅い部分があればご指南いただけると嬉しいです。)

## 筆者の開発環境

- サーバ: ubuntu 20.04
- nginx (docker)

## 目次

[1. ssl証明書の目的](#1-ssl証明書の目的)
[2. let's encryptについて](#2-lets-encryptについて)
[3. ssl証明書の取得手順](#3-ssl証明書の取得手順)
[4. ssl証明書の更新方法](#4-ssl証明書の更新方法)
[5. おわりに](#5-おわりに)

### 1. ssl証明書の目的

- 結論から言うと、ssl証明書の目的は webサイトの通信を暗号化してセキュリティを高めることです。
- 暗号化されていないhttp通信では、平文のままサーバ間でやりとりをするため通信が傍受されてしまった場合、情報を悪用されるリスクがあります。
- 一方でssl通信は、通信の内容が暗号化されているため傍受された場合でも、傍受者はその通信内容を悪用することができません。

:::note info
ssl証明書の目的

通信を暗号化して情報の漏洩を防ぐこと
:::

#### ssl証明書の種類

- webサイトでssl通信をするために必要な ssl証明書 は無料のものから十数万するものまであります。
- ただ、無料だからといって、通信の暗号化の機能が劣っているということではないそうです。
- では、なぜ有料のssl証明書があるかというと、サイト運営者の身分証明 のためです。
- ssl証明書には大まかに下記の３種類があります。

| 証明書の種類 | 身分証明方法 | 価格(3つの中の比較になります) | 
| -- | -- | -- |
| ドメイン認証 | ドメイン所有者の情報(メールアドレス等) | 低 |
| 企業認証 | 組織・企業の情報 | 中 |
| EV証明 | 組織・企業の情報(所在地含む) | 高 |

- 価格が高くなるにつれて、提示する個人情報が詳細になっていきます。
- 個人情報が詳細になるほどwebサイトのセキュリティ(信頼性)としては高いと言えます。
- これまで説明したように、ssl証明書は無料でメールアドレスの確認だけで作れてしまうものもあります。
    - サイトの運用者が悪者だった場合は、いくらssl通信で暗号化されているからといって安全とは言えません。
- もし、これが組織がお金を払い、身分情報を提示して発行したssl証明書であれば、サイトの信頼性は高く安全と言えると思います。

:::note info
有料のssl証明書が持つ役割

ドメイン所有者の個人情報を担保にして、サイトの信頼性を高めること
:::


### 2. lets encryptについて

- これまで 有料のssl証明書の役割について説明してきましたが、個人のwebサイトを試しに立ち上げてみたい、というような方であれば最低限のssl通信を実現できるssl証明書があれば十分だと思います。
- 無料のssl証明書 としてlet's encrypt が有名どころとしてあります。
    - この記事を作るにあたり他の無料ssl証明書も調べたところ zerossl というサービスもあるようです。全てブラウザで完結するとのことなので lets encrypt よりも簡単かもしれないです。
    - 詳しい内容は下記の記事でまとめられていたので興味を感じられた方をご覧ください。

> - 無料の SSL 証明書が得られる ZeroSSL を使ってみた
>   - https://zenn.dev/mattn/articles/b2c4c92c9116b1

- この記事では let's encryptを使ってwebサイトにssl証明書を適用する方法を紹介していきます。

### 3. ssl証明書の取得手順

##### let's encrypt 公式手順は docker 環境では参考にできない
- let's encrypt 公式のgetting started を見ます。
    - https://letsencrypt.org/ja/getting-started/
- 公式によると、`certbot`というサービスを使うことで簡単に証明書の取得ができるようです。
- `certbot`のページを開くと、各環境に合ったcertbotのインストール手順を紹介してくれます
    - ただ、今回の環境は dockerのnginxコンテナで動いているため該当する選択肢はないように見えました。

##### certbot のdockerイメージを取得する

- 情報を探すと、公式が出している`certbot`のコンテナイメージがありました。
    - https://hub.docker.com/r/certbot/certbot/
- こちらのイメージを使い、`Dockerfile`と`docker-compose.yml`を作成します
- 筆者はこのような構成でディレクトリを作成しました。

```markdown
.
└── nginx_docker
    ├── Dockerfile ... nginx用
    ├── certbot
    │   └── Dockerfile ... certbot用
    ├── conf.d
    │   ├── default.conf ...初めてssl証明書を取得するまで使うnginx設定ファイル
    │   └── ssl_server.conf.txt ...ssl証明書の取得後、使うnginx設定ファイル
    ├── dist ... nginxでこちらのディレクトリの資材へルーティングします
    │   └── index.html
    └── docker-compose.yml

```

#### ここからは各種設定ファイルの準備をしていきます

##### 1. certbotと、nginxののDockerfileを作る

- certbotとnginxのDockerfileの中身です

- certbot

```Dockerfile
# base image
FROM certbot/certbot:latest
```

- nginx

```Dockerfile
# base image
FROM nginx:latest

WORKDIR /dist

RUN nginx -t
RUN service nginx restart
```

##### 2. docker-compose.ymlにcertbotを追加する

[1/6 更新] 記載漏れをご指摘いただきましたので一部更新いたしました。
https://qiita.com/mttt/items/aa2ba3a0677a803d0436#comment-838affca72c5a7837fa7

```yml
version: "3"
services:
  nginx:
    image: nginx:latest
    # nginxをリスタートした時にコンテナが終了しないようにする
    restart: always ... certbotで証明書を取得した後、nginxをリスタートします。こちらを入れないとリスタートでコンテナが落ちるので追加します。
    container_name: nginx
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "80:80"
      - "443:443" # コンテナへhttps通信が通るようにport解放します
    # 20日おきにnginxを再読み込みする
    command: "/bin/sh -c 'while :; do sleep 20d & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
    volumes:
      # nginxの設定ファイル
      - ./conf.d:/etc/nginx/conf.d
      # nginxのルートパス
      - ./dist:/dist
      - /etc/letsencrypt:/etc/letsencrypt #ssl証明書が配置されるパス
  certbot:
    image: certbot/certbot:latest
    container_name: certbot
    build:
      context: .
      dockerfile: ./certbot/Dockerfile
    # 10日おきに証明書を更新する(期限切れまで30日以上あれば更新しない)
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 10d & wait $${!}; done;'"
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt #こちらに取得したssl証明書が配置されます。nginxコンテナでも同じくマウントして共有します。
      - ./dist:/dist
    command: ["--version"]
```

- 補足したい部分についてはコメントを追記しています。
  - 証明書を取得すると`/etc/letsencrypt`に配置されます。これはnginxコンテナでも使いたいので同じパスをマウントします。
  - nginxコンテナで`restart: always`しておくと、コンテナ外からnginxのリロードをしてもコンテナが落ちなくなるのでつけておくと良いです


##### 3. nginxの設定ファイルを作成する

- nginx設定ファイルは初回のssl証明書取得前後で、２つ使い分けます

- まずは初めてssl証明書を取得するまで使う設定ファイル`default.conf`

```conf
server {
    listen       80;
    server_name  {ドメイン名}; # ★適宜変更してください

    location / {
        root   /dist/;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```

- 次に、ssl証明書取得後に使う設定ファイルです。`ssl_server.conf.txt`
    - 使うときは、`.txt`の拡張子を外します。

```conf
server {
    server_name  {ドメイン名};　# ★ドメイン名を適宜変えてください

    listen 80;
    listen [::]:80;

    # 証明書更新のために:80/.well-knownにアクセスしたいので、ここだけリダイレクトはしない
    location ^~ /.well-known {
        root /dist/;
    }

    # 全てのリクエストをSSLサイトにリダイレクト
    location / {
        return 301 https://$host$request_uri;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

server {
    server_name  {ドメイン名};　# ★ドメイン名を適宜変えてください

    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate      /etc/letsencrypt/live/{ドメイン名}/fullchain.pem; # ★ドメイン名を適宜変えてください
    ssl_certificate_key  /etc/letsencrypt/live/{ドメイン名}/privkey.pem;　# ★ドメイン名を適宜変えてください
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_ciphers '{任意のアルゴリズム}'; ★適宜変えてください。ssl_ciphers自体を省略でも動作するようです。
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=2592000" always;

    location / {
        root   /dist/;
        index  index.html index.htm;
    }
}

```

- 80ポートに来た通信は全て、443ポートのhttps通信にリダイレクトされるようにしています。
- 2つの設定ファイルを使い分ける理由
    - certbotは初めて証明書を取得するときに、80ポートで通信します。そのため、`ssl_server.conf.txt`で設定されているように、443ポートにリダイレクトされると失敗します。初回は`.well-known`ディレクトリも使われないです。

#### 設定ファイルを元にコンテナを立ち上げていきます。

- docker-compose.ymlのあるパスまで移動して、下記コマンドを実行します。(コンテナイメージの取得)
    - `docker compose build`
- dockerコンテナを立ち上げます
    - `docker compose up -d nginx`
    - この時点だと、まだssl証明書が入っていないので、サイトは閲覧できないはずです
- certbotコンテナを実行して、ssl証明書を取得します。
    - `docker compose run --rm certbot certonly --webroot -w {nginxのドキュメントルート} -d {ドメイン名}`
        - {nginxのドキュメントルート}は設定ファイルのままであれば、`/dist`になります
        - {ドメイン名}は適宜変更してください
    - これで、`etc/letsencrypt`配下に証明書が配置されているはずです
- これで、ssl証明書を使う準備が整ったので、nginx.confを`ssl_server.conf.txt`に差し替えます。
    - `ssl_server.conf.txt`の`.txt`拡張子を外します。
    - `default.conf`に拡張子`.txt`を追加します。(.conf拡張子が外れるとnginxで適用されなくなります)
- そして`docker compose exec nginx nginx -s reload`を実行してnginxをリロードします
- 以上で、httpsで通信できれば作業は完了です。


> 2023/7/9更新 上記default.conf 8行目付近の記載に誤字があったため修正しました。
> location ^~ /.well-known { 
>   [誤] root /dsit/;
>   [正] root /dist/; 

### 4. ssl証明書の更新方法

- let's encrypt の証明書は`90日`で期限が切れてしまうために定期的に更新が必要です。
- ただ、今回紹介した`ssl_server.conf`の設定であれば、docker-compose.ymlで設定した`entrypoint`が働き、定期的に証明書を取り直してくれるため、基本的には更新作業は不要です。

- 証明書の自動更新方法については下記の記事を参考にさせていただきました。

> - docker nginx + Let's encrypt 自動更新(cron使わないで)
>   - https://silver0480.blog.fc2.com/blog-entry-474.html


- ※[注意] 自動更新が動作確認については本記事ではまだ確認できていないため、すぐに動作確認したい方は上記の記事をご覧ください。（時間が経過して動作確認がとれたら記事を更新する予定です）

### 5. おわりに

- 作業を振り返って、dockerを使った let's encrypt証明書の発行も意外と簡単でした。
    - 公式からdockerイメージが出されているのが大きかったです。
- 一方で、Dockerfileや、docker-compose.ymlのノウハウがあれば、自動更新周りをもう少し見通しよく作れそうだと感じたので、個人的には証明書更新よりもdocker難しいという感想を持ちました。
- ssl証明書の作成を通じて、webサイトのセキュリティについても興味を持ち始めたので、今後はそちらについても記事発信していきたいです。