クラウド境界で VPN クライアントの IP を保持するか SNAT するか
送信元を保持すればすべてのログでクライアントの身元が保たれ、SNAT ならルーティングテーブルを小さく保てます。どちらが必要かは三つの条件で決まります。
VPN クライアントのアドレスをアプリケーションまで保持すると、すべてのログ行とすべての許可リストのエントリが同じ意味を持つようになります。クラウドの境界で書き換えると、ルーティングテーブルは単純になり、ログは役に立たなくなります。どちらの選択もそれ自体が誤りではなく、それを決める論点が性能であることはほとんどありません。決め手は三つの条件で、どの人間が接続したかをアプリケーションが知る必要があるか、送ろうとしている送信元をクラウドファブリックが受け入れるか、そして後で戻り経路の責任を誰が引き受けるかです。
三つの条件が決める
ネットワーク図ではなく、アプリケーションの要件から始めます。認可や監査が個々のクライアントアドレスに依存するなら、それを保持することは機能要件であり、変換は後退です。アプリケーションがリモートアクセスの経路からトラフィックが届いたことだけを知ればよいなら、変換されたアドレスで十分であり、運用ははるかに安く済みます。
二つ目の条件は機械的です。仮想ネットワークは、送信インターフェースに割り当てられていないアドレスを主張するパケットを自動的に信頼しません。ほとんどのクラウドファブリックはそれをさまざまな方法と名前で強制しており、これを無視した設計は図の上では正しく見え、本番では静かに落ちます。
三つ目は組織的な条件です。送信元を保持するとは、アプリケーションサブネットのルートテーブルがクライアントのプレフィックスについてゲートウェイを指し返す必要があり、後からサブネットを追加するすべてのチームがその経路を一貫させ続ける必要があるということです。これは一度きりの小さなコストであり、数十のアカウントにわたる繰り返しのコストです。その行動を約束する前に、誰が払うのかを決めてください。
経路はなりすましの許可ではない
戻り経路を追加しても、応答がどこへ行くべきかがサブネットに伝わるだけで、そのパケットを転送するインスタンスが外部の送信元で送ってよいとファブリックに伝えるわけではありません。AWS では該当する制御は送信元と送信先のチェックであり、インスタンスごとに確認できます。
aws ec2 describe-instances --instance-ids i-0a1b2c3d4e5f60718 \
--query "Reservations[].Instances[].SourceDestCheck"
aws ec2 modify-instance-attribute --instance-id i-0a1b2c3d4e5f60718 \
--no-source-dest-check
それを無効にするのは、転送を文書化された役割とするインスタンスだけにし、他の何にも行わないでください。チェックを有効のままにするアプリケーション VM は、借り物の送信元でトラフィックを送れる機械が一つ減るということです。プラットフォームが AWS でない場合は、同じ挙動が引き継がれると仮定せず、同等のアンチスプーフィングや許可アドレスの設定を探してください。
短時間のパケットキャプチャは、コンソールを読むより速く結論を出します。応答が出ていってクライアントに一度も届かないなら、失敗しているのはアプリケーションではなく、ファブリックが偽装された送信元を捨てているのです。
変換が買うものと、その代償
ゲートウェイで送信元を書き換えると、戻り経路の問題は消えます。アプリケーションは到達方法をすでに知っている一つのアドレスを見るだけで、クライアントのプレフィックスがクラウドのルートテーブルに現れる必要はなく、Linux ではルールは一行なので、書ける範囲で最も狭い版を書く価値があります。
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 172.20.8.0/24 -o ens5 -j SNAT --to-source 10.40.3.10
アプリケーションは、すべての利用者について、すべての接続で、永遠に 10.40.3.10 を記録するようになります。その値に基づいて作られたアドレスの許可リストは、いまやリモートアクセス人口全体にアクセスを与えており、インシデントのレビューは、これは誰だったのかに答えるためにゲートウェイへ戻らなければなりません。
解決策は変換を捨てることではなく、アドレスが身元であるというふりをやめることです。クライアントの身元を認証済みのフィールドで運び、変換されたタプルとともにゲートウェイで記録し、アプリケーションがそれで結合できるようにします。結合キーがなければ、変換はネットワークの問題を恒久的なフォレンジックの問題に変えてしまいます。
リレーは別のインターフェース
アプリケーションの手前にリバースプロキシを置くと、ルーティングされた経路という問いを完全に回避できます。プロキシは自身のバックエンド接続を開くため、クライアントのプレフィックスが仮想ネットワーク内でルーティング可能である必要はなく、アプリケーションはプロキシが設定するヘッダーで身元を受け取れます。
そのヘッダーが信頼できるのは、バックエンドがプロキシからだけ受け取り、他の何からも受け取らない場合だけで、nginx の言葉で言えば、届くものすべてを信じるのではなく、信頼する相手を明示的に名指しするということです。
set_real_ip_from 10.40.3.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
バックエンドに直接到達できるクライアントは同じヘッダーを偽造できるため、バックエンドはプロキシ経由以外では到達できてはなりません。これは設定の意見ではなくファイアウォールの事実であり、スニペットをコピーするときに人々が飛ばす前提です。
リレーは、何を買っているのかも変えます。アプリケーション層の身元と、それを記録する自然な場所が手に入る一方で、新しいプロトコルごとに独自のプロキシ対応が必要になるため、汎用性は失われます。HTTP サービスにとってこの取引は通常は良いものであり、HTTP でないものについては通常まったく利用できません。
各境界が何を見るかを書き出す
何を選ぶにしても、設計レビューは、各ホップで送信元アドレスと証明可能な身元を記す一つの表で終えるべきです。それがなければ、最初のインシデントで三つのチームがそれぞれ別のクライアントを語ることになります。
| Boundary | Source the next hop sees | What is actually proven |
|---|---|---|
| Client to gateway | 172.20.8.14 | A certificate or credential was presented |
| Gateway to app subnet, preserved | 172.20.8.14 | The same session, if the return route exists |
| Gateway to app subnet, translated | 10.40.3.10 | Only that traffic came through the gateway |
| Proxy to backend | 10.40.3.10 plus a header | The proxy asserted an identity it authenticated |
そして選びます。アプリケーションの認可が本当にクライアントアドレスに依存し、経路を引き受けられるなら保持します。そうでなく、帯域外で身元を運べるなら変換し、プラットフォーム固有の転送設定には AWS の転送インスタンスのガイダンス を使ってください。サービスが HTTP を話し、身元をアドレスでなくしたいならリレーを選びます。