ALBはバックエンド証明書を検証しない、が公式仕様だった

  • AWS
  • ALB
  • ECS
  • TLS

この記事は、構成と文章の整理に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リクエストを送った時に出るやつで、原因は単純にアクセス方法の間違い。

ターミナル
# NG
curl http://localhost:8881
# OK
curl -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ネゴシエーションポリシーが最近設定可能になっているらしいので、そのあたりの挙動

このへんはまだ手を動かしていないので、実機で確認してから追記する。