Skip to content

Latest commit

 

History

History
590 lines (343 loc) · 16.5 KB

phase-3-3.md

File metadata and controls

590 lines (343 loc) · 16.5 KB

Phase 3.3: HTTPS対応(TLSの導入)演習

← 前へ: 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の数学的理解 ❌

ではなく:

「どのレイヤーで何が起きているか」

です。


URLとポート

HTTPSの既定ポートは443です。ポートを省略したhttps://localhost/は443へ接続するため、この演習のサーバには届きません。ここでは通常は管理者権限を必要としない3001を使い、URLを**https://localhost:3001/**に統一します。平文HTTPの既定ポートは80、Phase 1の演習用ポートは3000です。ポート番号だけで暗号化されるわけではなく、接続先でTLSを処理する必要があります。参照: RFC 9110 §4.2。

演習課題


Step 1: HTTPの限界を理解する

課題

Phase 1で作ったサーバに対して:

  • Phase 1のTCPクライアントまたはtelnetで、自分のローカルサーバに接続
  • リクエスト・レスポンスを確認

気づき

  • 平文のHTTP/1.1は、経路上で通信を観測できる者に内容を読まれ得る
  • HTTP自体には経路上の改ざんを防ぐ仕組みがない

TLSは通信内容の機密性・完全性を保護し、証明書の検証によって接続先を認証します。TLSの端点であるクライアントやサーバ自身には、復号後のHTTPが見えます。


Step 2: OpenSSLで証明書を作る

この演習では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.pem

subjectAltNameに接続先の名前localhostを含めます。-nodesは秘密鍵ファイルをパスフレーズで暗号化しない指定です。key.pemはローカル演習用として扱い、共有・コミットしないでください。


課題

  • key.pem と cert.pem の役割を調べる

Step 3: TLSソケットを使う

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 4: HTTPSサーバを起動する

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
end

TLSで包んでもHTTP/1.1のフレーミングは必要です。ヒアドキュメントのLFだけの改行ではなく、CRLFと本文のバイト数を明示しています。証明書を信頼しないクライアントがハンドシェイクを中断した場合も、接続を閉じて次の接続を受け付けます。タイムアウトや不正リクエストへの対応は省略しているので、loopbackでの演習に限定してください。


Step 5: ブラウザからアクセス

https://localhost:3001/


観察

  • 自己署名証明書を信頼していないブラウザでは警告が出る
  • 警告の回避可否はブラウザの方針による。表示できない場合は、後述のcurl --cacertで確認する

警告は接続先を認証できていないことを示します。自分で作ったlocalhost用の証明書であることを確認し、外部サイトの警告を無条件に回避する習慣にしないでください。


Step 6: HTTPとの違いを整理する

課題

以下を説明する:

項目 HTTP HTTPS
通信内容 ? ?
既定ポートと演習用ポート ? ?
セキュリティ ? ?

Step 7: なぜコードがほぼ同じか考える

課題

  • なぜHTTPとHTTPSでアプリケーションコードはほぼ同じなのか?
  • どの層で違いが吸収されているか?

発展課題


課題1: 自作RackサーバをHTTPS対応

  • Rack対応サーバをTLS化する

課題2: curlで確認

最初に通常の検証を有効にしたまま試し、信頼していない自己署名証明書でエラーになることを観察します。

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。


課題3: 通信内容を比較する

  • 平文HTTP/1.1 → TCPクライアントで要求を書き、応答を読める
  • HTTPS → telnetでHTTPの行を送るだけではTLSハンドシェイクができない。TLSに対応するcurlで応答を読む
  • curlやブラウザが復号後の本文を表示できることと、経路上で平文が読めることの違いを説明する

成果物


1. HTTPS対応サーバ

  • TLSで動作すること

2. 説明資料

  • HTTPとHTTPSの違い
  • TLSの役割

3. 気づき

  • 「なぜHTTPSが必要なのか」

レビュー観点

  • HTTPSの構造を理解しているか
  • レイヤーの違いを説明できるか
  • 実装と概念が結びついているか

AI活用

OK

  • TLSの概念説明
  • 証明書の役割の理解

NG

  • 実装の生成

よくあるつまずき


1. ブラウザで警告が出る

→ 自己署名証明書の信頼と、接続名がlocalhostであることを確認する。上のcurl --cacertも使う


2. 接続できない

→ ポートや証明書の設定を確認


3. HTTPと違いが分からない

→ 「どこで暗号化されているか」を意識


このフェーズの本質

