nginxでhttpsサイトを公開しよう！

# はじめに


[MYJLab Advent Calender](https://qiita.com/advent-calendar/2022/myjlab) 22日目の記事です。担当のセイです。

参加は2年目ですが、昨年と違って、今年は卒論に追われながら参加しています。

httpsの理念からSSL/TLSとか、非対称暗号とか、CAとルート証明書とか、man-in-the-middle attackとか、丁寧に語りたいが、なかなかそうも言っていられない状況ですので、超ざっくりで説明します。

<br/>
<br/>
<br/>

つまり、理屈とかいいかとにかくワイのサイトに綺麗な🔒マークをつけるぞ！！！！

<br/>
<br/>
<br/>



という勢いです。

なお、
この記事で作成したファイルは以下のリポジで公開しています。
[https://github.com/KuroiCc/myjlab-advent-calendar-2022-day22/tree/main](https://github.com/KuroiCc/myjlab-advent-calendar-2022-day22/tree/main)

作ったサイトは以下のlinkで公開しています。
気まぐれで（awsから請求が来たら）予告なく消す場合がありますので、見てみたい人はお早めに。
[https://www.myjlab-advent-calendar-2022-day22.ml/](https://www.myjlab-advent-calendar-2022-day22.ml/)

# 事前準備

1. dockerがインストールされていて、Public IPでアクセルできるホスト
2. ドメインをすでに持ってかつそのドメインは上記のホストのPublic IPに紐ついている

誰でも再現できるためにこの記事ではホストはAWS EC2で公開しています。OSは`Ubuntu, 22.04 LTS, amd64` 、インスタンスタイプは `t2.micro` を使用しています。

ドメインはfreenomというサイトに無料でとりました。


https://www.freenom.com

# HTTP(sじゃない方)で公開


新しいAWS EC2インスタンスを作ってsshで接続します。

まずは、apt更新とdockerのインストール

```Bash
sudo apt update && apt upgrade
sudo apt install docker docker-compose
```


dockerインストールしたら、動いてるかどうか試してみよう


```Bash
sudo docker run hello-world
```


![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/CEFA71BE-3078-44BD-A727-0E1779AD7ED1_2/PLHKpMBTQM9vksOJL6xePA9Ub84mpSdwW322QiygzVkz/Image.png)

よし、動いていそうですね

nginxをホストマシンにインストールするのは結構面倒臭いので、dockerでnginxサーバを立ち上げます。

今回のプロジェクトフォルダーを作って中に移動する

```Bash
mkdir nginx-docker && cd nginx-docker
```


docker-compose.ymlファイルを作る

```Bash
nano docker-compose.yml
```


中身はこんな感じ

```docker
services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80 # ホストの80ポートをこのコンテナの80に転送する
      - 443:443 # ホストの443ポートをこのコンテナの443に転送する
    volumes:
      # マウントする必要のあるもの
      - /etc/letsencrypt:/etc/letsencrypt # あとで発行する証明書の置き場
      - ./conf:/etc/nginx/conf.d # 設定ファイルの置き場
      - ./logs:/var/log/nginx # nginxのlog等の置き場
      - ./html:/usr/share/nginx/html # 公開する内容の置き場
    environment:
      - TZ=Asia/Tokyo # コンテナ内のtimezone
```


![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/6D8417BC-D09C-42BB-85D1-DA7ACF46FF3A_2/8QlZ9dyQXFoss7ryBryoddTUvNequ5GBSDIw04AJHpsz/Image.png)

次に公開するページを用意します。

```Bash
mkdir html
nano html/index.html
```


これ以上はないほどにシンプルなページです。

```html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MYJLab Advent Calendar 2022 Day 22 - Work Page</title>
  <!-- Tailwind CSS CDN -->
  <script src="https://cdn.tailwindcss.com"></script>

</head>

<body>
  <!-- some cool tailwind page with just title generate by copilot -->
  <div class="flex flex-col justify-center min-h-screen py-6 bg-gray-100 sm:py-12">
    <div class="relative py-3 sm:max-w-xl sm:mx-auto">
      <div
        class="absolute inset-0 transform -skew-y-6 shadow-lg bg-gradient-to-r from-cyan-400 to-light-blue-500 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl">
      </div>
      <div class="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
        <div class="max-w-md mx-auto">
          <div class="flex items-center justify-between space-x-2">
            <img src="https://www.myjlab.org/img/logo-small.png" class="h-7" alt="MYJLab">
            <img src="https://www.myjlab.org/img/favicon.ico" class="h-7 rounded" alt="MYJLab">
          </div>
          <div class="divide-y divide-gray-200">
            <div class="py-8 space-y-4 text-base leading-6 text-gray-700 sm:text-lg sm:leading-7">
              <p>MYJLab Advent Calendar 2022 Day 22 - Work Page</p>
              <p>Thank you for visiting my site.</p>
              <p>Have a nice day.</p>
            </div>
            <div class="pt-6 text-base font-bold leading-6 sm:text-lg sm:leading-7">
              <p>MYJLab Advent Calendar 2022</p>
              <p><a href="https://qiita.com/advent-calendar/2022/myjlab"
                  class="text-cyan-600 hover:text-cyan-700">https://qiita.com/advent-calendar/2022/myjlab</a></p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>

</html>
```


![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/556DEFED-FCF0-4793-9F99-9E1CF7013EC5_2/y6xHtEm5jvY0V3PEDK6Dxb565E1cLSq9YZpWmy2Su1Mz/Image.png)

あとでHTTPSのSSL証明書を発行ためのフォルダです。説明はあとでします。

```Bash
mkdir html/_letsencrypt
```


次はniginxの設定ファイルを作成します。

```Bash
mkdir conf
nano conf/default.conf
```


内容はこんな感じ

ページを公開するめに最低限の設定、`server_name` のところは自分のドメインの変更するように

```nginx
server {
    listen 80;
    listen [::]:80; # 80ポートを監視
    server_name www.myjlab-advent-calendar-2022-day22.ml;

    # コンテナ内部の/usr/share/nginx/htmlのフォルダ(=./html)をroot path(`/`)として公開
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
    }

    # コンテナ内部の/usr/share/nginx/html/_letsencrypt(=./html/_letsencrypt)を/.well-known/acme-challengeのpathとして公開
    location /.well-known/acme-challenge/ {
        root /usr/share/nginx/html/_letsencrypt;
    }
}

```


![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/08D1A09E-A992-4DAB-8830-E2E259713754_2/FQMGZV9qRp1esHLddxUrYCX1TcoGNgOyK4A6rWCjo0Uz/Image.png)

次はdockerを起動すればサイトは公開されるはず。

```Bash
sudo docker-compose up -d
```


httpでアクセスしてみると。。。。。アクセスできる！！！

だが、保護されない通信とある、これはいけません。

今からこのサイトに綺麗な🔒マークをつけていきます。

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/C2833624-316B-4E3E-AD66-2B02C1160980_2/a0NMS1TMBuZbbcrIQWBBJXYl741CPTjrHI1x2sOs9GQz/Image.png)

# HTTPSでサイトを公開


HTTPSにするには一番手間なのは証明書の発行です。

今回は米国の非営利団体で運営ししている [Let's Encrypt](https://letsencrypt.org) という認証局にSSL証明書を発行してもらいます。

実際はcertbotという[Let's Encrypt](https://letsencrypt.org)公式推奨なツールで証明書を発行します。

[Let's Encrypt](https://letsencrypt.org)が発行する証明書の有効期限は90日しかないが、certbotは自動更新もしてくれます。素晴らしい！！！

自動で証明書発行原理はあとでちょっとだけ説明しますが、

、、、、とりあえず、、、まずは、コマンド打って発行してみよう！！！

certbotをインストール

```Bash
sudo apt install certbot
```


インストールできたら以下のコマンドで会話的に証明書を発行できます。発行は成功しても失敗しても1日に上限がありますので、最初は `--dry-run` をつけてお試しに発行してみましょう

```Bash
sudo certbot certonly --webroot --dry-run
```


最初はユーザ登録のためemailを要求してきます。

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/57D3FF68-099A-4C6A-96F3-789AC1317AC0_2/jNyItDdyoehAVbuaXMkNlAyxk9cJrutXOsm5AecghnUz/Image.png)

利用契約を同意してくださいという意味です、もちろんyyyyyyyyyyes!

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/8F60CD5D-BAF5-4517-B2DE-54313C5BBCF9_2/7tHsUC0upPpYkP3Yswsee6CaubzgyUbqWOt86cPre08z/Image.png)

申し込むドメインを入力します

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/118BA142-A6AF-4C9F-871C-7180896CF573_2/jlxLYrAbIHy3zyRliGETqro5o282dGfwV0pAP48ZyMMz/Image.png)

webサイトのルートディレクトリを入力してくださいという意味です、

ここは `./html/_letsencrypt` と入れます

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/86F0D0CF-9699-4F81-9067-44A50FAAF683_2/hQqspRyi8byNEpQTfhEPXRoWJrdSJPDqDWslN7UpO2gz/Image.png)

このメッセージが表示されたら、発行成功です。

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/1A84D9D0-D7E3-4A56-B4B4-98BD6B4476CD_2/MXBTrfHOr6WcXZMOaImxWilCdW50go7g9oqVgt5wjlEz/Image.png)

