あめがえるのITブログ

頑張りすぎない。ほどほどに頑張るブログ。

AWS CloudFront+APIGateway(WAF)でカスタムヘッダーで接続元を制限してみた

CloudFront+APIGatewayの構成で送信元をCloudFrontのみに制限する場合に、APIGatewayにWAFを設定し、カスタムヘッダーで制限するのが一般的らしいのでやってみた。

やること

terraformで下記を実施。
CloudFront+APIGatewayを作り、APIGatewayにWAFをアタッチ
CloudFrontにカスタムヘッダーを設定
WAFにカスタムヘッダーを含まないHTTPリクエストはブロックするルールを設定

構成



実践!

1.環境作成
1-1.下記を実行し、コードを作成

# vi main.tf
provider "aws" {
  region = "ap-northeast-1"
}

## Lambda関数
resource "aws_lambda_function" "example" {
  function_name = "example_lambda_function"
  runtime       = "python3.8"
  handler       = "lambda_function.lambda_handler"

  filename = "lambda_function.zip"

  role = aws_iam_role.lambda_exec.arn
}

## Lambdaロール
resource "aws_iam_role" "lambda_exec" {
  name = "lambda_exec_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
      },
    ],
  })
}

# WAF
resource "aws_wafv2_web_acl" "example" {
  name        = "example-acl"
  description = "An example ACL"
  scope       = "REGIONAL"

  default_action {
    allow {}
  }

  rule {
    name     = "BlockMissingHeader"
    priority = 0  # 優先度を高く設定

    action {
      block {}
    }

    statement {
      not_statement {
        statement {
          byte_match_statement {
            field_to_match {
              single_header {
                name = "testheader"
              }
            }
            positional_constraint = "EXACTLY"
            search_string         = "testvalue"
            text_transformation {
              priority = 0
              type     = "NONE"
            }
          }
        }
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "BlockMissingHeader"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = false
    metric_name                = "example-acl"
    sampled_requests_enabled   = false
  }
}

## APIGateway
resource "aws_api_gateway_rest_api" "example" {
  name        = "ExampleAPI"
  description = "Example API"
}

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

resource "aws_api_gateway_method" "example" {
  rest_api_id   = aws_api_gateway_rest_api.example.id
  resource_id   = aws_api_gateway_resource.example.id
  http_method   = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "example" {
  rest_api_id = aws_api_gateway_rest_api.example.id
  resource_id = aws_api_gateway_resource.example.id
  http_method = aws_api_gateway_method.example.http_method

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.example.invoke_arn
}

resource "aws_api_gateway_deployment" "example" {
  depends_on = [aws_api_gateway_integration.example]

  rest_api_id = aws_api_gateway_rest_api.example.id
  # stage_name  = "test"
}

resource "aws_api_gateway_stage" "example_stage" {
  stage_name    = "test"
  rest_api_id   = aws_api_gateway_rest_api.example.id
  deployment_id = aws_api_gateway_deployment.example.id

  xray_tracing_enabled = false
}

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

  source_arn = "${aws_api_gateway_rest_api.example.execution_arn}/*/*/*"
}

# APIGatewayにWAFを関連付け
resource "aws_wafv2_web_acl_association" "example_association" {
  resource_arn = aws_api_gateway_stage.example_stage.arn
  web_acl_arn  = aws_wafv2_web_acl.example.arn
}

# CloudFront
resource "aws_cloudfront_distribution" "example" {
  origin {
    ## domain_name = aws_api_gateway_rest_api.example.domain_name
    ## domain_name = "${aws_api_gateway_rest_api.example.id}.execute-api.ap-northeast-1.amazonaws.com"
    domain_name = "${aws_api_gateway_rest_api.example.id}.execute-api.ap-northeast-1.amazonaws.com"
    origin_id   = "APIGatewayOrigin"

    custom_header {
      name  = "testheader"
      value = "testvalue"
    }

    custom_origin_config {
      http_port                = 80
      https_port               = 443
      origin_protocol_policy   = "http-only"
      origin_ssl_protocols     = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  enabled             = true
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "APIGatewayOrigin"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  ordered_cache_behavior {
    path_pattern     = "/test/*"  # ステージ名をビヘイビアのパスパターンに追加
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "APIGatewayOrigin"

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"  # HTTPSを強制
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_100"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

1-2.terraform実行

# terraform plan
# terraform apply


2.動作確認(CloudFront)
2-1.[CloudFront] - ディストリビューションドメイン名確認

2-2.下記をAWS外部から実行し、200OKが返ってくることを確認

> curl https://d15xfm2x3yiw75.cloudfront.net/test/examplepath -L -v

*   Trying 143.204.130.97:443...
* Connected to d15xfm2x3yiw75.cloudfront.net (143.204.130.97) port 443
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* using HTTP/1.1
> GET /test/examplepath HTTP/1.1
> Host: d15xfm2x3yiw75.cloudfront.net
> User-Agent: curl/8.4.0
> Accept: */*
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 20
< Connection: keep-alive
< Date: Thu, 23 Nov 2023 23:40:41 GMT
< x-amzn-RequestId: eed3548c-6b0d-432d-9016-7cfb7b5ea3e7
< x-amz-apigw-id: O4BnAH3xNjMEHog=
< X-Amzn-Trace-Id: Root=1-655fe2f9-37eab6833bcb060519da7ff4
< Via: 1.1 0f1561546531d4bd49ef6c69e8989712.cloudfront.net (CloudFront), 1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)
< X-Amz-Cf-Pop: SFO5-C3
< X-Cache: Hit from cloudfront
< X-Amz-Cf-Pop: SFO5-C1
< X-Amz-Cf-Id: DWcy_JqNhUQMEn3O-rKtfPbE_qs4R8PDpEjsaFh0a-JHJ7Mp3TQBLw==
< Age: 102
<
"Hello from Lambda!"* Connection #0 to host d15xfm2x3yiw75.cloudfront.net left intact


3.動作確認(APIGateway)
3-1.[API Gateway] - 作成したAPIを選択

3-2. [ステージ]

3-3.[GET]を選択し呼び出しURLを確認

3-4.下記をAWS外部から実行し、403 Forbiddenが返ってくることを確認

> curl https://i7yfsdlxxk.execute-api.ap-northeast-1.amazonaws.com/tes
t/examplepath -L -v

*   Trying 99.84.133.47:443...
* Connected to i7yfsdlxxk.execute-api.ap-northeast-1.amazonaws.com (99.84.133.47) port 443
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* using HTTP/1.1
> GET /test/examplepath HTTP/1.1
> Host: i7yfsdlxxk.execute-api.ap-northeast-1.amazonaws.com
> User-Agent: curl/8.4.0
> Accept: */*
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
< HTTP/1.1 403 Forbidden
< Content-Type: application/json
< Content-Length: 23
< Connection: keep-alive
< Date: Fri, 24 Nov 2023 00:09:38 GMT
< x-amzn-RequestId: 7f69ce70-ae7c-4d44-8a9b-bf4d7d681b47
< x-amzn-ErrorType: ForbiddenException
< x-amz-apigw-id: O4F2aFRQtjMEKBQ=
< X-Cache: Error from cloudfront
< Via: 1.1 591400b2958a6516fdef3d2bc0ac208e.cloudfront.net (CloudFront)
< X-Amz-Cf-Pop: NRT57-C3
< X-Amz-Cf-Id: JbsXNRIGw7dqYD_HoBqzykfb69mwmCA5X_kZXxvNP4wI2hYTL8h2dA==
<
{"message":"Forbidden"}* Connection #0 to host i7yfsdlxxk.execute-api.ap-northeast-1.amazonaws.com left intact



感想

簡単な設定なはずなのになかなかに苦戦した・・・( ̄д ̄)ハァ