こしあん
2025-01-08

API GatewayのリソースポリシーでWAFを使わずにIP制限


1{icon} {views}


モック統合でもリソースポリシーを利用すれば、API Gateway単体で簡単にIP制限をかけられる。WAFに比べ高機能ではないものの、開発時の安全対策として十分に活用できる。

はじめに

ポイント

API Gatewayのモック統合をベースに進める。

モックであってもIP制限は可能。なぜならモック統合とはLambdaのついていないAPI Gatewayの単体で仮のレスポンスを返す機能で、リソースポリシーはAPI Gatewayの自体の機能だから。

許可するIPレンジを変数で定義する。

variable "allowed_ips" {
  description = "APIアクセスを許可するIPアドレスまたはCIDRのリスト"
  type        = list(string)
  default     = ["123.0.45.0/24"] # 例: 許可するIPレンジ
}

localsにポリシーを定義する

locals {
  # ...

ip_restriction_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IPAllow",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.example_api.id}/*/*/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": ${jsonencode(var.allowed_ips)}
        }
      }
    }
  ]
}
POLICY
}

既に定義しているAPI Gatewayに対してポリシーを付与する

resource "aws_api_gateway_rest_api" "example_api" {
  name        = "ArithmeticOperationsAPI_Mock"
  description = "Mock API for arithmetic operations"
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_api_gateway_rest_api_policy" "ip_restriction" {
  rest_api_id = aws_api_gateway_rest_api.example_api.id
  policy      = local.ip_restriction_policy
}

再デプロイのトリガーに追加する

resource "aws_api_gateway_deployment" "example_deployment" {
  rest_api_id = aws_api_gateway_rest_api.example_api.id

  triggers = {
    # ...
    redeployment_policy               = sha1(local.ip_restriction_policy)
  }

  lifecycle {
    create_before_destroy = true
  }
}

結果

許可されているIPからcurlした場合

$ curl https://p5rkuqovn2.execute-api.ap-northeast-1.amazonaws.com/dev/add
{
  "message": "Mock response for add"
}

許可外のIPからcurlした場合

$ curl https://p5rkuqovn2.execute-api.ap-northeast-1.amazonaws.com/dev/add
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:ap-northeast-1:********9012:p5rkuqovn2/dev/GET/add"

注意点

  • ポリシーの変更が即反映されるわけではない。特にAPI Gatewayにキャッシュが入っていて(CloudFrontがついているケース)、キャッシュに古いポリシーが残っていると反映にラグがある場合がある。すぐ反映されなくても数分~数十分待ったほうが良さげ

全体のコード

variable "allowed_ips" {
  description = "APIアクセスを許可するIPアドレスまたはCIDRのリスト"
  type        = list(string)
  default     = ["123.0.45.0/24"] # 例: 許可するIPレンジ
}

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

locals {
  operation_funcs = {
    "add" = "add"
    "sub" = "sub"
    "prd" = "prd"
    "div" = "div"
  }

  ip_restriction_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IPAllow",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.example_api.id}/*/*/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": ${jsonencode(var.allowed_ips)}
        }
      }
    }
  ]
}
POLICY
}

resource "aws_api_gateway_rest_api" "example_api" {
  name        = "ArithmeticOperationsAPI_Mock"
  description = "Mock API for arithmetic operations"
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_api_gateway_rest_api_policy" "ip_restriction" {
  rest_api_id = aws_api_gateway_rest_api.example_api.id
  policy      = local.ip_restriction_policy
}

# 各operation用のリソース作成
resource "aws_api_gateway_resource" "operation_resource" {
  for_each    = local.operation_funcs
  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   = each.key
}

# 各operationに対してGETメソッドを作成
resource "aws_api_gateway_method" "operation_method" {
  for_each      = local.operation_funcs
  rest_api_id   = aws_api_gateway_rest_api.example_api.id
  resource_id   = aws_api_gateway_resource.operation_resource[each.key].id
  http_method   = "GET"
  authorization = "NONE"
}

# モック統合設定
resource "aws_api_gateway_integration" "operation_integration" {
  for_each = local.operation_funcs

  rest_api_id = aws_api_gateway_rest_api.example_api.id
  resource_id = aws_api_gateway_resource.operation_resource[each.key].id
  http_method = aws_api_gateway_method.operation_method[each.key].http_method
  type        = "MOCK"

  request_templates = {
    "application/json" = <<EOF
{
  "statusCode": 200
}
EOF
  }
}

resource "aws_api_gateway_method_response" "operation_method_response_200" {
  for_each = local.operation_funcs

  rest_api_id = aws_api_gateway_rest_api.example_api.id
  resource_id = aws_api_gateway_resource.operation_resource[each.key].id
  http_method = aws_api_gateway_method.operation_method[each.key].http_method
  status_code = "200"

  response_models = {
    "application/json" = "Empty"
  }
}

resource "aws_api_gateway_integration_response" "operation_integration_response_200" {
  for_each = local.operation_funcs

  rest_api_id = aws_api_gateway_rest_api.example_api.id
  resource_id = aws_api_gateway_resource.operation_resource[each.key].id
  http_method = aws_api_gateway_method.operation_method[each.key].http_method
  status_code = aws_api_gateway_method_response.operation_method_response_200[each.key].status_code

  response_templates = {
    "application/json" = <<EOF
{
  "message": "Mock response for ${each.key}"
}
EOF
  }
}

resource "aws_api_gateway_deployment" "example_deployment" {
  rest_api_id = aws_api_gateway_rest_api.example_api.id

  triggers = {
    redeployment_resource             = sha1(jsonencode(values(aws_api_gateway_resource.operation_resource)))
    redeployment_method               = sha1(jsonencode(values(aws_api_gateway_method.operation_method)))
    redeployment_integration          = sha1(jsonencode(values(aws_api_gateway_integration.operation_integration)))
    redeployment_method_response      = sha1(jsonencode(values(aws_api_gateway_method_response.operation_method_response_200)))
    redeployment_integration_response = sha1(jsonencode(values(aws_api_gateway_integration_response.operation_integration_response_200)))
    redeployment_policy               = sha1(local.ip_restriction_policy)
  }

  lifecycle {
    create_before_destroy = true
  }
}

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

output "api_endpoint" {
  value = aws_api_gateway_stage.example.invoke_url
}

所感

  • まあなんか使えそう
  • ガッツリこれで制限するっていうよりかは、テスト時の不正なリクエストを弾くのが目的かな。本番だったらWAFのほうが良さそう


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

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

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