次は `--dry-run` 抜きで、正式に発行してみましょう！

```Bash
sudo certbot certonly --webroot
```


![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/B919EA6F-3196-4CF6-A7D4-67960FDDE068_2/qWY9zGQy9sGbR4xa2om9rf2PXCuhZDPmapGVTWwSKyMz/Image.png)

と、、こういうメッセージが表示されたら成功です。

証明書は → `/etc/letsencrypt/live/www.myjlab-advent-calendar-2022-day22.ml/fullchain.pem`

キーは → `/etc/letsencrypt/live/www.myjlab-advent-calendar-2022-day22.ml/privkey.pem`

にそれぞれ保存だれています。

実は、dockerで設定しているときここには罠がございます。文章の最後に書きますので、興味ある人は読んでみてください。

次は、HTTPSに対応できるようniginxの設定ファイル変更します。

```Bash
echo "" > conf/default.conf
nano conf/default.conf
```


内容は

```nginx
server {
    # http 80番ポートでのリクエストをすべてhttpsにリダイレクト
    listen 80;
    listen [::]:80;
    server_name www.myjlab-advent-calendar-2022-day22.ml;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.myjlab-advent-calendar-2022-day22.ml;

    # SSL証明書の設定
    ssl_certificate /etc/letsencrypt/live/www.myjlab-advent-calendar-2022-day22.ml/fullchain.pem;
    # SSL秘密鍵の設定
    ssl_certificate_key /etc/letsencrypt/live/www.myjlab-advent-calendar-2022-day22.ml/privkey.pem;
    # SSLのプロトコルを指定
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    # コンテナ内部の/usr/share/nginx/htmlのフォルダ(=./html)をroot path(`/`)として公開
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
    }

    # コンテナ内部の/usr/share/nginx/html/_letsencrypt(=./html/_letsencrypt)を/.well-known/acme-challengeのpathとして公開
    # 証明書自動更新用
    location /.well-known/acme-challenge/ {
        root /usr/share/nginx/html/_letsencrypt;
    }
}
```


