TerraformでAPI GatewayにAWS WAFを組み込む

アプリケーションのセキュリティ対策のため、API Gatewayの手前にAWS WAFを導入しました。
導入にあたり、WAFとIAMロールとCloudWatch Logsの設定をTerraformで行ったので、備忘録として記事にまとめました。

以下がAWS WAF導入の利点となります。
- Webアプリケーションを一般的なWeb攻撃から保護可能。
- AWS管理のルールセットを利用して、効率的にセキュリティルールを適用可能。
- 特定のルールをオーバーライドすることで、柔軟なセキュリティ対策が可能。

https://aws.amazon.com/jp/waf/

# ディレクトリ構成
Terraformのディレクトリ構成は、モジュール化された構造をとっており、それぞれの環境(dev, stg, prod)ごとに設定を分けています。
`modules`ディレクトリに、プロジェクトで使用されるTerraformのモジュール(waf, iam_roles, cloudwatch_logsなど)を格納しています。

```lua
-- terraform-project/
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf
      -- stg/
         -- backend.tf
         -- main.tf
      -- prod/
         -- backend.tf
         -- main.tf
   -- modules/
      -- waf/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- iam_roles/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- cloudwatch_logs/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README.md
      -- ...other…
   -- docs/
      -- architecrture.drowio
      -- architecrture.png
```

# 実装

## WAF
以下のTerraformコードで、API GatewayにWAFを適用し、様々なセキュリティルールを設定しています。

```tf:./modules/waf/main.tf
resource "aws_wafv2_web_acl" "api_gateway_waf" {
  name        = "api-gateway-waf"
  description = "Managed rule WAF"
  scope       = "REGIONAL"

  default_action {
    allow {}
  }

  rule {
    name     = "AWS-AWSManagedRulesCommonRuleSet"
    priority = 0

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"

        rule_action_override {
          action_to_use {
            allow {}
          }
          name = "SizeRestrictions_BODY"
        }
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWS-AWSManagedRulesCommonRuleSet"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "AWS-AWSManagedRulesAmazonIpReputationList"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesAmazonIpReputationList"
        vendor_name = "AWS"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWS-AWSManagedRulesAmazonIpReputationList"
      sampled_requests_enabled   = true
    }
  }

  rule {
    name     = "AWS-AWSManagedRulesSQLiRuleSet"
    priority = 2

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesSQLiRuleSet"
        vendor_name = "AWS"
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWS-AWSManagedRulesSQLiRuleSet"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "apiGatewayWafMetrics"
    sampled_requests_enabled   = true
  }
}

resource "aws_wafv2_web_acl_logging_configuration" "waf_logging_config" {
  log_destination_configs = [var.log_group_arn]
  resource_arn            = aws_wafv2_web_acl.api_gateway_waf.arn

  depends_on = [aws_wafv2_web_acl.api_gateway_waf]

  redacted_fields {
    single_header {
      name = "user-agent"
    }
  }
}

// attach API Gateway dev
data "aws_ssm_parameter" "api_gateway_api_dev_arn" {
  name = "/api/gateway/arn/api/dev"
}

resource "aws_wafv2_web_acl_association" "api_dev" {
  resource_arn = data.aws_ssm_parameter.api_gateway_api_dev_arn.value
  web_acl_arn  = aws_wafv2_web_acl.api_gateway_waf.arn

  depends_on = [aws_wafv2_web_acl.api_gateway_waf]
}

// attach API Gateway stg
data "aws_ssm_parameter" "api_gateway_api_stg_arn" {
  name = "/api/gateway/arn/api/stg"
}

resource "aws_wafv2_web_acl_association" "api_stg" {
  resource_arn = data.aws_ssm_parameter.api_gateway_api_stg_arn.value
  web_acl_arn  = aws_wafv2_web_acl.api_gateway_waf.arn
}

// attach API Gateway prod
data "aws_ssm_parameter" "api_gateway_api_prod_arn" {
  name = "/api/gateway/arn/api/prod"
}

resource "aws_wafv2_web_acl_association" "api_prod" {
  resource_arn = data.aws_ssm_parameter.api_gateway_api_prod_arn.value
  web_acl_arn  = aws_wafv2_web_acl.api_gateway_waf.arn
}
```

