ALBのmTLS後に付与されたHTTPヘッダーをリスナールールの条件判定に使えるかやってみた

ALBのmTLS後に付与されたHTTPヘッダーをリスナールールの条件判定に使えるかやってみた

Clock Icon2024.11.03

はじめに

ALBのmTLS機能で「トラストストアを使った検証」を行う場合、認証後にALBによって付与されるHTTPヘッダーをリスナールールの条件として利用できるのか検証してみました。

字面だけだとイメージしづらいので、以下の図のような流れになるのか?ということを検証しました。
Frame 1

先にまとめ

ALBのmTLS後に付与されるHTTPヘッダーをリスナールールの条件に利用することはできませんでした。
実際は以下の流れでした。
Frame 2

つまり、HTTPヘッダーを使った処理を行いたい場合は、ALBのターゲット側で受け取ったHTTPヘッダーを処理する仕組みを作る必要があります。

何がしたかったのか?

私がやりたかったのは、
ALBでmTLSを行いつつ、特定のCommon Name(以下CN)を持つクライアント証明書のみ、特定のターゲットにルーティングする。ということが実現できるのか知りたくて検証を行いました。

結論は、ALBだけでは上記のやりたいことをすべてカバーすることはできませんでした。
以下に検証したことを書いていきます。

事前準備

前提として、各種証明書、クライアント、mTLSを有効化したALBを用意しておきます。
具体的な方法は以下のブログを参考に作成しました。
https://dev.classmethod.jp/articles/mutual-authentication-for-application-load-balancer-trust-store/

クライアント証明書の発行時に聞かれるCommon Nameには clientA と入力しておきます。

Common Name (eg, your name or your server's hostname) []:clientA

以下は作成したクライアント証明書です。

$ openssl x509 -text -noout -in client_cert.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            6d:d0:cf:55:aa:53:13:fc:c7:3d:47:7d:55:3c:34:17:48:08:b8:be
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = JP, ST = Osaka, L = Default City, O = example.org, OU = example.org.unit, CN = privateCA
        Validity
            Not Before: Nov  3 04:09:55 2024 GMT
            Not After : Dec  3 04:09:55 2024 GMT
        Subject: C = JP, ST = Osaka, L = Default City, O = example.org, OU = example.org.unit, CN = clientA
        Subject Public Key Info:
        ...

今回は確認のみなので、最初の図のようにEC2は配置せずに固定レスポンスを返します。

mTLSによる認証ができることを確認

上記のブログを参考に、まずはmTLSで認証が通ることを確認しておきます。
クライアントからALBに対してリクエストを送ります。
問題なく認証が通ると Default Action を返します。

$ curl https://hogehoge.com --key client_key.pem --cert client_cert.pem -v

* Host hogehoge.com:443 was resolved.
* IPv6: (none)
* IPv4: 18.178.46.15, 35.76.60.97
*   Trying 18.178.46.15:443...
* Connected to hogehoge.com (18.178.46.15) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / prime256v1 / rsaEncryption
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=hogehoge.com
*  start date: Nov  3 00:00:00 2024 GMT
*  expire date: Dec  3 23:59:59 2025 GMT
*  subjectAltName: host "hogehoge.com" matched cert's "hogehoge.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://hogehoge.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: hogehoge.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: hogehoge.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< server: awselb/2.0
< date: Sun, 03 Nov 2024 04:36:23 GMT
< content-type: text/plain; charset=utf-8
< content-length: 14
<
* Connection #0 to host hogehoge.com left intact
Default Action

mTLSによる認証が通っていることが確認できました。
ちなみに、証明書を設定せずにリクエストすると以下のように認証が失敗します。

$ curl https://hogehoge.com -v

* Host hogehoge.com:443 was resolved.
* IPv6: (none)
* IPv4: 35.76.60.97, 18.178.46.15
*   Trying 35.76.60.97:443...
* Connected to hogehoge.com (35.76.60.97) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* Recv failure: Connection reset by peer
* OpenSSL SSL_connect: Connection reset by peer in connection to hogehoge.com:443
* Closing connection
curl: (35) Recv failure: Connection reset by peer

では、mTLS機能の確認はできたので、ここからさらにリスナールールを追加していきます。

リスナールールの追加

ドキュメントではmTLSにより付与されるヘッダーについて、以下のように書かれています。

相互TLS検証モードでは、Application Load Balancer は次のヘッダーを使用します。
X-Amzn-Mtls-Clientcert-Subject
このヘッダーには、被写体の識別名 (DN) のRFC2253文字列表現が含まれています。
ヘッダーの内容の例:
X-Amzn-Mtls-Clientcert-Subject: CN=client_.com,OU=client-3,O=mTLS,ST=Washington,C=US

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/mutual-authentication.html

つまり、CNの値を条件にルーティングさせるには以下のような設定が必要になりそうです。
HTTPヘッダーX-Amzn-Mtls-Clientcert-Subject*CN=clientA* が含まれている場合、固定レスポンスを返す。

以下のドキュメントを参考に、値の部分は*(ワイルドカード)が使えるようなので、*CN=clientA* として、CN=clientAが含まれていることを条件に設定しています。
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-listeners.html#:~:text=てください。-,HTTP ヘッダー条件,-HTTP ヘッダー条件

それではリスナールールを作成していきます。
今回はcliでルールを追加します。

aws elbv2 create-rule \
  --listener-arn <リスナールールのARN> \
  --priority 100 \
  --conditions '[
    {
      "Field": "http-header",
      "HttpHeaderConfig": {
        "HttpHeaderName": "X-Amzn-Mtls-Clientcert-Subject",
        "Values": ["*CN=clientA*"]
      }
    }
  ]' \
  --actions '[
    {
      "Type": "fixed-response",
      "FixedResponseConfig": {
        "ContentType": "text/plain",
        "StatusCode": "200",
        "MessageBody": "Success!!"
      }
    }
  ]'

