AWS WAFでネストしたOR/ANDを含むルールを楽して作りたい

2023.06.06

初めに

AWS WAFを触った時、マネジメントコンソールから独自のルールを作る際にAND/OR自体は選択できるものの、ANDの中にOR、ORの中にANDが作れない...となったことはありませんか?

CloudFormationから作っている方は構造に沿っていれば特に制約なくAND/ORを重ねられるので特に意識しない方も多いかもしれませんが、マネジメントコンソール作成する際にビジュアルモード限定の制約でORやANDをネストすることができません。

https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/waf-rule-statement-type-and.html
ビジュアルエディタを使用して OR または AND ステートメントをネストすることはできません。このタイプのネストを設定するには、JSON でルールステートメントを指定する必要があります。

ではマネジメントコンソールでは対応できないかというとそんなことはなく、JSONエディタモードが用意されており設定値をJSONで書くことでマネジメントコンソールからでも設定が可能です。

とはいえいきなりJSONを0から手打ちでというのも気後れしてしまうのもわかりますし、そもそも全手打ち自体が非常に手間です。

では何かいい方法はないかというと、ビジュアルエディタで作成したものをJSONで出力することも可能なので、これをうまく使いパズルのように組み立てられるようになりましょう。

条件指定をわかった気になる

単一ルールの条件を見てみる

まずはシンプルな例でURIパスが/admin/から始まる場合にブロックするルールの条件を見てみます。

{
  "Name": "nested-base",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "nested-base"
  },
  "Statement": {
    "ByteMatchStatement": {
      "FieldToMatch": {
        "UriPath": {}
      },
      "PositionalConstraint": "STARTS_WITH",
      "SearchString": "/admin/",
      "TextTransformations": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ]
    }
  }
}

とりあえずここではStatementブロック内にリクエストの条件が指定されているということがわかれば大丈夫です!

この中身以外の部分はマネジメントコンソールの指定で十分ですので触りません。

  "Statement": {
    /** URIパスが/admin/から始まるという条件 */
  }

AND/ORによる条件演算子

では先ほどの条件に加えてAuthorizationヘッダがxxxxxxxxxではないときという条件をビジュアルエディタで加えて再度表示してみましょう。

{
  "Name": "nested-base",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "nested-base"
  },
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "FieldToMatch": {
              "UriPath": {}
            },
            "PositionalConstraint": "STARTS_WITH",
            "SearchString": "/admin/",
            "TextTransformations": [
              {
                "Type": "NONE",
                "Priority": 0
              }
            ]
          }
        },
        {
          "NotStatement": {
            "Statement": {
              "ByteMatchStatement": {
                "FieldToMatch": {
                  "SingleHeader": {
                    "Name": "Authorization"
                  }
                },
                "PositionalConstraint": "EXACTLY",
                "SearchString": "xxxxxxxxx",
                "TextTransformations": [
                  {
                    "Type": "NONE",
                    "Priority": 0
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

ネストが多くなって辛いですが、ピックアップしてみれば個別の条件がANDの要素の中に存在するだけです。

  "Statement": {
    "AndStatement": {
      "Statements": [
        {/** URIパスが/admin/から始まるという条件 */},
        {/** Authorizationヘッダがxxxxxxxxxという値であるという条件 */}
      ]
    }
  }

OR条件もAndStatementというキーがOrStatementというキーになっているだけで作りは同じです。

  "Statement": {
    "OrStatement": {
      "Statements": [
        {/** 条件1の内容 */},
        {/** 条件2の内容 */},
        ...
      ]
    }
  }

Not条件は単一条件なのでちょっとだけ形が異なりますが、概ね似たような構造です。

  "Statement": {
    "NotStatement": {
      "Statement": {
        {/** 条件1の内容 */}
      }
    }
  }

ANDやORをネストする

ではネストされている状態のJSONというのはどういうものかというと、 先ほどの例で条件1の内容をORにしたい...となった場合はそのまま条件1をOrStatementに置き換えてしまうだけです。

  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "OrStatement": {
            "Statements": [
              {/** 条件1-1の内容 */},
              {/** 条件1-2の内容 */},
              ...
            ]
          }
        },
        {/** 条件2の内容 */},
        ...
      ]
    }
  }

実際に作ってみる

今回は以下のルールを作ります。

  • 以下の両方を満たすアクセス以外にはカスタムレンスポンスで404を返却する
    • 以下のいずれかを満たす(条件1)
      • URIパスが/admin/から始まる(前方一致)
      • URIパスが/maintenance.htmlである(完全一致)
    • アクセス元の国がJP以外である(条件2)

※ 今回のような条件1の場合は正規表現の選択肢もありますが説明のためこのような方式にしています

条件部品を作る

まずは個別の条件となる部分の部品を作っていきます。

まず条件の外枠となるAndStatement条件のブロックを用意しておきましょう

    "AndStatement": {
      "Statements": [
        {/** 条件1 */},
        {/** 条件2 */}
      ]
    }

まずルールとして条件1だけを満たすルール、条件2だけを満たすルールをビジュアルエディタで作成し、それぞれJSONを取り出します(保存は不要)。

Statementの中身が欲しいのでそれ以外の値は適当で大丈夫です。

条件1
{
  "Name": "condition1",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "condition1"
  },
  "Statement": {
    "OrStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "FieldToMatch": {
              "UriPath": {}
            },
            "PositionalConstraint": "STARTS_WITH",
            "SearchString": "/admin/",
            "TextTransformations": [
              {
                "Type": "NONE",
                "Priority": 0
              }
            ]
          }
        },
        {
          "ByteMatchStatement": {
            "FieldToMatch": {
              "UriPath": {}
            },
            "PositionalConstraint": "EXACTLY",
            "SearchString": "/maintenance.html",
            "TextTransformations": [
              {
                "Type": "NONE",
                "Priority": 0
              }
            ]
          }
        }
      ]
    }
  }
}
条件2
{
  "Name": "condition2",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "condition2"
  },
  "Statement": {
    "NotStatement": {
      "Statement": {
        "GeoMatchStatement": {
          "CountryCodes": [
            "JP"
          ]
        }
      }
    }
  }
}

