0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ALBのmTLS認証 — 仕組みと障害切り分けの基本

0
Posted at

概要

「mTLSを有効にしたのに、なぜ不正なクライアントがバックエンドまで到達しているのか」
「証明書を1枚追加しただけなのに、なぜ既存ユーザーが繋がらなくなったのか」
「認証が失敗しているはずなのに、ALBのアクセスログには何も残っていない」

これらの疑問はすべて、mTLSの「設定の仕方」ではなく「ALBが何をしているか」を知らないことから生まれます。本記事では、mTLSの基本的な仕組みと、よくある障害のパターンを解説します。


1. mTLSとは — 「サーバーがクライアントを確認する」通信

通常のHTTPS(TLS)では、クライアントが「このサーバーは本物か」をサーバー証明書で確認します。サーバーはクライアントが誰かを問いません。mTLS(Mutual TLS、相互TLS)は、これに加えて サーバーもクライアントに証明書の提示を求め、本物か確認する 仕組みです。

patterns.png

mTLSが向く場面:

  • B2B連携(取引先のシステムとAPI接続する)
  • 管理者向け画面(特定の端末・ユーザーだけ許可したい)
  • 内部マイクロサービス間の認証

IP制限は「同じIPからなら誰でも通す」、Basic認証はパスワード漏洩リスクがあります。mTLSは「その秘密鍵を持つクライアントだけ」という証明書ベースの認証なので、より強固です。

ただし mTLS は「誰が来たか(認証)」を担うのみで、「何をしたか(入力の検証)」は担いません。SQLインジェクションなどの攻撃への対策には別途WAFが必要です。


2. mTLSと他認証方式の違い

方式 セキュリティ 運用負荷
Basic認証
IP制限
VPN
mTLS 非常に高い

3. まず決める分岐 — verify と passthrough

ALBのmTLSには2つのモードがあり、どちらを選ぶかで「誰が証明書を検証するか」が変わります。

モード 検証の主体 バックエンドへの渡し方 向いている場面
mTLS verify ALB(Trust Storeで検証) 検証済み証明書のメタ情報をヘッダーで付与 ALBに認証を任せてアプリをシンプルにしたい
mTLS passthrough バックエンドアプリ(ALBは検証しない) クライアント証明書をそのままバックエンドへ渡す アプリ側で独自の認証・認可を実装したい

passthroughの大きな落とし穴: ALBは証明書チェーンを検証せず、そのままバックエンドに渡すだけです。アプリ側が証明書を検証していなければ、誰でも通ってしまいます。「ALBで弾かれているはず」は verify モードのみ の話です。

以降は、より広く使われる verify モード を前提に説明します。


4. ALBが行う証明書検証の流れ

verify モードでは、ALBはHTTPリクエストを受け付ける前にクライアント証明書の検証を行います。

flow.png

クライアントが接続 → ALBがクライアント証明書を要求
      ↓
証明書が提示されない → 接続を拒否(→ パターン①)
      ↓
Trust Store の CA と照合(発行元が信頼できるか)
→ 一致しない → 拒否(→ パターン②/④)
      ↓
有効期限チェック → 期限切れ → 設定次第で拒否(→ パターン③)
      ↓
失効チェック(CRL登録時のみ)→ 失効済み → 拒否(→ パターン⑤)
      ↓
認証成功 → ヘッダーを付与してバックエンドへ転送

重要なポイント: 証明書なし・CA不一致・期限切れ拒否・失効による失敗は、HTTPリクエストが成立する前に起きます。そのため、ALBのアクセスログには一切記録が残りません(理由は第5章)。


5. バックエンドへ渡される証明書情報

verify モードで認証に成功すると、ALBはクライアント証明書のメタ情報をヘッダーとして付与します。アプリ側はこのヘッダーを使って「どのクライアントか」を識別・認可できます。

ヘッダー 内容
X-Amzn-Mtls-Clientcert-Serial-Number 証明書のシリアル番号
X-Amzn-Mtls-Clientcert-Issuer 発行者
X-Amzn-Mtls-Clientcert-Subject サブジェクト(識別名)
X-Amzn-Mtls-Clientcert-Validity 有効期間

passthrough モードでは証明書チェーン全体が X-Amzn-Mtls-Clientcert(無印)ヘッダーに入ります。verify と passthrough でヘッダー名が異なるため、モードを混同するとアプリ側でずっと値が取れない状態になります。

※ 公式リンク Application Load Balancer での TLS による相互認証


6. なぜ「ログに何も出ない」のか

mTLS障害の切り分けで最初に理解すべき点: 失敗した「層」によって、記録が残る場所が変わります。

失敗の層 症状 記録が残る場所
証明書検証段階(HTTPより前) ブラウザにSSLエラー 接続ログClientTLSNegotiationErrorCount
アプリ層(HTTP成立後) HTTP 4XX / 5XX ALBアクセスログ、アプリログ