コンソール画面からルールが追加されていることを確認します。
CleanShot 2024-11-03 at 12.46.34@2x

準備が整ったので、クライアントからアクセスしてみましょう!
最後のレスポンスが Success!! であれば成功です。

$ curl https://hogehoge.com --key client_key.pem --cert client_cert.pem -v

* Host hogehoge.com:443 was resolved.
* IPv6: (none)
* IPv4: 35.76.60.97, 18.178.46.15
*   Trying 35.76.60.97:443...
* Connected to hogehoge.com (35.76.60.97) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / prime256v1 / rsaEncryption
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=hogehoge.com
*  start date: Nov  3 00:00:00 2024 GMT
*  expire date: Dec  3 23:59:59 2025 GMT
*  subjectAltName: host "hogehoge.com" matched cert's "hogehoge.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://hogehoge.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: hogehoge.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: hogehoge.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< server: awselb/2.0
< date: Sun, 03 Nov 2024 04:42:47 GMT
< content-type: text/plain; charset=utf-8
< content-length: 14
<
* Connection #0 to host hogehoge.com left intact
Default Action

レスポンスは Default Action ですね。
つまりmTLSは成功しているけど、その後のHTTPヘッダーの検証は失敗していることが分かります。
これでALBのmTLS後に付与されたHTTPヘッダーはリスナールールの条件としては利用できないことが分りました。

ALB前後のHTTPヘッダーの確認

念の為、ALB前後でHTTPヘッダーがどのように変化しているのか確認してみます。
ALBにAWS WAFを、ALBのターゲットにHTTPヘッダーを確認するLambdaを配置します。
以下は構成のイメージです。
Frame 3 (1)

まずはALBにアタッチしたWAFのログから、どのようなHTTPヘッダーが送られて来ているのか見てみます。

{
    "timestamp": 1730608283690,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:ap-northeast-1:1234567890:regional/webacl/mtls-alb/b31d72ff-19ae-406e-bc46-22afb8043ff4",
    "terminatingRuleId": "Default_Action",
    "terminatingRuleType": "REGULAR",
    "action": "ALLOW",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "ALB",
    "httpSourceId": "1234567890-app/mtls-alb/d01c2a8dc18aeefb",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "35.203.211.31",
        "country": "GB",
        "headers": [
            {
                "name": "Host",
                "value": "35.76.60.97"
            },
            {
                "name": "User-Agent",
                "value": "Expanse, a Palo Alto Networks company, searches across the global IPv4 space multiple times per day to identify customers&#39; presences on the Internet. If you would like to be excluded from our scans, please send IP addresses/domains to: [email protected]"
            },
            {
                "name": "Accept-Encoding",
                "value": "gzip"
            }
        ],
        "uri": "/",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "1-6726fc9b-0cf63c9f212964f5681c2181"
    },
    "ja3Fingerprint": "19e29534fd49dd27d09234e639c4057e"
}

