📘

Azure Policy で * (wildcard) を使うときは count 式を使う

2023/05/16に公開

Azure Policy で * (wildcard) を使うときは count 式を使う

もう title で内容はほぼ伝わってるんですが、具体例を踏まえてみてみます。

当初案 (うまくいかなかったやつ)

今回、Azure Firewall の Firewall Policy において、その送信元や宛先に * を指定できないようにする Azure Policy を作成しようとしていました。
で、ChatGPT の力を借りながらまぁこんなもんだろう、と作ったのがこちらです。

うまくいってないので隠しておきます
{
  "mode": "All",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Network/firewallPolicies/ruleCollectionGroups"
        },
        {
          "anyOf": [
            {
              "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*]",
              "equals": "*"
            }
          ]
        },
        {
          "anyOf": [
            {
              "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.destinationAddresses[*]",
              "equals": "*"
            }
          ]
        }
      ]
    },
    "then": {
      "effect": "deny"
    }
  },
  "parameters": {}
}

で、該当部分はこんな感じですが、これがまぁ意図どおりには動いてくれない、ということですね。

            {
              "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*]",
              "equals": "*"
            }

すでに rule が定義されていて、それを変更して * を入れるときには deny してくれたのですが、rule 自体を追加するときには deny されない、みたいな動作になっていました。
結果から言うと、これは Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*] のすべてが * である場合にのみ true ということを指しています。
今回は、Azure Firewall の Firewall Policy の中で一つでも * がある場合に deny する、ということをしたかったので、これではうまくいきません。

docs はこちらにあります。
結構長くてわかりづらい部分もあるのですが、よく読むとかなり理解が進む内容です。

https://learn.microsoft.com/azure/governance/policy/concepts/definition-structure

注意書きのところにも書いてあるので引用しておきます。

[*] エイリアスを参照する field 式では、配列内の各要素は、要素間で論理積を使用して個別に評価されます。 詳細については、「配列リソース プロパティを参照する」を参照してください。

論理積、つまり AND で評価されているわけですが、今回やりたいのは OR (論理和) にあたるわけですね。

修正版

どうするかというと、やや面倒ではあるのですが count 式というのを使って、条件に合致する要素を数え上げ、それの数が 0 より大きいか、とかそんな比較をしていきます。
更新版の該当箇所はこんな感じになります。

            {
              "count": {
                "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*]",
                "where": {
                  "count": {
                    "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*]",
                    "where": {
                      "count": {
                        "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*]",
                        "where": {
                          "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*]",
                          "equals": "*"
                        }
                      },
                      "greater": 0
                    }
                  },
                  "greater": 0
                }
              },
              "greater": 0
            },

ちょ待てよ、というくらいに長くなりましたが、いろいろ試した結果として、[*] があるたびに count 式を使っていかないとどうもうまくいかないような感じでした。
なんか複雑だな、という気もしますが、逆に [*] があるたびに count 式を使って入れ子にすればいい、と考えればまぁシンプルなのかもしれません。

一番内側の count 式のところで実際に * を含む sourceAddresses があるかを数え上げ、それに該当するような rule があるかを数え上げ、それに該当するような ruleCollection があるかを数え上げ、という感じですね。
一番内側で 0 より大きな数になれば結果的に全体が true となり、結果 deny されるという感じです。
実際にはこれは sourceAddresses に関する条件で、destinationAddresses に関する条件も付けたしたので同じようなのをもう一つ作り、このような感じになりました。

{
  "mode": "All",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Network/firewallPolicies/ruleCollectionGroups"
        },
        {
          "anyOf": [
            {
              "count": {
                "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*]",
                "where": {
                  "count": {
                    "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*]",
                    "where": {
                      "count": {
                        "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*]",
                        "where": {
                          "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.sourceAddresses[*]",
                          "equals": "*"
                        }
                      },
                      "greater": 0
                    }
                  },
                  "greater": 0
                }
              },
              "greater": 0
            },
            {
              "count": {
                "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*]",
                "where": {
                  "count": {
                    "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*]",
                    "where": {
                      "count": {
                        "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.destinationAddresses[*]",
                        "where": {
                          "field": "Microsoft.Network/firewallPolicies/ruleCollectionGroups/ruleCollections[*].FirewallPolicyFilterRuleCollection.rules[*].NetworkRule.destinationAddresses[*]",
                          "equals": "*"
                        }
                      },
                      "greater": 0
                    }
                  },
                  "greater": 0
                }
              },
              "greater": 0
            }
          ]
        }
      ]
    },
    "then": {
      "effect": "deny"
    }
  },
  "parameters": {}
}