ALBのアクセスログは「HTTPリクエストが成立してから記録される」ため、証明書検証で弾かれた接続は記録されません。

これを検知するには 接続ログ(connection logs) を別途有効化する必要があります。接続ログにはクライアントIP・証明書情報・接続成否が残ります。mTLSを導入する際は必ず有効化しましょう。

アクセスログに何も出ていない ≠ 正常。証明書検証が大量に失敗していても、アクセスログは静かなままです。


7. よくある障害パターン

パターン① 証明書が配布されていない

  • 症状: ブラウザにSSLエラー。接続が確立しない。
  • 原因: クライアントに証明書がインストールされていない。
  • 確認: ClientTLSNegotiationErrorCount の増加、接続ログの失敗記録。

パターン② CA不一致

  • 症状: 接続が確立しない(HTTPの403ではない点に注意)。
  • 原因: 提示された証明書の発行元CAが、Trust Storeに登録されていない。
  • 確認: ClientTLSNegotiationErrorCount の増加、接続ログ。

パターン③ 証明書の期限切れ

  • 症状: 環境によって繋がる・繋がらないが食い違う。
  • 原因: verify モードには期限切れ証明書の許否設定があり、デフォルトは拒否。ignore に設定すると期限切れでも通過する。「期限切れなのに繋がる」報告があったらこの設定を確認する。
  • 確認: リスナーの期限切れ設定の確認、接続ログ。

パターン④ Trust Store更新ミス

  • 症状: 新しいCAで発行した証明書の利用者だけ失敗。または更新直後に広範囲で失敗。
  • 原因: 新CAをバンドルに含め忘れた、または「1枚だけ追加」操作で既存CAを消してしまった。
  • 確認: ClientTLSNegotiationErrorCount の増加。更新作業のタイムスタンプと症状開始時刻の相関を確認

パターン⑤ CRLによる失効

  • 症状: 有効期限内なのに特定クライアントだけが失敗。
  • 原因: CRLに登録された証明書は期限内でも拒否される。CRL更新のタイミングずれや誤登録も疑う。
  • 確認: 接続ログの失敗記録。対象クライアントの証明書シリアル番号をCRLと突き合わせる。

8. Trust Store — 運用で事故になりやすい仕様

Trust Storeとは、ALBが信頼する証明書の一覧を保存する仕組みです。

社員証の名簿のようなものと考えると分かりやすいでしょう。ALBは「この名簿に載っているCAが発行した証明書だけを信頼する」という動作をします。CA証明書をS3に置き、Trust Storeがそれを参照します。

運用上の重要な注意点:

  • CA証明書は「まとめて1ファイル」で更新する。個別追加はできない。
    証明書を1枚追加するだけでも、全CA証明書をまとめたファイルを再アップロードする必要があります。「1枚だけ追加しよう」と既存ファイルを上書きしてしまい、既存CAが消えて広範囲の接続不可になる事故が多いです(→ パターン④)。

  • 1つのリスナーに関連付けられるTrust Storeは1つのみ。1つのTrust Storeを複数リスナーで共有することは可能。

  • CRL(証明書失効リスト) を登録すると、有効期限内でも特定の証明書を拒否できます。

主なクォータ:

項目
Trust Store / アカウント 20
CA証明書 / Trust Store 25(要申請で変更可)
verify モードのリスナー / LB 2(変更不可)

最後の制限は設計上重要です。verify モードは1つのLBにつき最大2リスナーまで(申請しても変更不可)。大量のセキュアリスナーすべてに verify を付ける構成は組めません。

※ 公式リンク Application Load Balancer のクォータ


9. 切り分けの手順

ステップ1: TLS層かHTTP層かを判断する

ClientTLSNegotiationErrorCount が増加している?
  Yes → 証明書検証で失敗(パターン①〜⑤)
        → 接続ログで詳細を確認
  No  → ハンドシェイクは通過。HTTP層の問題
        → HTTPCode_ELB_4XX/5XX とアクセスログを確認

ステップ2: 接続ログで原因を絞る

  1. 証明書が提示されているか → なければパターン①
  2. CAバンドルと照合 → 辿れなければパターン②/④
  3. 有効期限 → 超過ならパターン③(許否設定も確認)
  4. シリアル番号をCRLと突合 → 一致ならパターン⑤

確認するメトリクス(namespace: AWS/ApplicationELB):

メトリクス 確認目的
ClientTLSNegotiationErrorCount 証明書検証失敗の検知。Sum で監視
HTTPCode_ELB_4XX_Count HTTP成立後のクライアントエラー
HTTPCode_ELB_5XX_Count ターゲット側のエラー
RequestCount 成立したリクエスト数

