一覧CL-08-2 · ロードバランサーと配信

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

1
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

2
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}
3
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
4
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

5
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 True
6
forwarded = headers(backend_http)["X-Forwarded-For"] print("leftmost", forwarded.split(",")[0].strip())
leftmost 198.51.100.99
7
print(client_address("10.0.0.5", forwarded, {"10.0.0.5"}))
203.0.113.10
8
print(client_address("198.51.100.77", forwarded, {"10.0.0.5"}))
198.51.100.77
9
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

演習完了