はじめに
MYJLab Advent Calender 22日目の記事です。担当のセイです。
参加は2年目ですが、昨年と違って、今年は卒論に追われながら参加しています。
httpsの理念からSSL/TLSとか、非対称暗号とか、CAとルート証明書とか、man-in-the-middle attackとか、丁寧に語りたいが、なかなかそうも言っていられない状況ですので、超ざっくりで説明します。
つまり、理屈とかいいかとにかくワイのサイトに綺麗な🔒マークをつけるぞ!!!!
という勢いです。
なお、
この記事で作成したファイルは以下のリポジで公開しています。
https://github.com/KuroiCc/myjlab-advent-calendar-2022-day22/tree/main
作ったサイトは以下のlinkで公開しています。
気まぐれで(awsから請求が来たら)予告なく消す場合がありますので、見てみたい人はお早めに。
https://www.myjlab-advent-calendar-2022-day22.ml/
事前準備
- dockerがインストールされていて、Public IPでアクセルできるホスト
- ドメインをすでに持ってかつそのドメインは上記のホストのPublic IPに紐ついている
誰でも再現できるためにこの記事ではホストはAWS EC2で公開しています。OSはUbuntu, 22.04 LTS, amd64
、インスタンスタイプは t2.micro
を使用しています。
ドメインはfreenomというサイトに無料でとりました。
HTTP(sじゃない方)で公開
新しいAWS EC2インスタンスを作ってsshで接続します。
まずは、apt更新とdockerのインストール
sudo apt update && apt upgrade
sudo apt install docker docker-compose
dockerインストールしたら、動いてるかどうか試してみよう
sudo docker run hello-world
よし、動いていそうですね
nginxをホストマシンにインストールするのは結構面倒臭いので、dockerでnginxサーバを立ち上げます。
今回のプロジェクトフォルダーを作って中に移動する
mkdir nginx-docker && cd nginx-docker
docker-compose.ymlファイルを作る
nano docker-compose.yml
中身はこんな感じ
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
次に公開するページを用意します。
mkdir html
nano html/index.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>
あとでHTTPSのSSL証明書を発行ためのフォルダです。説明はあとでします。
mkdir html/_letsencrypt
次はniginxの設定ファイルを作成します。
mkdir conf
nano conf/default.conf
内容はこんな感じ
ページを公開するめに最低限の設定、server_name
のところは自分のドメインの変更するように
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;
}
}
次はdockerを起動すればサイトは公開されるはず。
sudo docker-compose up -d
httpでアクセスしてみると。。。。。アクセスできる!!!
だが、保護されない通信とある、これはいけません。
今からこのサイトに綺麗な🔒マークをつけていきます。
HTTPSでサイトを公開
HTTPSにするには一番手間なのは証明書の発行です。
今回は米国の非営利団体で運営ししている Let's Encrypt という認証局にSSL証明書を発行してもらいます。
実際はcertbotというLet's Encrypt公式推奨なツールで証明書を発行します。
Let's Encryptが発行する証明書の有効期限は90日しかないが、certbotは自動更新もしてくれます。素晴らしい!!!
自動で証明書発行原理はあとでちょっとだけ説明しますが、
、、、、とりあえず、、、まずは、コマンド打って発行してみよう!!!
certbotをインストール
sudo apt install certbot
インストールできたら以下のコマンドで会話的に証明書を発行できます。発行は成功しても失敗しても1日に上限がありますので、最初は --dry-run
をつけてお試しに発行してみましょう
sudo certbot certonly --webroot --dry-run
最初はユーザ登録のためemailを要求してきます。
利用契約を同意してくださいという意味です、もちろんyyyyyyyyyyes!
申し込むドメインを入力します
webサイトのルートディレクトリを入力してくださいという意味です、
ここは ./html/_letsencrypt
と入れます
このメッセージが表示されたら、発行成功です。
次は --dry-run
抜きで、正式に発行してみましょう!
sudo certbot certonly --webroot
と、、こういうメッセージが表示されたら成功です。
証明書は → /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の設定ファイル変更します。
echo "" > conf/default.conf
nano conf/default.conf
内容は
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のコンテナを再起動すればうまくいくはずです。
sudo docker-compose restart
サイトをアクセスしてみると!!!!
綺麗な🔒マークが、、、、、ついてる!!!!!!!
証明書の中身は確認できている。すばら!
参考までに、ec2を立ち上げてから入れた全てのコマンドここに置いときます
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 はACME(アクミー, Automatic Certificate Management Environment)というプロトコルで証明書を発行している。それについてすごくざっくり説明します。
とあるドメインに対して証明書を発行するには、認証局にそのドメインは確かに自分がコントロールしていることを証明する必要があります。
以前は人工的にやる必要があるが、それを自動化するため、ACMEというプロトコルが提出されました。
原理は、まず証明書の利用者がACMEクライアント(この記事の場合はcertbot)のコマンドを実行して、ホストに暗号ファイルを置きます。
次に、ACMEサーバ(認証局、この記事の場合はLet's Encrypt)がドメインをアクセルし、暗号ファイルを取得して答え合わせをします。答え合わせができたら、利用者は確かにそのドメインをコントロールしていることが証明され、証明書は発行されます。
検証
証明書を更新するコマンドを実行します
sudo certbot renew --webroot --dry-run
すると提供したルートディレクトリ(この記事の場合は ./html/_letsencrypt
)にこんなファイルが生成された
それと同時にnginx-docker/logs/access.log
からもこんなアクセル履歴を確認できます
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設定
# コンテナ内部の/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 利用時の落とし穴
証明書を発行成功するとき
証明書は → /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
を書いてしまいがちです。
volumes:
- /etc/letsencrypt/live:/etc/letsencrypt/live
あるいは
volumes:
- /etc/letsencrypt/live/<your.domain>:/etc/letsencrypt/live/<your.domain>
でもこの書き方は間違いです、ダメです、バツ❌です。
nginxのコンテナは証明書を見つかりませんとかいうエラーを吐いて、一瞬で終了しちゃいます。
その原因は、実は証明書とそのキーはメッセージで表示された場所には保存していませんからです。
ls -al
で証明書が保存されるとしているフォルダの中身を確認してみると
そのフォルダの中身は証明書の本体ではなく、本体へのソフトリンクでした
上記のように docker-compose.yml
を書くと /etc/letsencrypt/aechive
はコンテナにマウントされないので、当然証明書は見つからないわけだ!
ということで、必ず
volumes:
- /etc/letsencrypt:/etc/letsencrypt
と書かないといけない。
まとめ
certbotを使えばLet's Encryptから簡単にSSLの証明書を発行することができて、自分のサイトに綺麗な🔒マークをつけさせる、、いかがでしたか?
クラウドインフラが主流の今ではhttpsなんてやってくれるところが多いのであんまり必要ないかもしれないね
眠いのでここまでにしときます。
では僕が無事卒業できることを祈って、またいつかでお会いしましょう!