注意: ClientTLSNegotiationErrorCount はCipher不一致など他のTLSエラーも含む。増加=mTLS起因とは断定せず、接続ログで裏付けを取る。


10. 多層防御における位置付け

mTLSは万能ではなく、他の防御層と役割が異なります。

architecture.png

仕組み 担う役割
mTLS 誰が来たか(認証)
WAF 何を送ってきたか(攻撃遮断・入力検証)
IP制限 どこから来たか(ネットワーク制限)

mTLSを通過した正規クライアントが不正なペイロードを送る可能性は残るため、WAFは不要になりません。組み合わせて使うのが基本です。

推奨構成:

CloudFront → WAF → ALB(mTLS verify) → バックエンド

複数の証明書を持つクライアント環境では、どの証明書を提示すべきか判断できず接続失敗することがあります。ALBの Advertise CA subject names(認証局(CA)の件名) を有効にすると、ALBが信頼するCAの一覧をクライアントに提示し、クライアントが適切な証明書を選びやすくなります。


11. よくある誤解

誤解 実際
mTLSを設定すればWAFは不要 役割が違う。mTLS=誰か / WAF=何をしたか
ALBが必ず不正クライアントを弾いてくれる verify モードのみ。passthrough はアプリ側が責任を持つ
証明書を1枚だけ追加できる Trust Store はバンドル単位での更新。個別追加不可
アクセスログに何も出ない = 正常 証明書検証失敗はアクセスログに記録されない
有効な証明書があれば接続エラーは起きない 複数証明書環境では選択失敗あり。Advertise CA subject names で低減

12. 簡易的な検証リソース構成

構成図

Route53
  └─ ALB (mTLS verify)
        ├─ Trust Store ← S3 (CA バンドル)
        └─ Target Group
              └─ EC2 (Nginx)

HTTPS リスナー1つと EC2 ターゲット1台があれば、mTLS の基本動作を確認できます。

Terraform での環境構築

サンプルコード一式は GitHub で公開しています:
aws-alb-mtls-sample

データソース(AMI)

data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

セキュリティグループ(ALB / EC2)