以下のような rule の作成を試してうまくいくことを確認していますが、もし参考にする場合にはいろいろ試してみていただければと思います。

  • 送信元、または、宛先に、* を含む rule を新規に作成する場合に deny
  • 送信元、または、宛先に、* を含まない rule を新規に作成する場合に allow
  • 既存の rule に対して、送信元、または、宛先に、* を含む rule を追加する場合に deny
  • 既存の rule に対して、送信元、または、宛先に、* を含まない rule を追加する場合に allow

そもそも Azure Policy の field に何が書けるかがわからん

いやここが Azure Policy の一番うーんなポイントだと思っていて、たぶん Azure の REST で投げる JSON で定義されているものが全部評価できるわけではない、、、はずなんですね、たしか記憶によれば、、、勘違いだと嬉しい。。
で、じゃあどれが評価できるんだよ、というのの一覧がぱっとないと思っていたのですが、例えば PowerShell で取れるようでした。

(Get-AzPolicyAlias -NamespaceMatch 'network').Aliases | clip

ちょっと試した感じだと 13 万行ほど吐かれるので、| clip を入れて clipboard にコピーされるようにしています。
ここから、適当になんかいーーー感じのを探して、それを Azure Policy の field に入れます。

docs はこちらにあります。
Visual Studio Code の extension を利用するのが recommended らしいのですが、なんとなく PowerShell で一覧出した方が別の選択肢とかをざーーーっと眺めるのに便利な気がしています。

https://learn.microsoft.com/azure/governance/policy/concepts/definition-structure#aliases

トライ アンド エラーがめんどい

Azure Policy の検証でめんどいのは、反映に少し時間がかかることから、実際に deny されたときにそれが Azure Policy 変更後なのか反映待ちなのかがわからない点ですね。
加えて、Azure Portal 上での編集もまためんどいということですね。

前者についてはどうしようもないのですが、後者に関しては Bicep で書いてあげましょう。
雰囲気としてはこんな感じで、definition と assignment をまとめることができます。

definition は subscription レベルでやる必要があるため targetScope = 'subscription' という宣言が冒頭に必要です。
ただ、assignment は今回 resource group に適用することにしたため、scope が合いません。
その場合でも、assignment 側を別の bicep module として切り出し、scope 付きで呼び出してあげれば一括の deployment として実行できます。

main.bicep
targetScope = 'subscription'

resource definition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
  name: 'xxxxxxxx-xxxx-xxxx-xxxx-32d084b055b9'
  properties: {
    displayName: 'issue84'
    policyType: 'Custom'
    mode: 'All'
    policyRule: {
<snip>
    }
  }
}

module assignment 'assignment.bicep' = {
  name: 'xxxxxxxxxxxxxxxxxxxxxxxx'
  scope: resourceGroup('issue')
  params: {
    policyId: definition.id
  }
}
assignment.bicep
param policyId string

resource assignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
  name: 'xxxxxxxxxxxxxxxxxxxxxxxx'
  properties: {
    policyDefinitionId: policyId
  }
}

念のため、実行は az deployment sub create となり、よく使う az deployment group create ではないので注意してください。

> az deployment sub create -l japaneast -f main.bicep

まとめ

Azure Policy はコストもかからず、governance の観点から非常にいいサービスなのですが、どうもとっつきづらい部分がありますね。
今回のも、なんか設定したとおりに動いていない、みたいな先入観からどうもうーんと思っていたのですが、まぁ結果間違ってたのはこちら側だったので、すまん、という感じです。
alias に関しても PowerShell 等で一覧が取れるのがわかったので、今後はもう少し積極的に使っていければと思います。

Microsoft (有志)

Discussion