はじめに
前に似たようなタイトルで記事を書きましたが、そのときは送信元が * または Internet タグだったら、というシンプルなものでした。
今回はそれよりもう少し細かく制御するポリシーを作りました。
ロジックがなかなか面倒だったのでその辺の紹介も兼ねて。
ちなみに前回のはこれ
2022/10/28 x.x.x.x/x でのレンジ指定にも対応できるよう改修しました。
目次
やりたいこと
条件を満たした NSG ルールを Deny する、カスタム ポリシーを作ります。
ポリシーで記述する条件は以下。これらを満たしたら deny する
- NSG の受信許可ルールであること
- 送信元 IP アドレスに、"許可されている IP アドレス リスト" 以外の IP アドレスが含まれていること
- 宛先ポートに、3389, 22, * のいずれかが含まれていること
"許可された IP からしか RDP/SSH を許可できないようにする"、というのが想定しているところです。
"許可されている IP アドレス" の部分は、パラメータにして任意に指定できるようにします。
これをカスタム ポリシーにします。
カスタム ポリシー
作成したポリシーがこれです。
ロジック部分を順に解説します。
条件 1 : NSG の受信許可ルールであること
これを記述しているのは、ポリシーのこの部分です。
type が NSG のルールであるかどうか、許可ルールかどうか、受信ルールかどうかを判定しています。
ここは通常のポリシーでもあるようなシンプルな記述なのでわかりやすいと思います。
条件 2 : 送信元 IP アドレスに、許可されている IP アドレス以外が含まれていること
ここについては、まず抑えるべきポイントがあります。
ポイント 1 : 送信元アドレスが格納されるプロパティ
NSG のルールを作成したとき、送信元 IP アドレスがカンマ区切りで入力されているかどうかにより、sourceAddressPrefix と sourceAddressPrefixes のどちらのプロパティに格納されるかが異なります。
- 送信元 IP アドレスがカンマ区切りではない場合
この場合、sourceAddressPrefix に値が格納され、もう一方の sourceAddressPrefixes は空になります。
- 送信元 IP アドレスがカンマ区切りの場合
この場合、sourceAddressPrexies に値が格納され、もう一方の sourceAddressPrefix は消えます。 (get しても空すら見えない)
このように、入力の仕方でプロパティが異なるため、それぞれに対して判定の条件が必要です。
また、プロパティが空の場合があるため、空だった場合は評価しないように処理することも必要です。
ポイント 2 : 配列の要素の参照と比較
sourceAddressPrefixes の場合は配列のため、"許可されている IP アドレス以外が含まれているか" を評価するには各要素の比較が必要です。
その場合はここにあるように、sourceAddressPrefixes[*] という書き方をします。
これを比較に用いる場合、"配列のすべての要素が式を満たしたら True, 1 つでも満たさなければ False" という評価が行われます。
例として以下の場合、objectArray 配列内のすべてのプロパティが value と等しければ True、そうでなければ False という評価です。
{ "field": "Microsoft.Test/resourceType/objectArray[*].property", "equals": "value" }
この 2 つを抑えたうえで実際の記述についてです。
条件 2 を記述しているのはこの部分です。
レンジ指定にも対応しようといろいろ考えた結果こうなりました。
送信元アドレスが 1 つの場合
1 つの場合、この部分が該当します。
ここでは "A かつ (B または C)" という 3 ブロックの構成になっており、それぞれ以下のように処理しています。
1 つ目のブロックでは、sourceAddressPrefix が空かどうかを評価します。
2 つ目のブロックでは、送信元として指定された値が、Any または Internet タグかどうかを評価します。
3 つ目のブロックでは、送信元として指定された値が、allowedIpAddressesList に含まれる回数を数え、0 より大きければ許可されている IP アドレスである、という評価をしています。
この時、文字列として比較するのではなくレンジで含まれるかどうか比較するため、ipRangeContains という関数を使います。
これは、ipRangeContains(range, targetRange) とすることで、range の範囲に targetRange のアドレスが含まれるかどうかを判定してくれる関数です。
range = 1.1.1.1, targetRange = 1.1.1.1 と同じ値になったときも判定してくれるので、これでレンジ指定でも、そうでなくても (/32 でも) 処理することができます。
ただし注意点として、Any 指定の * がパラメータとして与えられると処理できずにエラーになってしまいます。
そのため、sourceAddressPrefix が * の場合は即 False を返す (0 回になる) よう、if 分で処理しています。
送信元アドレスが複数の場合
複数の場合、この部分が該当します。
ここは A かつ B の 2 ブロック構成です。
上のブロックで、sourceAddressPrefixes が空かどうか評価します。
下のブロックで、sourceAddressPrefixes 配列に格納されている各 IP アドレスが、allowedIpAddressesList に含まれているかどうかを評価します。
ここでは、count を 2 回使って、2 重 for 文のような処理をしています。
58 行目のコードで、sourceAddressPrefixes 配列から 1 つ値を取り出します。
60 から 68 行目のコードで、取り出した値が、allowedIpAddressesList に含まれるかどうかを、回数をカウントすることで評価します。
ここは、前述のアドレスが 1 つの場合と同様に、ipRangeContains 関数でレンジ指定も含めて評価を行い、Any 指定 (*) の場合の処理を if 文で行います。
1 つでも含まれる場合があれば、68 行目の条件に当てはまり、外側の count の回数が増加します。
ここで、"どうやって sourceAddressPrefixes[*] のうち 1 つでも allowedIpAddressesList に含まれない IP アドレスがあるか評価するか" ですが、souceAddressPrefixes に指定した値が何個であれ、各要素に対して "allowedIpAddressesList のうち 1 つにでも当てはまれば (含まれていれば) 外側のカウントを +1" という処理が行われます。
そのため、souceAddressPrefixes の要素全てが許可されるの値なのであれば、"外側の count 数 = sourceAddressPrefixes 配列の要素数" となるはずです。
それを用いて、外側の count (71 行目) で "カウントした回数が送信元 IP アドレスに指定した要素より少なかったら監査対象" という条件にしました。
これで、sourceAddressPrefixes のうち 1 つでも許可されない IP があれば監査、という評価を実現できていると思います。
文字ばかりになってしまい非常にわかりづらいと思うので、できればフローチャート的なのいれたい…
ここを考えるのが一番難しかったです。これでうまくできていると思うのですが、そうならないパターンがあれば、ぜひ教えてください。
条件 3 : 宛先ポートに、3389, 22, * のいずれかが含まれていること
宛先ポートについても送信元アドレスと同様に、カンマ区切りで入力されているかどうかにより、destinationPortRange と destinationPortRanges のどちらのプロパティに格納されるかが異なります。
そのため、この前半部分は送信元 IP アドレスの場合と同じ考え方で OK です。
一方で、ポートの場合は 20 - 100 のようなハイフンでの連番指定があるため、それを処理する必要があります。
連番指定の場合
連番指定の場合、"a - b" のように入力されます。
ここに 3389 または 22 が含まれるか判定するには、"-" が含まれていたらハイフンの両側にある a, b を個別に取得し、a ≦ 3389 (or 22) ≦ b を満たすかどうかを評価します。
カンマ区切りでない場合のハイフンの評価と、カンマ区切りの場合のハイフンの評価をし、どこかで 3389 または 22 の許可を検知したら True としています。
それを実装しているのが後半のこの辺りです。
これで宛先ポートに 3389 or 22 が含まれるかどうか評価することができます。
未実装な部分 (実装済み)
ここまででもだいぶ手がかかったのですが、実はこれでも不十分です。
それは、x.x.x.x/x のように指定しても文字列として比較しているため、レンジ的には許可 IP アドレスの範囲に含まれる、というのが評価できません。
例として、パラメーターの許可 IP アドレス リストに 192.168.0.0/16 を指定した状態で、NSG のルール作成時に送信元アドレスに 192.168.1.10 を入力したルールを作成した場合、 レンジには含まれるので 3389/22 宛てでも許可してほしいところですが、本ポリシーだと deny されます。
192.168.0.0/16 と 192.168.1.10 を文字列として比較し、一致していないためです。
本来はそこまでできて完成なのですが、ここまでのポリシー作成のためのロジックと検証で力つきました。
レンジに含まれるかどうか判定する ipRangeContains という関数があるので、/ を含んでいる場合はそれを使って判定、ということをする感じになると思います。
余裕があれば足したいとは思っていますが、現状はこういう状態なので参照する場合は注意してください。
今回の改修でレンジ指定に対応できるようになったので、全部対応できるはず!
が、すべてのケースを整理してテストしているわけではないので、そのまま使用する場合は必ずテストしてください。
終わりに
NSG 向けのカスタム ポリシーを頑張って作ってみました。結果、かなり大変でした。
NSG 以外のリソースならカンマ区切りくらいはあれど、プロパティが変わったりハイフンや CIDR のレンジ指定はほとんどないだろうと思うので、NSG のルールだからこうなのだろうと思います…
FW のルールとかくらいかな…
少しずつ変えながら何度も検証して、いろんな Docs やサンプルとか回ってやっとできたので、ガバナンスのためにいろんなリソースをポリシーで制御しよう、でもビルトインにないからカスタムで作っていこう、となったらなかなか大変そう。