dockerのコンテナを再起動すればうまくいくはずです。

```bash
sudo docker-compose restart
```


サイトをアクセスしてみると！！！！

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/DF4A6637-15E2-4C31-8337-6142CD801495_2/qpdHHxXKuw03nk0kGNML62uijrspkUhwj4vxup3CjyIz/Image.png)

綺麗な🔒マークが、、、、、ついてる！！！！！！！

証明書の中身は確認できている。すばら！

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/AD91AD9B-766E-4F06-9737-A8C28B92E8D7_2/U82oH2DJncrO8Q0H4oK3F3xbh4Xv1x8K9gsIg6k49v4z/Image.png)

参考までに、ec2を立ち上げてから入れた全てのコマンドここに置いときます

```Bash
ubuntu@ip-172-31-40-34:~/nginx-docker$ history 

1  sudo apt update && apt upgrade  
2  sudo apt install docker docker-compose  
3  sudo docker run hello-world  
4  mkdir nginx-docker && cd nginx-docker  
5  nano docker-compose.yml  
6  mkdir html  
7  nano html/index.html  
8  mkdir html/_letsencrypt  
9  mkdir conf  
10  nano conf/default.conf  
11  sudo docker-compose up -d  
12  sudo apt install certbot  
13  sudo certbot certonly --webroot --dry-run  
14  sudo certbot certonly --webroot  
15  nano conf/default.conf  
16  echo "" > conf/default.conf  
17  nano conf/default.conf  
18  sudo docker-compose restart  
19  history

```


# 証明書の自動発行の原理及びACME