このフェーズで得るべきものは:

「Webは安全に動くための仕組みの上に成り立っている」という理解


Before / After

Before

  • HTTPが動く

After

  • HTTPSの意味と構造が理解できる

最後に

このフェーズを理解すると:

  • セキュリティが「設定」ではなく「構造」として見える
  • なぜ本番はHTTPS必須なのか理解できる
  • インフラとの境界が見えてくる

ここまで来ると、Webシステムの全体像がかなり見えてきています。

発展課題: 本番のTLS終端位置と内部通信をどう選ぶか

この課題では、実務で一般的に採用される構成である

  • Nginx
  • ロードバランサ(LB)

による「TLS終端」について考察します。


この課題の目的

  • HTTPSが「どこで処理されるか」を理解する
  • アプリケーションとインフラの責務分離を理解する
  • TLSをNginx/LBに集約する場合と、アプリ側でも処理する場合の選択理由を説明する

前提知識(確認)

Phase 3.3で以下を理解していること:

  • HTTPSはTLSで暗号化されている
  • アプリケーションコードはHTTPとほぼ同じ

課題1: 構成の違いを理解する

パターンA: アプリがTLSを処理

Client → HTTPS → App Server

パターンB: TLS終端後は平文HTTP

Client → HTTPS → Nginx/LB → HTTP → App Server

これは構成の一例です。TLS終端とは、その地点で受信したTLS接続を処理して復号することです。次の区間の通信方式は別に選びます。

パターンC: TLS終端後に再暗号化

Client → HTTPS → Nginx/LB → HTTPS(必要ならmTLS)→ App Server

クライアントからのTLS接続と、Nginx/LBからアプリへのTLS接続は別です。再暗号化しても、終端点のNginx/LBは復号した内容を扱えます。


問い

  • 各パターンの違いは何か?
  • どこで暗号化が解除されているか?

課題2: TLSの処理をどこに配置するか

以下の観点で考察してください:


観点1: パフォーマンス

  • TLS処理は重いか?
  • どこで処理するのが効率的か?

観点2: スケーラビリティ

  • アプリサーバが増えた場合どうなるか?
  • 証明書の管理はどうなるか?

観点3: セキュリティ

  • 秘密鍵はどこに置くべきか?
  • 攻撃対象はどこに集中させるべきか?

観点4: 責務分離

  • アプリの責務は何か?
  • インフラの責務は何か?

課題3: 実際の構成を調べる

調査

以下を調べてまとめる:

  • 一般的なRailsアプリの本番構成
  • Nginx / ALB / CDN などの役割

課題4: TLS終端後の通信方式を選ぶ

「内部だから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を選ぶ理由と、証明書の検証・更新を担当する箇所はどこか?

課題5: 自分のサーバで考える

課題

以下を設計してください:

  • 自作HTTPサーバの前に「TLS終端役」を置くとしたらどうするか?

深掘り課題(任意)


課題1: リバースプロキシとは何か

  • なぜ必要か?
  • フォワードプロキシとの違い

課題2: TLS終端 vs TLSパススルー

  • 違いは何か?
  • パススルーではLBがTLSを復号せず、アプリなど後段が終端する。HTTPヘッダを使うルーティングや検査にどう影響するか?
  • どちらを選ぶべきか?

課題3: ヘッダの扱い

TLS終端後に重要になるヘッダ:

  • X-Forwarded-Proto
  • X-Forwarded-For

問い

  • なぜ必要か?

成果物


1. 構成図

以下を図解:

  • TLS終端ありの構成
  • 各コンポーネントの役割

2. レポート

以下を説明:

  • TLSをどこで終端するか、その理由
  • 終端後のHTTP・HTTPS・mTLSを選ぶ条件

3. 自分の言葉での説明

「この構成でTLS終端の位置と内部通信方式をどう選ぶか」


レビュー観点

  • 抽象ではなく具体で説明できているか
  • パフォーマンス・セキュリティ・運用の観点があるか
  • 自分のサーバ実装と結びつけているか

この課題の本質

この課題で得るべき理解は:

「Webシステムはアプリ単体ではなく、構成全体で設計される」


最後に

この課題を理解すると:

  • インフラ構成がブラックボックスでなくなる
  • DevOpsの議論についていけるようになる
  • 「どこで何をやるべきか」が判断できるようになる

ここまで来ると、単なる実装者ではなく
システム設計に関与できるエンジニアに近づいています。


← 前へ: Phase 3.2 | 目次 | 次へ: Phase 4.1 →