TLS終端と転送元ヘッダー
暗号化される区間とHTTPを読める位置、接続から分かる値とヘッダーで申告された値を分けて説明します。
説明
TLSをロードバランサーやプロキシで終端すると、そこでHTTPを復号して解釈できます。そこから後段へHTTPで送るか、別のTLS接続で送るかは別の設定です。また、後段が見る接続元はプロキシなので、元のクライアントを示すヘッダーの信頼範囲も決める必要があります。
PythonのSSLObjectとMemoryBIOでTLS 1.2を処理し、同じHTTP要求の暗号化前後を確認します。要求にはクライアントが付けたX-Forwarded-Forを入れ、プロキシが確認した接続元203.0.113.10と比較します。後段への転送と信頼済みプロキシの判定まで進めます。
暗号化される区間とHTTPを読める位置、接続から分かる値とヘッダーで申告された値を分けて説明します。
1クライアントが送るHTTP要求
最初のHTTPバイト列を表示します。ここでのX-Forwarded-ForとX-Forwarded-Protoは、クライアントが自分で付けた値です。
from transport import REQUEST, exchange, summary, forward, headers, client_address
print(REQUEST.decode()暗号化前のHTTPバイト列を文字として読む, end="")ヘッダーは198.51.100.99とhttpを申告しています。これだけでは、実際の接続元やTLS利用を証明しません。プロキシが受けた接続の情報と照合する必要があります。
2TLSレコードの中へHTTPを入れる
service.example.testの証明書を確認するクライアントと、終端側のTLSを接続し、要求を暗号化・復号します。
front = exchange(REQUEST)TLSを確立してHTTPを暗号化・復号する
print(summary(front)TLSの版・暗号・レコード長を表示する)TLSv1.2、ECDHE-RSA-AES128-GCM-SHA256です。record_type=23はApplication Dataで、HTTP要求の平文をそのまま含むバイト列にはなっていません。この固定要求113バイトに対し、レコードの内容はGCMの付加分を含む137バイトです。
3終端側ではHTTPを読める
TLSの終端側が復号したバイト列を表示し、元の要求との一致を確認します。
print(front["plaintext"].decode()終端側で復号した要求を読む, end="")
print("same bytes", front["plaintext"] == REQUEST元のHTTPと一致することを確認する)元のHTTPと同じ内容を読めます。TLSで経路上のバイト列を保護しても、終端するプロキシはヘッダーや本文を扱えます。
4後段へHTTPで転送する場合
プロキシが確認した接続元203.0.113.10をX-Forwarded-Forの右へ追加し、受信したTLSに基づいてProtoをhttpsへ設定します。後段へのHTTPバイト列を表示します。
backend_http = forward(front["plaintext"], "append")接続情報を付加した後段用HTTPを作る
print(backend_http.decode()後段へ渡すHTTPの内容を読む, end="")X-Forwarded-Forは198.51.100.99, 203.0.113.10です。左の値はクライアントの申告、右の値はこのプロキシが見た接続元です。このバイト列をHTTPで送る区間は、フロント側のTLSによって暗号化されるわけではありません。
5後段にも独立したTLSを使う場合
後段のbackend.example.testへ、新しいTLS接続で同じ転送要求を送ります。
back = exchange(backend_http, "backend.example.test")後段への別のTLS接続を確立する
print(summary(back)後段区間のTLS情報を読む)
print("backend received same HTTP", back["plaintext"] == backend_http転送後のHTTPが届いたことを確認する)後段でもTLSによる保護があり、受信側では転送後のHTTPが復号されます。フロント側のTLSをそのまま通過させたのではなく、プロキシを境に二つの接続を持っています。
6先頭の値を無条件に信頼する問題
転送ヘッダーから左端のIPだけを取り出します。
forwarded = headers(backend_http)["X-Forwarded-For"]転送元のヘッダーを取り出す
print("leftmost", forwarded.split(",")[0].strip()先頭だけを採用する誤った処理を確認する)198.51.100.99となり、クライアントが申告した値を採用してしまいます。ヘッダーに複数の値があるとき、どのプロキシがどこを追加したかを確認する必要があります。
7信頼する直近プロキシからさかのぼる
後段の実際の接続元は10.0.0.5とし、それだけを信頼するプロキシに登録します。右端から信頼済みの中継だけを取り除きます。
print(client_address("10.0.0.5", forwarded, {"10.0.0.5"})信頼済み中継だけを右端から取り除く)203.0.113.10が残ります。この先は信頼する中継ではないため、左側の申告値まで進みません。接続元の検査と、転送ヘッダーの処理を組み合わせます。
8プロキシを経由しない接続では申告を採用しない
後段へ198.51.100.77が直接接続し、同じヘッダーを付けた場合を評価します。
print(client_address("198.51.100.77", forwarded, {"10.0.0.5"})信頼済み中継を通らない接続を評価する)198.51.100.77です。直近の接続元が信頼済みプロキシでないため、ヘッダーを使って元のIPを置き換えません。後段への直接到達を制御することも、信頼境界の設計に含まれます。
9入口で申告値を置き換える方針
単一の入口プロキシが接続元を確定する構成として、受け取ったX-Forwarded-Forを引き継がず、自分が確認した値に置き換えます。
clean = forward(REQUEST, "replace")入口で確認した接続情報へ置き換える
print(clean.decode()後段へ渡すヘッダーを確認する, end="")X-Forwarded-Forは203.0.113.10だけ、Protoはhttpsです。中継の構成に合わせて、引継ぎ・追加・置換の方針と、後段で信頼する接続元をそろえます。
まとめ
TLSを終端する位置ではHTTPを読め、後段の暗号化は別の接続として設定します。
転送ヘッダーには利用者が申告した値が混ざるため、先頭を無条件に信用しません。
直近の接続元と信頼する中継の範囲から、採用する値を決めます。
操作履歴と解説を振り返れます。
Python 3
from transport import REQUEST, exchange, summary, forward, headers, client_address
print(REQUEST.decode(), end="")GET /api/orders HTTP/1.1 Host: service.example.test X-Forwarded-For: 198.51.100.99 X-Forwarded-Proto: http
front = exchange(REQUEST)
print(summary(front)){'host': 'service.example.test', 'version': 'TLSv1.2', 'cipher': 'ECDHE-RSA-AES128-GCM-SHA256', 'record_type': 23, 'record_payload_length': 137, 'plaintext_visible': False}print(front["plaintext"].decode(), end="")
print("same bytes", front["plaintext"] == REQUEST)GET /api/orders HTTP/1.1 Host: service.example.test X-Forwarded-For: 198.51.100.99 X-Forwarded-Proto: http same bytes True
backend_http = forward(front["plaintext"], "append")
print(backend_http.decode(), end="")GET /api/orders HTTP/1.1 Host: service.example.test X-Forwarded-For: 198.51.100.99, 203.0.113.10 X-Forwarded-Proto: https
back = exchange(backend_http, "backend.example.test")
print(summary(back))
print("backend received same HTTP", back["plaintext"] == backend_http){'host': 'backend.example.test', 'version': 'TLSv1.2', 'cipher': 'ECDHE-RSA-AES128-GCM-SHA256', 'record_type': 23, 'record_payload_length': 152, 'plaintext_visible': False}
backend received same HTTP Trueforwarded = headers(backend_http)["X-Forwarded-For"]
print("leftmost", forwarded.split(",")[0].strip())leftmost 198.51.100.99
print(client_address("10.0.0.5", forwarded, {"10.0.0.5"}))203.0.113.10
print(client_address("198.51.100.77", forwarded, {"10.0.0.5"}))198.51.100.77
clean = forward(REQUEST, "replace")
print(clean.decode(), end="")GET /api/orders HTTP/1.1 Host: service.example.test X-Forwarded-For: 203.0.113.10 X-Forwarded-Proto: https
演習完了