個別のルールができたので、先ほどのAndStatementの外枠のコメント部分をそれぞれの条件のStatementブロックの中身で置き換えましょう。

置換後
    "AndStatement": {
      "Statements": [
        {
          "OrStatement": {
            "Statements": [
              {
                "ByteMatchStatement": {
                  "FieldToMatch": {
                    "UriPath": {}
                  },
                  "PositionalConstraint": "STARTS_WITH",
                  "SearchString": "/admin/",
                  "TextTransformations": [
                    {
                      "Type": "NONE",
                      "Priority": 0
                    }
                  ]
                }
              },
              {
                "ByteMatchStatement": {
                  "FieldToMatch": {
                    "UriPath": {}
                  },
                  "PositionalConstraint": "EXACTLY",
                  "SearchString": "/maintenance.html",
                  "TextTransformations": [
                    {
                      "Type": "NONE",
                      "Priority": 0
                    }
                  ]
                }
              }
            ]
          }
        },
        {
          "NotStatement": {
            "Statement": {
              "GeoMatchStatement": {
                "CountryCodes": [
                  "JP"
                ]
              }
            }
          }
        }
      ]
    }

設定するルールを作る

条件部分ができましたので実際に適用するルールを作っていきます。

ビジュアルエディタでIf requestの条件以外の部分を想定する値に設定します。
If requestの部分は後ほど上記のAndStatementで差し替えるのでなんでもいいです。

この時点ではJSONは以下の通りです。

置換前JSON
{
  "Name": "developer-page-return-not-found",
  "Priority": 0,
  "Action": {
    "Block": {
      "CustomResponse": {
        "ResponseCode": "404"
      }
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "developer-page-return-not-found"
  },
  "Statement": {
    "SizeConstraintStatement": {
      "FieldToMatch": {
        "UriPath": {}
      },
      "ComparisonOperator": "EQ",
      "Size": "1",
      "TextTransformations": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ]
    }
  }
}

先ほどのAndStatementのJSON値を上記のStatementパラメータの中と差し替え、Validateをかけて問題なければ保存して目的のルールの完成です。

なおValidateの実行時にJSONに誤りがある場合エラーを出すだけのケースもあれば、階層を取り除いたり整形を入れられる場合があるようですので実行前にJSONを退避しておき保存前に比較しておくのが安全です。

置換後JSON
{
  "Name": "developer-page-return-not-found",
  "Priority": 0,
  "Action": {
    "Block": {
      "CustomResponse": {
        "ResponseCode": "404"
      }
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "developer-page-return-not-found"
  },
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "OrStatement": {
            "Statements": [
              {
                "ByteMatchStatement": {
                  "FieldToMatch": {
                    "UriPath": {}
                  },
                  "PositionalConstraint": "STARTS_WITH",
                  "SearchString": "/admin/",
                  "TextTransformations": [
                    {
                      "Type": "NONE",
                      "Priority": 0
                    }
                  ]
                }
              },
              {
                "ByteMatchStatement": {
                  "FieldToMatch": {
                    "UriPath": {}
                  },
                  "PositionalConstraint": "EXACTLY",
                  "SearchString": "/maintenance.html",
                  "TextTransformations": [
                    {
                      "Type": "NONE",
                      "Priority": 0
                    }
                  ]
                }
              }
            ]
          }
        },
        {
          "NotStatement": {
            "Statement": {
              "GeoMatchStatement": {
                "CountryCodes": [
                  "JP"
                ]
              }
            }
          }
        }
      ]
    }
  }
}

完了後に保存されたルールを確認して目的の設定になっていることを確認しましょう。

エラーっぽい表示が出ていますがビジュアルエディタだと開けない旨が書いてあるので問題ありません。

なおValidate時ではなく保存のタイミングでインデントの調整やJSONのパラメータの順序が特定の順番に調整されるようです。

個人的な思いですがビジュアルエディタだとActionVisibilityConfigが上に出るのに保存すると下側に移るのは少し違和感はあるのでなんとかならないかの気持ちはあります。

終わりに

今回はビジュアルエディタを併用してJSONエディタの設定を行うことで、AND/ORのネストが必要な場合でもほとんどJSONを打ち込むことなく設定してみました。

JSONエディタ触ったことない人の入口としてもやりやすいですし、CloudFormationで組み立てているような人でも具体的なルール部分をうまくビジュアルエディタに任せ抽出することで作業の効率化になるケースもあるかと思いますので、ビジュアルエディタ使えないケースだからと拘らず併用して作業の効率化を図ってみてはいかがでしょうか。