こしあん
2024-04-21

[Terraform]API Gateway+WAFで短期間の同一IPからのアクセスをブロックする


285{icon} {views}


API Gateway+WAFで、短時間の同一IPからのアクセスをブロックするレートベースのルールを試してみました。Terraformだと数個リソースを追加するだけでよく、手軽にできます。この例では、スロットリングとして動作します。

はじめに

Blackbelt OnlineのWAF見てたら、「5分間に同一IPから一定のリクエストがあったらブロックする」というルールがあったので、API Gatewayに統合してみた。WAF扱うの初めてだったが、Terraformでコード生成させたらそこまで難しくなかったのでやってみた。

GPT Log

WAFの部分とAPI Gatewayの統合

https://chat.openai.com/share/69504a1d-ac1e-4a9a-9115-6dd80db1941c

コード

ディレクトリ構成

* main.tf
* lambda_functgion.py

lambda_function.py

def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": "Hello World form WAF Applied API!"
    }

main.tf

terraform {
  backend "local" {
    path = ".cache/terraform.tfstate"
  }
}

provider "aws" {
  region  = "ap-northeast-1"
  profile = "develop"
}

# Deploy Lambda
data "aws_caller_identity" "current" {}

data "archive_file" "lambda_archive" {
  type        = "zip"
  source_file = "lambda_function.py"
  output_path = ".cache/lambda_function.zip"
}

resource "aws_lambda_function" "discord_lambda" {
  function_name    = "WAFAPIGatewayTestLambda"
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.12"
  role             = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/LambdaBasicExecutionRole"

  filename         = data.archive_file.lambda_archive.output_path
  source_code_hash = data.archive_file.lambda_archive.output_base64sha256
}

# API Gateway
resource "aws_api_gateway_rest_api" "example_api" {
  name        = "WAFExampleAPI"
  description = "Example API Gateway with WAF"
}

resource "aws_api_gateway_resource" "example_resource" {
  rest_api_id = aws_api_gateway_rest_api.example_api.id
  parent_id   = aws_api_gateway_rest_api.example_api.root_resource_id
  path_part   = "examplepath"
}

resource "aws_api_gateway_method" "example_method" {
  rest_api_id   = aws_api_gateway_rest_api.example_api.id
  resource_id   = aws_api_gateway_resource.example_resource.id
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda_integration" {
  rest_api_id             = aws_api_gateway_rest_api.example_api.id
  resource_id             = aws_api_gateway_resource.example_resource.id
  http_method             = aws_api_gateway_method.example_method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.discord_lambda.invoke_arn
}

resource "aws_api_gateway_deployment" "example_deployment" {
  depends_on = [
    aws_api_gateway_integration.lambda_integration
  ]

  rest_api_id = aws_api_gateway_rest_api.example_api.id
}

resource "aws_api_gateway_stage" "dev_stage" {
  stage_name    = "dev"
  rest_api_id   = aws_api_gateway_rest_api.example_api.id
  deployment_id = aws_api_gateway_deployment.example_deployment.id
}



resource "aws_lambda_permission" "api_gateway_permission" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.discord_lambda.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_rest_api.example_api.execution_arn}/*/*/${aws_api_gateway_resource.example_resource.path_part}"
}

# WAF
resource "aws_wafv2_web_acl" "example_waf" {
  name        = "example-web-acl"
  scope       = "REGIONAL"  # REGIONAL for API Gateway, CLOUDFRONT for CloudFront
  description = "An example Web ACL"

  default_action {
    allow {}  # デフォルトではすべてのリクエストを許可
  }

  rule {
    name     = "RateLimit5Min100Req"
    priority = 1

    action {
      block {}  # 条件に合致したリクエストをブロック
    }

    statement {
      rate_based_statement {
        limit              = 100
        aggregate_key_type = "IP"
      }
    }

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

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

resource "aws_wafv2_web_acl_association" "example" {
  resource_arn = aws_api_gateway_stage.dev_stage.arn
  web_acl_arn  = aws_wafv2_web_acl.example_waf.arn
}

WAFの部分だけ追加した。あとは普通のAPI Gateway+Lambdaと同じ。

テストする

ローカルから

テストコードは以下の通り。こんな感じでローカルからリクエストを送る。

import requests
import time

def main(num_send=1):
    for i in range(num_send):
        result = requests.post(
            "https://6u6rolssf5.execute-api.ap-northeast-1.amazonaws.com/dev/examplepath")
        print(i, result.status_code, result.text)

        time.sleep(0.1)

if __name__ == "__main__":
    main()

結果はこの通りに、結果が返ってくる。WAF自体は認証をしていないので、特にブロックされなければ結果が常に返ってくる。

0 200 Hello World form WAF Applied API!

Colabから大量アクセスをかける

ColabのIPをBANさせるように大量アクセスをかけてみる。ルールの最小値の5分あたり100でかける。

この通り、最初の100アクセスぐらいは普通にリクエストが通る。ただ、100アクセスしたら厳密にブロックされるわけではなくWAFからブロックされるまで少し遅延があるようだ。

あまり調子に乗ってるとブロックされる(403)になる。

この間に、ローカルからアクセスすると正常に結果が返ってくる(ローカルのIPはブロックされていないため)

ちなみに一定期間してからColabからアクセスすると、また200になるため、この設定だと永続的にブロックされているわけではないようだ。あくまでIPに対応するスロットリングとしての機能になっている。

WAFの画面

ちなみにWAFの画面はこのようになっていた。WAF→WebACLから。作ったACLが出てこない場合は、リージョンを変える。右上ではなく、WebACLのリスト内にリージョンの切り替えがある。

作ったルールを見ると、Blockが発生していた。これはColabからアクセスしたもの。

このルールはなんと2WCUsでいいそうだ。1500WCUsまでWCU部分に追加料金はかからない。

所感

  • WAFの初期費用(月額5ドル)と、ルール単位(月額1ドル)がかかってしまうが、時間按分されるのでテストで作ってすぐterraform destroyしてしまえば、ほとんどコストがかからないと思われる。特に大量アクセスの関係で、WAF以上のコストが発生しているなら割と効果ありそう
  • 最低6ドルかかるので個人で使うのは微妙な気がするが、簡易DDoS攻撃対策にはなるし、あとマネージドルールもいくつかあるし、使いやすいサービスな気がする。WAFはどっちかというと、DDoSよりSQLインジェクション対策な気がするが、少なくともShield Advancedなんかよりかは全然安い
  • デフォルトの無料のAWS Shieldはあるが、カバーしてるのが「Elastic Load Balancing (ELB)、Application Load Balancer、Amazon CloudFront、Amazon Route 53など」と書いてあり、API Gatewayはカバーしていないのかもしれない。
    公式のKnowledge CenterだとWAFを使う例を紹介している。
  • API Gatewayの大量アクセス対策は、デフォルトでついてるスロットリングで一定の効果はあるが、スロットリングしかできないので、マネージドのルールと組み合わせるならWAFが効果があるだろう。


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です