

ALBはバックエンド証明書を検証しない、が公式仕様だった
この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。
ALB + ECS Fargateの構成で、ゼロトラストの考え方に寄せるならVPC内通信も暗号化した方がいいだろう、というところから始まった話。ALBとコンテナの間もHTTPSにできないか調べた。
結論
自己署名証明書でALB↔ECS間のHTTPSはそのまま動く。ALBはターゲット(バックエンド)の証明書のチェーンを検証していないから。
これは体感や伝聞じゃなくて、AWSの公式ドキュメント(load balancer target groupsのRouting configuration周り)に書いてある。ALBはターゲットにインストールされた証明書でTLS接続を確立するが、その証明書自体は検証しない、と明記されている。だから自己署名証明書でも期限切れ証明書でも、TLSハンドシェイクは成立してしまう。
全体構成
最終的に落ち着いたのはこの形。
flowchart LR
Client -->|HTTPS| ALB
ALB -->|HTTPS| Nginx["Nginx (Fargateコンテナ, TLS終端)"]
Nginx -->|HTTP| App["Node.js / AdonisJS (localhost:3000)"]
nginxをFargateコンテナの手前に置いてTLS終端させ、アプリ本体とはHTTPで喋らせる構成。ALB側はHTTPSリスナー・HTTPSターゲットグループにする。
ただし、アプリがNode.js(AdonisJS)なら、本来nginxを挟む必然性は薄い。TLS終端をアプリ側でやるか、ALBで終端してバックエンドはHTTPのままにするか、どちらにしても ALB → Node.js 直結で足りるケースが多い。要否の判断軸は ALB配下のEC2でnginxって要る?フレームワーク別に見てみた にまとめた。この記事は「当時すでにnginx+Node.jsの2コンテナになっていた構成で、ALB↔バックエンドをHTTPS化する」前提のメモとして読んでほしい。nginx+Node.jsをECS Fargateで続ける場合のCPU/メモリ配分は ECS FargateでnginxとNode.js動かすなら、まずtask cpu 512 / memory 1024でよさそう を参照。
CNをlocalhostにするかどうか
技術的には動きそうだが、推奨はしない。内部向けのホスト名を決めて使う方が後々分かりやすい。今回はadministrators-web-app.internalのような名前にした(実際のプロジェクトでは案件名に応じた名前に変える)。
証明書に入れる項目は以下で足りる。
- CN:
administrators-web-app.internal - SAN:
DNS:administrators-web-app.internal
O(Organization)、OU、L、ST、Cは空でも問題ない。
20年有効な自己署名証明書を作る
社内向けの検証用途なので、有効期限は長めに取った。
openssl req -x509 -nodes -newkey rsa:2048 \ -keyout server.key \ -out server.crt \ -days 7300 \ -subj "/CN=administrators-web-app.internal" \ -addext "subjectAltName=DNS:administrators-web-app.internal"-days 7300で約20年。server.keyはパスフレーズなし、server.crtが20年有効の証明書になる。
nginx側の設定
TLS終端はnginxにやらせて、アプリへは素のHTTPでプロキシする。
server { listen 443 ssl; server_name administrators-web-app.internal;
ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
location / { proxy_pass http://localhost:3000;
proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}ALB側はターゲットグループのプロトコルをHTTPS、ポート443にしておけばTLS1.2/1.3もそのまま使える。
ローカルでDockerに立てて確認した
docker run -d \ --name administrators-web-nginx-c \ --network temp-admin-app-net \ -p 8881:443 \ administrators-web-nginx最初、うっかりHTTPでアクセスして
The plain HTTP request was sent to HTTPS portというエラーが出た。これはHTTPSで待ち受けてるポートにHTTPリクエストを送った時に出るやつで、原因は単純にアクセス方法の間違い。
# NGcurl http://localhost:8881
# OKcurl -k https://localhost:8881-k(証明書検証スキップ)付きのhttpsでアクセスしたら普通に疎通した。ブラウザならhttps://localhost:8881にアクセスすればいい。
通信フローのおさらい
エラーが出た時と、正常に疎通した時の違いを図にするとこうなる。
sequenceDiagram
participant C as クライアント
participant N as Nginx (443, TLS終端)
C->>N: curl http://localhost:8881 (NG)
N-->>C: The plain HTTP request was sent to HTTPS port
C->>N: curl -k https://localhost:8881 (OK)
N-->>C: 200 OK (TLSハンドシェイク成立)
今後確認したいこと
- ALBのヘルスチェックが自己署名証明書でも正常に動くか
- ALB→nginx間で実際にTLS1.3がネゴシエートされているか(nginxのアクセスログに
$ssl_protocolと$ssl_cipherを出せば確認できるはず) - ECS/Fargateのタスク定義に証明書をどう組み込むか
- Secrets Managerでの証明書管理、ローテーションの運用設計
- ALBのバックエンド向けTLSネゴシエーションポリシーが最近設定可能になっているらしいので、そのあたりの挙動
このへんはまだ手を動かしていないので、実機で確認してから追記する。