当然ですが、HTTPヘッダーには X-Amzn-Mtls-Clientcert-Subject のヘッダーはありませんね。

では、ターゲットのLambdaでHTTPヘッダーを確認してみます。
以下のコードでLambda関数を作成して、HTTPヘッダーをCloudWatch Logsに出力します。

import json
import logging

# Loggerの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    # リクエストヘッダーを取得
    headers = event.get('headers', {})

    # ヘッダーを整形
    formatted_headers = json.dumps(headers, indent=2)

    # すべての情報を1つのログエントリにまとめる
    log_message = f"Received HTTP Headers:\n{formatted_headers}"
    logger.info(log_message)

    # レスポンスを返す
    return {
        'statusCode': 200,
        'body': json.dumps('Headers logged to CloudWatch Logs')
    }

作成した関数をALBのターゲットに登録して再度クライアントからアクセスします。

{
    "accept": "*/*",
    "host": "hogehoge.com",
    "user-agent": "curl/8.5.0",
    "x-amzn-mtls-clientcert-leaf": "-----BEGIN%20CERTIFICATE-----%...-----%0A",
    "x-amzn-mtls-clientcert-serial-number": "6DD0CF55AA5313FCC73D477D553C34174808B8BE",
    "x-amzn-mtls-clientcert-subject": "CN=clientA,OU=example.org.unit,O=example.org,L=Default City,ST=Osaka,C=JP",
    "x-amzn-mtls-clientcert-validity": "NotBefore=2024-11-03T04:09:55Z;NotAfter=2024-12-03T04:09:55Z",
    "x-amzn-trace-id": "Root=1-672702e9-3f32b8590ccb1cf0424cc22c",
    "x-forwarded-for": "43.207.149.150",
    "x-forwarded-port": "443",
    "x-forwarded-proto": "https"
}

ALBのターゲットのLambdaには x-amzn-mtls-clientcert-subject が付与されていることが確認できます。

つまり、ALBでの認証後にヘッダーは付与されているが、リスナールールの条件を検討する時点ではヘッダーは付与されていない。ということが分かりました。
図にするとこうですね。(再掲)
Frame 2

要件を実現するために試したこと

  1. ALBの背後にALBを配置する

ALBのターゲットにALBを設定することはできませんでした。
NLB→ALBは可能ですが、ALB→ALBはできないようです。

今回のようなケースは稀だとは思いますが、ALB→ALBが実装できれば
前段のALBでmTLSを行いHTTPヘッダーの付与、後段のALBでリスナールールによるルーティング
が実現できるので期待しましたが、できませんでした。

  1. ALBで別のALBにリダイレクトする

これは1.を無理やり実装しようとしました。
しかし、リダイレクトは転送ではなく、クライアントがアクセスするURL(今回は別のALBのURL)を発行するだけなので、HTTPヘッダーを付与するということはできませんでした。

結局のところやりたい要件を実現するには、ALBのターゲットにNginxのようなWebサーバーを配置してルーティングする構成を組む必要がありそうです。
もしくは、プライベートCAをクライアント証明書ごとに作成できる場合はクラアイント証明書の種類とプライベートCAを1:1にすることが最も管理が少ない構成になります。

まとめ

以下の記載がAWSブログに書かれていたので、恐らく付与されるHTTPヘッダーはリスナールールの条件としては使えないだろうな〜。と思って検証してみましたが、やはり使えませんでした。

クライアント認証を実行するほか、ALB は次の証明書メタデータをバックエンドターゲットに送信します。

AWSブログ: https://aws.amazon.com/jp/blogs/news/introducing-mtls-for-application-load-balancer/

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.