Github actionsから、AWS WAFで設定したIP制限を突破する

概要

cloudfront等で、AWS WAFによってIPアクセス制限がかかっているリソースに対して、github actions hostedな環境からリクエストを送りたい場合があると思います。

github actionsが利用するグローバルIPの範囲は公開されていて、こちら全てをIP許可リストに追加することも可能ですが、保守面やセキュリティ観点としても推奨されるものではないと思います。

GitHub の IP アドレスはときどき変更されます。 IP アドレスによる許可はお勧めしません

GitHubのIPアドレスについて - GitHub Docs

そこで、実行中のgithub actions環境のIPを取得し、動的にIP許可リストに追加する方法を紹介します。

ちなみに、そもそもIPでなく専用のHTTPヘッダーを利用してWAFを突破するような方法も考えられますが、ここでは割愛します。

前提

以下はterraformの例ですが、WAF IPセットと許可ルールによるリソース構成になっている場合を想定します。

resource "aws_wafv2_ip_set" "my_project_github_actions" {
  provider           = aws.virginia
  name               = "github-actions"
  scope              = "CLOUDFRONT"
  ip_address_version = "IPV4"

  // 許可IPリストはgithub actions内の実行時に動的に追加/削除するためignoreする
  lifecycle {
    ignore_changes = [
      description,
      addresses,
    ]
  }
}

resource "aws_wafv2_web_acl" "my_project" {
  provider    = aws.virginia
  name        = "my-project-waf"
  scope       = "CLOUDFRONT"

 ~~中略~~

  rule {
    name     = "allow-github-actions-ip"
    priority = 1

    action {

      allow {
      }
    }

    statement {
      ip_set_reference_statement {
        arn = aws_wafv2_ip_set.my_project_github_actions.arn
      }
    }
  }
 ~~中略~~

結果

~~ 中略 ~~
jobs:
  sample:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    env:
      WAF_IPSET_SCOPE: {YOUR_WAF_IPSET_SCOPE}
      WAF_IPSET_REGION: {YOUR_WAF_IPSET_REGION}
      WAF_IPSET_ID: {YOUR_WAF_IPSET_ID}
      WAF_IPSET_NAME: {YOUR_WAF_IPSET_NAME}
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v3
      - name: authenticate to aws
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
          role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
      - name: Get IP address
        id: get-ip
        run: echo "ip=$(curl -s https://checkip.amazonaws.com)" >> $GITHUB_OUTPUT
        shell: bash
      - name: Add IP address to ipset
        shell: bash
        run: |
          current_ip=${{ steps.get-ip.outputs.ip }}/32

          # このワークフローを並列で同時実行しても動くように、元から存在するIPをリストから消さないように更新する
          lock_token=$(aws wafv2 get-ip-set --scope $WAF_IPSET_SCOPE --region $WAF_IPSET_REGION --id $WAF_IPSET_ID --name $WAF_IPSET_NAME | jq -r '.LockToken')
          existing_ips=$(aws wafv2 get-ip-set --scope $WAF_IPSET_SCOPE --region $WAF_IPSET_REGION --id $WAF_IPSET_ID --name $WAF_IPSET_NAME | jq -r '.IPSet.Addresses[]')
          aws wafv2 update-ip-set --scope $WAF_IPSET_SCOPE --region $WAF_IPSET_REGION --id $WAF_IPSET_ID --name $WAF_IPSET_NAME --addresses $existing_ips $current_ip --lock-token $lock_token

          sleep 10  # ipsetの更新が反映されるまで待つ
      # ~~ IP制限しているリソースへのリクエスト付きの処理 ~~
      - name: Delete IP address from ipset
        if: always()
        shell: bash
        run: |
          ip_to_remove=${{ steps.get-ip.outputs.ip }}/32
          lock_token=$(aws wafv2 get-ip-set --scope $WAF_IPSET_SCOPE --region $WAF_IPSET_REGION --id $WAF_IPSET_ID --name $WAF_IPSET_NAME | jq -r '.LockToken')
          existing_ips=$(aws wafv2 get-ip-set --scope $WAF_IPSET_SCOPE --region $WAF_IPSET_REGION --id $WAF_IPSET_ID --name $WAF_IPSET_NAME | jq -r '.IPSet.Addresses[]')

          # このワークフローを並列で同時実行しても動くように、元から存在するIPをリストから消さないように更新する
          updated_ips=()
          for ip in $existing_ips; do
            if [ "$ip" != "$ip_to_remove" ]; then
              updated_ips+=("$ip")
            fi
          done

          if [ ${#updated_ips[@]} -eq 0 ]; then
              updated_ips_string='[]'
          else
              updated_ips_string=$(printf "%s " "${updated_ips[@]}")
          fi

          aws wafv2 update-ip-set --scope $WAF_IPSET_SCOPE --region $WAF_IPSET_REGION --id $WAF_IPSET_ID --name $WAF_IPSET_NAME --addresses $updated_ips_string --lock-token $lock_token

設定値は以下です

・YOUR_WAF_IPSET_SCOPE: CLOUDFRONT か REGIONAL
・YOUR_WAF_IPSET_REGION: ipsetの存在するリージョン(CLOUDFRONTの場合、globalではなくus-east-1を指定する必要がありました)
・YOUR_WAF_IPSET_ID: 添付図参照
・YOUR_WAF_IPSET_NAME: 添付図参照

ipset

解説

・処理の流れは大きく以下になってます

  1. 実行環境のIP取得
  2. 1で取得したIPをipsetにinsert
  3. IP制限環境へのリクエス
  4. 2で登録したIPをipsetから削除

aws wafv2 では、v1やセキュリティグループの仕様とは違って、ipsetのリストへそのまま任意のIPを挿入/削除することはできないようでした。 (おそらく、AWSの内部的なデータ整合性の観点だと思われます)

そのため、一度入れ替え更新のようなコードになっています。

This operation completely replaces the mutable specifications that you already have for the IP set with the ones that you provide to this call. To modify an IP set, do the following: 1. Retrieve it by calling GetIPSet 2. Update its settings as needed 3. Provide the complete IP set specification to this call

docs.aws.amazon.com