[Let's Encrypt](https://letsencrypt.org) はACME(アクミー, Automatic Certificate Management Environment)というプロトコルで証明書を発行している。それについてすごくざっくり説明します。

とあるドメインに対して証明書を発行するには、認証局にそのドメインは確かに自分がコントロールしていることを証明する必要があります。

以前は人工的にやる必要があるが、それを自動化するため、ACMEというプロトコルが提出されました。

原理は、まず証明書の利用者がACMEクライアント(この記事の場合はcertbot)のコマンドを実行して、ホストに暗号ファイルを置きます。

次に、ACMEサーバ(認証局、この記事の場合は[Let's Encrypt](https://letsencrypt.org))がドメインをアクセルし、暗号ファイルを取得して答え合わせをします。答え合わせができたら、利用者は確かにそのドメインをコントロールしていることが証明され、証明書は発行されます。

## 検証


証明書を更新するコマンドを実行します

```Bash
sudo certbot renew --webroot --dry-run
```


すると提供したルートディレクトリ(この記事の場合は `./html/_letsencrypt`)にこんなファイルが生成された

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/8AD98855-B7F7-4757-9666-59CCE9939515_2/PfVUYBprdteigTfIIn2DfFCNHW0efwa0d1O4BBUqRyEz/Image.png)

それと同時に`nginx-docker/logs/access.log` からもこんなアクセル履歴を確認できます

```other
23.178.112.106 - - [22/Dec/2022:22:43:06 +0900] "GET /.well-known/acme-challenge/VxVUs8mtN14xNYYomMK-cTP8p6nxLMlAkrPh9WP_gkg HTTP/1.1" 200 87 "http://www.myjlab-advent-calendar-2022-day22.ml/.well-known/acme-challenge/VxVUs8mtN14xNYYomMK-cTP8p6nxLMlAkrPh9WP_gkg" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" "-"
```


## 認証用nginx設定


```other
    # コンテナ内部の/usr/share/nginx/html/_letsencrypt(=./html/_letsencrypt)を/.well-known/acme-challengeのpathとして公開
    location /.well-known/acme-challenge/ {
        root /usr/share/nginx/html/_letsencrypt;
    }
```


前にあとで説明したというこの設定はこの自動認証を通すために設定です。

今回のように静的サイトの場合静的サイトの場合は**URLのパスはそのままディレクトリのpathに変換されます**ので、この設定を消して、webルートディレクトリーをほんとのルートディレクター `./html` に設定しても認証チャレンジ自体は通ることができます。

`<your domain>/any/path/here` に対してアクセルすると
↓
`./html/any/path/here` というファイルのアクセルに変換されます

認証チャレンジの時は

`./html/.well-known/acme-challenge/<暗号ファイル with long hash as its name>` にこの場合暗号ファイルは生成されて、
↑
`<your domain>/.well-known/acme-challenge/<暗号ファイル with long hash as its name>` のurl対するアクセルはこのままファイルのアクセルに変換されます。

ところが、webサイトとして公開する内容はReactなどのSPAの場合は、URLのpath関係なく全部のアクセルは index.htmlのアクセスにリダイレクトされる。

`<your domain>/any/path/here`
`<your domain>/どんな/パス/であっても` に対するアクセルはすべて
↓
`./html/index.html` に転送されます。

この場合は認証チャレンジを通ることはできないので、特別に `/.well-known/acme-challenge/` というパスへのアクセルするときのみ、認証専用のフォルダに転送するよう設定する必要があります。

# docker nginxに[Let's Encrypt](https://letsencrypt.org) 利用時の落とし穴


証明書を発行成功するとき

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/7A47DE78-EFD9-4A9A-933C-2BA614EFA9C2_2/vYSG2cLrrAx0yRTYNmZpD364nph3b3Pf6LPC3BzRsCoz/Image.png)

証明書は → `/etc/letsencrypt/live/www.myjlab-advent-calendar-2022-day22.ml/fullchain.pem`  
キーは → `/etc/letsencrypt/live/www.myjlab-advent-calendar-2022-day22.ml/privkey.pem`

に保存されましてよというメッセージが表示されます。

これをみるとこういう風に `docker-compose.yml` を書いてしまいがちです。

```docker
volumes:
  - /etc/letsencrypt/live:/etc/letsencrypt/live
```


あるいは

```docker
volumes:
  - /etc/letsencrypt/live/<your.domain>:/etc/letsencrypt/live/<your.domain>
```


でもこの書き方は間違いです、ダメです、バツ❌です。

nginxのコンテナは証明書を見つかりませんとかいうエラーを吐いて、一瞬で終了しちゃいます。

その原因は、実は証明書とそのキーはメッセージで表示された場所には保存していませんからです。

`ls -al` で証明書が保存されるとしているフォルダの中身を確認してみると

![image](https://res.craft.do/user/full/d848fa91-9600-e1b9-5a97-6c91c0ea832e/doc/B4799826-B6E6-4B7F-A4CA-2B207395CC62/84E6FD83-EFBB-4622-8F77-E2572537BF16_2/pImsOSO5TGYLv08RcLf5Mv7vNDUI1AJ7uCFge5NSU7Az/Image.png)

そのフォルダの中身は証明書の本体ではなく、本体へのソフトリンクでした

上記のように `docker-compose.yml` を書くと `/etc/letsencrypt/aechive` はコンテナにマウントされないので、当然証明書は見つからないわけだ！

ということで、必ず

```docker
volumes:
  - /etc/letsencrypt:/etc/letsencrypt
```


と書かないといけない。

# まとめ

certbotを使えばLet's Encryptから簡単にSSLの証明書を発行することができて、自分のサイトに綺麗な🔒マークをつけさせる、、いかがでしたか？

クラウドインフラが主流の今ではhttpsなんてやってくれるところが多いのであんまり必要ないかもしれないね

眠いのでここまでにしときます。

では僕が無事卒業できることを祈って、またいつかでお会いしましょう！