Github Actionsの自動APIテストでWAFの地域制限を突破する

この記事は [DENSOアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/denso) の4日目の記事です。

## はじめに
**本記事の要点を3行で**
Webアプリケーションのセキュリティ向上のためにWAF（Web Application Firewall）を利用中
WAFの地域制限が原因でGitHub ActionsからのAPIテストに失敗する課題があり
この問題を解決する方法の一例を紹介

## 課題の詳細
### WAFと地域制限の概要
- **WAF（Web Application Firewall）の役割**
  - WAFは、Webアプリケーションに対する攻撃を検知・防御するセキュリティ対策ツール
  - 代表的な攻撃例:
    - SQLインジェクション
    - クロスサイトスクリプティング（XSS）
    - ボットによる不正アクセス
  - 主にトラフィックを監視し、設定したルールに基づいてアクセスを制御する

- **地域制限の目的と動作**
  - WAFでは、特定の地域からのアクセスを制限する機能を持つ場合が多い
  - この制限は、次の理由で有効:
    - 攻撃元となる地域をブロックすることでセキュリティリスクを軽減
    - 特定地域内のユーザーに限定したサービス提供
  - 制限の実装例:
    - IPアドレスの地域情報に基づきアクセスを許可または拒否

### GitHub Actionsの制限
- **GitHub Actionsの動作環境**
  - ワークフローの実行にはGitHubが提供する共有ランナーを利用する
  - 共有ランナーでは、実行ごとに異なる動的なIPアドレスが使用される

### 発生した課題
- **地域制限との衝突**
  - WAFの地域制限が、GitHub Actionsからのリクエストをブロックする可能性
  - 例えば、共有ランナーが許可地域外のIPを使用した場合、APIアクセスが遮断される
  - この結果、テストやデプロイメントが失敗する事態が発生

## 解決方法の概要
APIテストを実行するAWS Lambda関数を許可地域にデプロイし、Github Actionsから該当のLambda関数を実行する

## 手順
1. AWS側にIdPを設定しRoleを引き受けるよう設定（[参考リンク](https://aws.amazon.com/jp/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/)）
RoleはLambda関数を実行できる許可ポリシーが必要
1. **許可地域のリージョン**でAPIテスト実行用のLambda関数を準備
1. Github Actionsで2.のLambda関数を実行し、結果を取得する

ポイントは2.でLambda関数をデプロイするリージョンを許可地域内にしているところです。
これにより、WAFの地域制限を突破することができます。

GitHub Actionsのワークフロー例は以下の通りです。
[こちらのページ](https://style.biglobe.co.jp/entry/2023/09/27/100000)を参考にさせていただきました。

``` .github/workflows/lambda-invoke.yml
name: lambda-invoke
on: pull_request
permissions:
  id-token: write
  contents: read
jobs:
  api_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/lambda-invoke
        with:
          lambda-function-name: hogehoge
          aws-account-id: ${{ secrets.AWS_ACCOUNT_ID }}
          iam-role-name: ${{ secrets.IAM_ROLE_NAME }}
```

``` .github/actions/lambda-invoke/action.yml
name: Lambda Invoke
description: Lambda関数を同期実行し、レスポンスがfalseの場合Github Actionsを失敗させる

inputs:
  lambda-function-name:
    required: true
    description: 実行する Lambda の名前
  aws-region:
    default: ap-northeast-1
    required: true
    description: AWSリージョン
  aws-account-id:
    required: true
    description: AWSアカウントID
  iam-role-name:
    required: true
    description: IAMロール名
  read-timeout-second:
    default: 300
    required: true
    description: 最大ソケット読み取り時間(秒)
outputs:
  result:
    value: ${{ steps.execute-lambda.outputs.result }}
    description: Lambda の実行結果 (OK / NG)
  response:
    value: ${{ steps.execute-lambda.outputs.response }}
    description: Lambda function のレスポンス

runs:
  using: composite

  steps:
    # IAMロールを引き受ける
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-region: ${{ inputs.aws-region }}
        role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/${{ inputs.iam-role-name }}
        role-session-name: gh-action-${{ github.run_id }}

    # AWS CLIでLambda関数を実行し、レスポンスがfalseの場合Github Actionsを失敗させる
    - name: Execute Lambda
      id: execute-lambda
      shell: bash
      env:
        PAYLOAD_JSON: ${{ steps.check-json-format.outputs.payload-json }}
        LAMBDA_FUNCTION_NAME: ${{ inputs.lambda-function-name }}
        READ_TIMEOUT_SECOND: ${{ inputs.read-timeout-second }}
      run: |
        output=$(
          aws lambda invoke \
            --function-name "${LAMBDA_FUNCTION_NAME}" \
            --cli-binary-format raw-in-base64-out \
            --cli-read-timeout "${READ_TIMEOUT_SECOND}" \
            /dev/stdout \
            | jq -s
        )
        echo "${output}"

        if echo "${output}" | jq -e -r '.[1].FunctionError' > /dev/null; then
          echo "ERROR: Lambda execution failed."
          result="NG"
        else
          echo "INFO: Lambda execution success."
          result="OK"
        fi

        echo "result=${result}" >> "${GITHUB_OUTPUT}"
        response=$(echo "${output}" | jq .[0])
        echo "INFO: Lambda function response: "${response}

        response="${response//'%'/'%25'}"
        response="${response//$'\n'/'%0A'}"
        response="${response//$'\r'/'%0D'}"
        echo "response=${response}" >> "${GITHUB_OUTPUT}"

        if [ "${response}" = "false" ]; then
          exit 1
        fi
```

## 考慮した前提条件
- サービスインフラとしてAWSを使用しているので、可能であればAWSを利用したい
- 管理の手間がかかるので可能な限りマネージドサービスを使いたい

## その他の実現方法例
他にも下記方法などがあると思いますが、今回は着手の容易性などからLambda関数で実装する方式をとっています。
- 制限対象地域のリージョンにGitHub Actionsのセルフホステッドランナーを構築して実行する
- PostmanなどのSaaS製品を使い、リクエスト元のリージョンを指定する