```tf:./modules/waf/variables.tf
variable "log_group_arn" {
  description = "The ARN of the CloudWatch Logs group"
  type        = string
}
```

- `aws_wafv2_web_acl`リソースで、Web ACLを作成します。ここでは、`api-gateway-waf`という名前で、REGIONALスコープのWeb ACLを作成しています。
- `default_action`ブロックでは、特定のルールにマッチしないリクエストに対するデフォルトのアクションを設定しています。ここでは、許可(allow)アクションを設定しています。設定されたルールにリクエストがマッチする場合、そのルールで定義されたアクション(例：ブロック)が実行されます。
- `rule`ブロックでは、各ルールを設定しています。このコードでは、AWSが管理する3つのルールセットを使用しています。
    - `AWSManagedRulesCommonRuleSet`：一般的な脆弱性やエクスプロイトを防ぐためのルールセット(XSS、サイズ制限など)。
    - `AWSManagedRulesAmazonIpReputationList`：Amazonが管理するIPリピュテーションリスト(スパム送信者やマルウェア配布者のIPアドレス)を使用して、不正なトラフィックをフィルタリングするルールセット。
    - `AWSManagedRulesSQLiRuleSet`：SQLインジェクション攻撃を検出・防ぐためのルールセット。
- `visibility_config`ブロックでは、CloudWatchにメトリクスを送信するための設定をしています。
- `log_group_arn`では、CloudWatch LogsグループのARNを変数として定義しています。このARNは、WAFのログをCloudWatch Logsに送信する際に使用されます。

### AWS管理ルールセットで一部のルールを許可
AWS管理のルールセットを使うと楽ではあるのですが、一部のルールを許可したい(ブロックしたくない)ときもあります。その場合は`rule_action_override`で設定を上書きします。以下では、`AWSManagedRulesCommonRuleSet`から`SizeRestrictions_BODY`ルール(APIリクエストのサイズ制限に関するルール)に当てはまるリクエストについては許可する設定になっています。

```tf
        rule_action_override {
          action_to_use {
            allow {}
          }
          name = "SizeRestrictions_BODY"
        }
```

マネジメントコンソールでいうと、各ルールのRule action設定の更新と同じです。
![スクリーンショット 2023-09-23 21.04.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7455b9b8-7dc3-57d9-ad14-e2b4434d74c1.png)

## IAMロール
以下のTerraformコードで、WAFのログをCloudWatch Logsに送信するためのIAMロールを作成しています。

```tf:./modules/iam_roles/main.tf
resource "aws_iam_role" "waf_logging" {
  name = "waf_logging"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "waf.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "waf_logging" {
  name = "waf_logging"
  role = aws_iam_role.waf_logging.id

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
EOF
}
```
- `aws_iam_role`リソースで、`waf_logging`という名前のIAMロールを作成しています。
- `aws_iam_role_policy`リソースで、このロールにポリシーをアタッチしています。このポリシーにより、WAFはCloudWatch Logsにログを送信できます。

## CloudWatch Logs
以下のTerraformコードで、WAFのログを保存するためのCloudWatch Logsグループを作成しています。

```tf:./modules/cloudwatch_logs/main.tf
resource "aws_cloudwatch_log_group" "waf_logs" {
  name = "aws-waf-logs-api-gateway"
}
```

```tf:./modules/cloudwatch_logs/outputs.tf
output "log_group_arn" {
  description = "The ARN of the CloudWatch Logs group"
  value       = aws_cloudwatch_log_group.waf_logs.arn
}
```

`aws_cloudwatch_log_group`リソースで、`aws-waf-logs-api-gateway`という名前のロググループを作成しています。

# コードの適用
各modulesの作成が完了したら、`./environments/{environment}/main.tf`でmodulesを読み込んで、`terraform apply`で適用します。

```tf:./environments/{environment}/main.tf
module "waf" {
  log_group_arn = module.cloudwatch_logs.log_group_arn

  source = "../../modules/waf"
}

module "cloudwatch_logs" {
  source = "../../modules/cloudwatch_logs"
}

module "iam_roles" {
  source = "../../modules/iam_roles"
}
```

適用が完了すると、コンソール画面に作成したWebACLが表示されます。
![スクリーンショット 2023-09-23 21.18.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/065c96c4-534a-8e4d-707e-759f22eebcd4.png)