# ALB: インターネットから 443 を受け付ける
resource "aws_security_group" "alb" {
  name   = "mtls-alb-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# EC2: ALB SG からの 80 のみ許可
resource "aws_security_group" "ec2" {
  name   = "mtls-ec2-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

S3 バケット / Trust Store

resource "aws_s3_bucket" "ca_bundle" {
  bucket = "mtls-ca-bundle"
}

resource "aws_s3_bucket_versioning" "ca_bundle" {
  bucket = aws_s3_bucket.ca_bundle.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_object" "ca_bundle" {
  bucket = aws_s3_bucket.ca_bundle.id
  key    = "ca-bundle.pem"
  source = var.ca_bundle_local_path
  etag   = filemd5(var.ca_bundle_local_path)

  depends_on = [aws_s3_bucket_versioning.ca_bundle]
}

resource "aws_lb_trust_store" "mtls" {
  name                             = "mtls-trust-store"
  ca_certificates_bundle_s3_bucket = aws_s3_bucket.ca_bundle.bucket
  ca_certificates_bundle_s3_key    = "ca-bundle.pem"
}

ALB / ターゲットグループ / HTTPS リスナー(mTLS 設定の核心)

resource "aws_lb" "main" {
  name               = "mtls-alb"
  load_balancer_type = "application"
  subnets            = var.public_subnet_ids
  security_groups    = [aws_security_group.alb.id]
}

resource "aws_lb_target_group" "main" {
  name     = "mtls-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    path = "/"
  }
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.certificate_arn

  mutual_authentication {
    mode            = "verify"
    trust_store_arn = aws_lb_trust_store.mtls.arn
  }

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

EC2(Nginx ターゲット)

resource "aws_instance" "target" {
  ami                    = data.aws_ami.al2023.id
  instance_type          = "t3.micro"
  subnet_id              = var.private_subnet_id
  vpc_security_group_ids = [aws_security_group.ec2.id]

  user_data = <<-EOF
    #!/bin/bash
    dnf install -y nginx
    echo "<h1>mTLS OK</h1>" > /usr/share/nginx/html/index.html
    systemctl enable --now nginx
  EOF

  tags = {
    Name = "mtls-target"
  }
}

resource "aws_lb_target_group_attachment" "target" {
  target_group_arn = aws_lb_target_group.main.arn
  target_id        = aws_instance.target.id
  port             = 80
}

使用する変数(variables.tf で定義):

変数 説明
vpc_id 検証用 VPC の ID
public_subnet_ids ALB を配置するパブリックサブネット(2AZ 以上)
private_subnet_id EC2 を配置するプライベートサブネット
certificate_arn ACM に登録済みのサーバー証明書 ARN(下記「サーバー証明書の作成と ACM へのインポート」で取得)
ca_bundle_local_path CA バンドル PEM のローカルパス(デフォルト: ../key/ca-bundle.pem

証明書の準備(OpenSSL)

検証には本物の証明書は不要です。OpenSSL で自作した証明書を使います。

作業順序:

  1. サーバー証明書を作成して ACM にインポート → ARN を取得
  2. terraform.tfvars に ARN を設定
  3. Root CA・クライアント証明書を作成
  4. CA バンドルファイルをローカルに準備(cp ca.crt ca-bundle.pem
  5. terraform apply(S3 バケット作成 → CA バンドルのアップロード → Trust Store 作成を自動実行)

サーバー証明書の作成と ACM へのインポート

ALB の HTTPS リスナーに使うサーバー証明書です。terraform apply の前に ACM へ登録し、出力された ARN を terraform.tfvars に設定します。

# サーバー秘密鍵
openssl genrsa -out server.key 2048

# 自己署名サーバー証明書(1年有効)
openssl req -x509 -new -nodes -key server.key -sha256 -days 365 \
  -subj "/CN=mtls-sample.example.com" \
  -out server.crt

# ACM にインポート
aws acm import-certificate \
  --certificate fileb://server.crt \
  --private-key fileb://server.key \
  --region ap-northeast-1

コマンド実行後、次の形式で ARN が出力されます。

{
    "CertificateArn": "arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

この ARN を terraform.tfvarscertificate_arn に設定してから次の手順に進みます。

注意: 自己署名証明書を使う場合、ブラウザや curl はサーバー証明書の検証でエラーを出します。検証手順では -k / --insecure フラグを付けて実行してください。

Root CA の作成

# CA 秘密鍵
openssl genrsa -out ca.key 2048

# CA 証明書(自己署名・10年有効)
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/CN=Test Root CA" \
  -out ca.crt

クライアント証明書の作成(CA 署名)

# クライアント秘密鍵
openssl genrsa -out client.key 2048

# CSR の生成
openssl req -new -key client.key \
  -subj "/CN=test-client" \
  -out client.csr

# CA で署名(1年有効)
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 365 -sha256 \
  -out client.crt

CA 不一致テスト用の証明書(パターン②の再現)

# 別の CA(Trust Store に登録しない)
openssl genrsa -out other-ca.key 2048
openssl req -x509 -new -nodes -key other-ca.key -sha256 -days 3650 \
  -subj "/CN=Other Root CA" \
  -out other-ca.crt

# 別 CA で署名したクライアント証明書
openssl genrsa -out other-ca-client.key 2048
openssl req -new -key other-ca-client.key \
  -subj "/CN=other-client" \
  -out other-ca-client.csr
openssl x509 -req -in other-ca-client.csr -CA other-ca.crt -CAkey other-ca.key \
  -CAcreateserial -days 365 -sha256 \
  -out other-ca-client.crt

Trust Store 用の CA バンドルを準備する

# バンドルファイル作成(CA が複数ある場合は cat で連結)
cp ca.crt ca-bundle.pem

S3 へのアップロードと Trust Store 作成は terraform apply が自動的に行います(aws_s3_object リソースが ca_bundle_local_path 変数のパスからファイルを読み込んでアップロードします)。

other-ca.crt は Trust Store に含めません。これがパターン②(CA不一致)の再現に使います。

検証手順

まず接続ログを有効化した上で、以下の3パターンを順に試します。

① 証明書あり(成功を確認)

curl -k --cert client.crt --key client.key https://your-alb.example.com/
# → 200 OK。X-Amzn-Mtls-Clientcert-* ヘッダーがバックエンドに届くか確認
返却例

image.png

② 証明書なし(パターン①の再現)

curl -k https://your-alb.example.com/
# → SSL エラー。アクセスログには出ず、接続ログと ClientTLSNegotiationErrorCount に記録される
返却例

image.png

③ CA 不一致(パターン②の再現)

curl -k --cert other-ca-client.crt --key other-ca-client.key https://your-alb.example.com/
# → SSL エラー。Trust Store に登録されていない CA の証明書を提示した場合
返却例

image.png

総括

3パターンを通して「どのログ・メトリクスにどう出るか」を体で確認しておくと、本番障害時の切り分けが速くなります。


まとめ

ALBのmTLSは、「特定の証明書を持つクライアントだけを許可する」ための認証機能です。

まずは verify モードから始めるのがおすすめです。

接続できない場合は、

  1. ClientTLSNegotiationErrorCount
  2. 接続ログ
  3. Trust Store

の順で確認すると原因を特定しやすくなります。

検証環境で

  • 証明書あり
  • 証明書なし
  • CA不一致

を再現しておくと、本番障害時の切り分けが大幅に楽になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?