← 前へ: Phase 3.2 | 目次 | 次へ: Phase 4.1 →
実行前に共通セットアップと各Phaseの確認手順を確認してください。演習コードはwork/で前のPhaseから引き継ぎます。
このフェーズでは、自作HTTPサーバをHTTPS(TLS)対応させます。
Phase 1ではHTTP/1.1をTCP上で平文のまま送受信しました。このフェーズでは、そのHTTP/1.1の通信をTLSで保護します。HTTPSはHTTP/2・HTTP/3でも使われますが、このTCPソケットの演習でそれらを実装するわけではありません。
本フェーズでは:
「なぜHTTPSが必要なのか」
「HTTPとHTTPSの違いは何か」
「暗号化はどこで行われているのか」
を実装を通じて理解します。
以下を説明できる状態になること:
- HTTPとHTTPSの違い
- TLSとは何か(役割レベルでOK)
- HTTPSの既定ポート443と演習用ポート3001の違い
- なぜHTTPサーバのコードをほとんど変えずにHTTPS化できるのか
- 自作HTTPサーバをHTTPS対応させる
- ブラウザまたはcurlから
https://localhost:3001/で応答を確認できる - 証明書の役割と、暗号化・接続先の認証の違いを説明できる
このフェーズで扱うのは:
- 暗号アルゴリズムの詳細 ❌
- TLSの数学的理解 ❌
ではなく:
「どのレイヤーで何が起きているか」
です。
HTTPSの既定ポートは443です。ポートを省略したhttps://localhost/は443へ接続するため、この演習のサーバには届きません。ここでは通常は管理者権限を必要としない3001を使い、URLを**https://localhost:3001/**に統一します。平文HTTPの既定ポートは80、Phase 1の演習用ポートは3000です。ポート番号だけで暗号化されるわけではなく、接続先でTLSを処理する必要があります。参照: RFC 9110 §4.2。
Phase 1で作ったサーバに対して:
- Phase 1のTCPクライアントまたはtelnetで、自分のローカルサーバに接続
- リクエスト・レスポンスを確認
- 平文のHTTP/1.1は、経路上で通信を観測できる者に内容を読まれ得る
- HTTP自体には経路上の改ざんを防ぐ仕組みがない
TLSは通信内容の機密性・完全性を保護し、証明書の検証によって接続先を認証します。TLSの端点であるクライアントやサーバ自身には、復号後のHTTPが見えます。
この演習ではwork/で、localhost用の自己署名証明書を作成します。一般にブラウザやcurlは、この証明書を初期状態では信頼しません。
生成前に証明書・秘密鍵の取扱いに従ってignore設定と追跡状態を確認します。鍵と証明書をファイル配信のルートに置かないでください。
umask 077
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
chmod 600 key.pemsubjectAltNameに接続先の名前localhostを含めます。-nodesは秘密鍵ファイルをパスフレーズで暗号化しない指定です。key.pemはローカル演習用として扱い、共有・コミットしないでください。
key.pemとcert.pemの役割を調べる
RubyのOpenSSLを使用します:
require 'socket'
require 'openssl'
tcp_server = TCPServer.new("127.0.0.1", 3001)
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read("cert.pem"))
ssl_context.key = OpenSSL::PKey::RSA.new(File.read("key.pem"))
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ssl_context)
ssl_server.start_immediately = false # ハンドシェイクをループ内で実行するStep 3と次のコードをtls_server.rbに保存し、bundle exec ruby tls_server.rbで起動します。Phase 1と同じく、ボディなしGET・1接続1リクエストだけを扱う学習用の例です。
loop do
ssl_socket = nil
begin
ssl_socket = ssl_server.accept
ssl_socket.sync_close = true
ssl_socket.accept # TLSハンドシェイク
request = ssl_socket.gets
next unless request
puts request
while (line = ssl_socket.gets)
break if line == "\r\n"
end
next unless line == "\r\n"
body = "<h1>Hello HTTPS</h1>"
response = "HTTP/1.1 200 OK\r\n" \
"Content-Type: text/html\r\n" \
"Content-Length: #{body.bytesize}\r\n" \
"Connection: close\r\n\r\n" \
"#{body}"
ssl_socket.write(response)
rescue OpenSSL::SSL::SSLError, SystemCallError => e
warn "TLS/接続エラー: #{e.message}"
ensure
ssl_socket&.close
end
endTLSで包んでもHTTP/1.1のフレーミングは必要です。ヒアドキュメントのLFだけの改行ではなく、CRLFと本文のバイト数を明示しています。証明書を信頼しないクライアントがハンドシェイクを中断した場合も、接続を閉じて次の接続を受け付けます。タイムアウトや不正リクエストへの対応は省略しているので、loopbackでの演習に限定してください。
- 自己署名証明書を信頼していないブラウザでは警告が出る
- 警告の回避可否はブラウザの方針による。表示できない場合は、後述の
curl --cacertで確認する
警告は接続先を認証できていないことを示します。自分で作ったlocalhost用の証明書であることを確認し、外部サイトの警告を無条件に回避する習慣にしないでください。
以下を説明する:
| 項目 | HTTP | HTTPS |
|---|---|---|
| 通信内容 | ? | ? |
| 既定ポートと演習用ポート | ? | ? |
| セキュリティ | ? | ? |
- なぜHTTPとHTTPSでアプリケーションコードはほぼ同じなのか?
- どの層で違いが吸収されているか?
- Rack対応サーバをTLS化する
最初に通常の検証を有効にしたまま試し、信頼していない自己署名証明書でエラーになることを観察します。
curl --http1.1 https://localhost:3001/次に、自分で生成した証明書をこのコマンドの信頼対象として指定します。Step 2のlocalhostという名前も検証されます。
curl --http1.1 --cacert cert.pem https://localhost:3001/検証を無効にした場合との比較は、ローカル演習だけで行います。
curl --http1.1 -k https://localhost:3001/-k(--insecure)はサーバ証明書の信頼性やホスト名の検証を無効にします。TLSの暗号化は続きますが、意図した接続先である保証を失い、中間者攻撃を検出できなくなります。本番・外部サービス・機密情報を送る通信で、証明書エラーを回避するために使わないでください。--cacertで信頼対象を明示する方法との違いを説明してください。参照: curlの--insecure、--cacert。
- 平文HTTP/1.1 → TCPクライアントで要求を書き、応答を読める
- HTTPS → telnetでHTTPの行を送るだけではTLSハンドシェイクができない。TLSに対応するcurlで応答を読む
- curlやブラウザが復号後の本文を表示できることと、経路上で平文が読めることの違いを説明する
- TLSで動作すること
- HTTPとHTTPSの違い
- TLSの役割
- 「なぜHTTPSが必要なのか」
- HTTPSの構造を理解しているか
- レイヤーの違いを説明できるか
- 実装と概念が結びついているか
- TLSの概念説明
- 証明書の役割の理解
- 実装の生成
→ 自己署名証明書の信頼と、接続名がlocalhostであることを確認する。上のcurl --cacertも使う
→ ポートや証明書の設定を確認
→ 「どこで暗号化されているか」を意識
このフェーズで得るべきものは:
「Webは安全に動くための仕組みの上に成り立っている」という理解
- HTTPが動く
- HTTPSの意味と構造が理解できる
このフェーズを理解すると:
- セキュリティが「設定」ではなく「構造」として見える
- なぜ本番はHTTPS必須なのか理解できる
- インフラとの境界が見えてくる
ここまで来ると、Webシステムの全体像がかなり見えてきています。
この課題では、実務で一般的に採用される構成である
- Nginx
- ロードバランサ(LB)
による「TLS終端」について考察します。
- HTTPSが「どこで処理されるか」を理解する
- アプリケーションとインフラの責務分離を理解する
- TLSをNginx/LBに集約する場合と、アプリ側でも処理する場合の選択理由を説明する
Phase 3.3で以下を理解していること:
- HTTPSはTLSで暗号化されている
- アプリケーションコードはHTTPとほぼ同じ
Client → HTTPS → App Server
Client → HTTPS → Nginx/LB → HTTP → App Server
これは構成の一例です。TLS終端とは、その地点で受信したTLS接続を処理して復号することです。次の区間の通信方式は別に選びます。
Client → HTTPS → Nginx/LB → HTTPS(必要ならmTLS)→ App Server
クライアントからのTLS接続と、Nginx/LBからアプリへのTLS接続は別です。再暗号化しても、終端点のNginx/LBは復号した内容を扱えます。
- 各パターンの違いは何か?
- どこで暗号化が解除されているか?
以下の観点で考察してください:
- TLS処理は重いか?
- どこで処理するのが効率的か?
- アプリサーバが増えた場合どうなるか?
- 証明書の管理はどうなるか?
- 秘密鍵はどこに置くべきか?
- 攻撃対象はどこに集中させるべきか?
- アプリの責務は何か?
- インフラの責務は何か?
以下を調べてまとめる:
- 一般的なRailsアプリの本番構成
- Nginx / ALB / CDN などの役割
「内部だからHTTPでよい」とは限りません。誰が通信を観測・改ざんできるか、どのホストやネットワークを信頼するかという脅威モデルと運用要件から判断します。
| 選択肢 | 選ぶ条件・確認点 |
|---|---|
| 平文HTTP | 同一ホストのloopbackや、アクセス制御・分離があり経路上の盗聴や改ざんのリスクを許容できる区間で検討する。内部の第三者や侵害ホストから到達できないか確認する |
| HTTPSで再暗号化 | 共有ネットワーク、ホスト・拠点・信頼境界をまたぐ通信、機密性や暗号化要件のある区間で使う。プロキシが上流サーバ証明書の信頼性と名前を検証する設定が必要 |
| mTLS | 暗号化に加え、呼び出すサービスの身元も証明書で認証したい場合に使う。サーバ証明書とクライアント証明書を相互に検証し、認可や証明書の配布・更新も設計する |
mTLSはTLSの相互認証です。通常のサーバ認証に加え、サーバがクライアント証明書を要求・検証します。TLSの再暗号化だけで相互認証になるわけではありません。参照: RFC 8446 §4.3.2。
たとえばNginxでは上流HTTPSへのproxy_ssl_verifyは既定でoffなので、HTTPSという指定だけで上流の証明書も検証済みだと思い込まないでください。信頼するCA、接続先名、検証の設定を確認します。参照: Nginxのproxy_ssl_verify。
- 自分の構成で、TLSが保護する区間と平文になる区間はどこか?
- 平文HTTPを選ぶなら、どのアクセス制御・分離を前提とし、何のリスクを許容するか?
- 再暗号化やmTLSを選ぶ理由と、証明書の検証・更新を担当する箇所はどこか?
以下を設計してください:
- 自作HTTPサーバの前に「TLS終端役」を置くとしたらどうするか?
- なぜ必要か?
- フォワードプロキシとの違い
- 違いは何か?
- パススルーではLBがTLSを復号せず、アプリなど後段が終端する。HTTPヘッダを使うルーティングや検査にどう影響するか?
- どちらを選ぶべきか?
TLS終端後に重要になるヘッダ:
- X-Forwarded-Proto
- X-Forwarded-For
- なぜ必要か?
以下を図解:
- TLS終端ありの構成
- 各コンポーネントの役割
以下を説明:
- TLSをどこで終端するか、その理由
- 終端後のHTTP・HTTPS・mTLSを選ぶ条件
「この構成でTLS終端の位置と内部通信方式をどう選ぶか」
- 抽象ではなく具体で説明できているか
- パフォーマンス・セキュリティ・運用の観点があるか
- 自分のサーバ実装と結びつけているか
この課題で得るべき理解は:
「Webシステムはアプリ単体ではなく、構成全体で設計される」
この課題を理解すると:
- インフラ構成がブラックボックスでなくなる
- DevOpsの議論についていけるようになる
- 「どこで何をやるべきか」が判断できるようになる
ここまで来ると、単なる実装者ではなく
システム設計に関与できるエンジニアに近づいています。