Application Gateway の裏側で API Management の IP フィルタリング機能を使う

Pocket

はじめに

Azure API Management (以下APIM) は IP フィルタリング機能を持っているが、フロントにリバースプロキシを挟んだ場合に想定していた動作と異なったので対応方法をまとめたみた。

実現したいこと

APIM を外部 (インターネット) からと内部 (VNet) からの両方からアクセスできる構成にするために下図のように Application Gateway (以下AppGW) をインターネットからのアクセスの間に挟んだ構成にしている。
APIM では複数システムの API を管理している。

図1

この構成でも AppGW がデプロイされたサブネット上のネットワークセキュリティグループ (以下NSG) で IP フィルタリングを実現できるが、それだと API 毎に細かく IP フィルタリングが行えない。

例えば、システムA の API は x.x.x.x からのみアクセス可能で、システムB の API は y.y.y.y/24 からのみアクセス可能な状態にするなどが NSG だけでは実現ができない。

APIM の IP フィルタリング機能

APIM でも下記のように ip-filter ポリシーを利用すれば IP フィルタリングを簡単に行うことができる。

<policies>
  <inbound>
    <ip-filter action="allow">
      <address>x.x.x.x</address>
    </ip-filter>
  </inbound>
   :

ただ、ここで注意が必要だ。
このフィルタが対象にしている IP アドレスは送信元の IP アドレスで、AppGW などのリバースプロキシを挟んでいる場合は AppGW の IP アドレスがチェック対象になる。

クライアント IP (30.0.0.1)
   ↓
AppGW IP (20.0.0.1)
   ↓  HTTP ヘッダに X-Forwarded-For: 30.0.0.1:port が付与される
APIM IP (10.0.0.1)   ※ <ip-filter> は直前の 20.0.0.1 のアドレスで判定する

この判定は仕様的に問題はないが、APIM がリバプロとして動作するなら Nginx の下記のような設定ができることを期待した。
色々と調べてみたが APIM には該当する方法を見つけることができなかった。

set_real_ip_from   20.0.0.1/32;       # 信頼できる AppGw のアドレス
real_ip_header     X-Forwarded-For;   # 送信元として扱うHTTPヘッダ

Nginx では set_real_ip_from で設定したアドレスからのアクセスの場合に real_ip_header で設定した HTTPヘッダの IP アドレスを送信元として扱うようになる。

力技で乗り切る

APIM には様々な種類のポリシーがあり、ポリシーの中で c# を使いある程度の処理を記述することもできる。
今回試したのは、choose ポリシーで condition 属性を使ってリクエストヘッダの X-Forwarded-For の値を使って送信元の IP アドレス判定を行うように記述してみた。

※ 下記のポリシー設定はグローバルスコープに記述する

<!-- X-Forwarded-Forをもとに判定する処理 -->
<policies>
  <inbound>
    <choose>
      <when condition="@{
if (!context.Variables.ContainsKey("ip-filter")) {
    return false;
}

long? ipConv(string ip) => ip?.Split(new char[]{'.'})
    .Select(x => int.Parse(x)).Aggregate(new { f = 0x1000000, sum = 0L }, (x, y) => {
            return new { f = x.f >> 8, sum = x.sum + x.f * y };
    }).sum;

var forwardIp = ipConv(context.Request.Headers.FirstOrDefault(x => x.Key == "X-Forwarded-For")
    .Value?.Last().Split(new char[]{':'}).First() ?? context.Request.IpAddress);

return !((string)context.Variables["ip-filter"]).Split(new char[]{','}).Any(x => {
        var range = x.Split(new char[]{'-'});
        return range.Count() > 1 ?
            ipConv(range[0]) <= forwardIp && forwardIp <= ipConv(range[1]) :
            ipConv(range[0]) == forwardIp;
    });
}">
        <ip-filter action="allow">
            <address>0.0.0.0</address>
        </ip-filter>
      </when>
    </choose>
  </inbound>
    :

処理の内容を簡単に説明する。

choose ポリシーで条件 (condition) が真 (true) の場合に ip-filter ポリシーが発動するようにしている。
ip-filter ポリシーでは 0.0.0.0 のアドレスのみを許可するように設定しているため、このポリシーが発動すると無条件でブロックすることになる。
つまり、choose ポリシーで真を返すとブロックするということになり 偽 (false) を返すとブロックしないということになる。

ip-filter ポリシーを使う以外でも set-status ポリシーを使って 401 などのステータスに書き換える方法もあるが、その場合はステータスだけの変更になり後続処理が発動してしまうため、あえて例外が発生して後続処理を実行しないようにしている。

次に choose ポリシーの処理の中身を見てみよう。

最初に context.Variables.ContainsKey("ip-filter") で ip-filter という名前の変数がポリシーで定義されているかをチェックしている。
今回作成したフィルタリング処理は汎用的に利用できるようにしているため、許可したい IP アドレスを ip-filter という変数に設定してある場合のみ動作する。
そうすることで、グローバルスコープに本コードを記述して IP フィルタリングが必要な API や オペレータのスコープで ip-filter という変数に許可するアドレスを記述するだけで動作するようにしている。

例) システムAの API のみで IP フィルタリングを行う場合の記述例

※ 下記のポリシー設定はシステムAの API スコープに記述する

<policies>
  <inbound>
    <set-valiable name="ip-filter" value="x.x.x.x" />
    <base />
  </inbound>
    :

ip-filter には CIDR 形式にも対応しており、カンマ区切りで複数の IP 指定もできるようになっている。

    <set-valiable name="ip-filter" value="x.x.x.x, y.y.y.y/24" />

これだけでは不十分

ここまでの実装では Nginx で言うところの real_ip_header に相当する部分だけなので、X-Forwarded-For ヘッダが偽装されれば意味がない。
Nginx では信頼された送信先の場合だけ real_ip_header が動作するように set_real_ip_from もセットにしている。

real_ip_header に相当するものが APIM にも必要になる。

それを実現するには、送信元 IP が AppGW からであればいいので、ここは普通に ip-filter ポリシーを使ってフィルタリングする。

※ 下記のポリシー設定はグローバルスコープに記述する

<policies>
  <inbound>
    <choose>
      <when condition="@(context.Variables.ContainsKey("ip-filter"))">
        <ip-filter action="allow">
          <address-range from="10.127.3.145" to="10.127.3.158" />
        </ip-filter>
      </when>
       :

ip-filter 変数が設定してある場合のみ AppGW からのアクセスかをチェックするために when ポリシーで ip-filter ポリシーを挟んでいる。
AppGW の送信元 IP はインスタンスの数だけ存在するので、AppGw をデプロイしてサブネットのレンジを許可するようにしている。

まとめ

これでやっと実現できたわけだが Nginx ではたった2行を追加するだけのことが APIM だと長い道のりが必要だった。
ただ、ポリシー+ c# を使えば柔軟性があるため何でもできそうだが、慣れていないととっつきにくい印象がある。
これ以外にもバックエンドへのリクエスト時に Host ヘッダを書き換えられないなどまだまだ不足している点は多い。

かゆいところに手が届かないところは多いが、ちょっと前に発表があったゲートウェイのセルフホスト化など進んでいるなど方向性はとても好印象が持てる。

https://azure.microsoft.com/ja-jp/updates/self-hosted-api-management-gateway/

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です