AWS CloudFrontの前段にWAFを導入する

こんにちは!開発本部 SINIS for X 開発チームの西野(@fingerEase24)です。

今回はAWSのCloudFrontの前段にWAFを導入する作業を行うにあたっての流れや躓きポイントについて説明します。

なお、インフラ環境についてはTerraformによるIaC管理を行っています。

想定読者

  • AWSでのインフラ構築経験がある方
  • TerraformやCloudFormationなどによるIaC管理を行ったことがある方

なぜWAFの導入に至ったか

SINIS for X チームでは週次のSRE活動を行っており、サービスの状態を継続的に監視して健全性を保つことに努めています。

その中で定期的にALBへのリクエストがスパイクしていることが確認されており、リクエストログを解析したところ、これが攻撃的なアクセスであることが判明しました。

リクエストは全て失敗しており直接的な被害は受けていなかったものの、余計なリクエストによる負荷の増大も含め、今後実害が出る前に防御策を講じておこうということでCloudFrontの導入と併せてWAF v2の導入に至りました。

CloudFrontの導入については以下の記事で解説していますので、ぜひそちらもご覧ください!

techblog.tetemarche.co.jp

導入手順

導入にあたって、以下の手順で作業を行いました。

  1. 全てのルールについてActionをCountにしてWeb ACLを作成
  2. ログ出力設定
  3. 定期的にログを確認しつつルールを調整
  4. ActionをCountからBlockに変更して実運用開始

順に解説していきます。

全てのルールについてActionをCountにしてWeb ACLを作成

最終的にはWAFに設定したルールに検知したリクエストについてはBlockする設定で運用を行いますが、導入段階では、設定したルールが誤検知を起こし、正常なリクエストをブロックしないようにActionをCountに設定します。

CloudFront前段のWAFはバージニア北部リージョンに配置する必要があるため、 us-east-1 のproviderを定義しておきます。

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

弊サービスではまず以下のようにマネージドルールを設定しました(一部抜粋)。

resource "aws_wafv2_web_acl" "example_web_acl" {
  name     = "example-web-acl"
  scope    = "CLOUDFRONT"
  provider = aws.us_east_1

  default_action {
    allow {}
  }

  rule {
    name     = "AWSManagedRulesAmazonIpReputationList"
    priority = 10

    override_action {
      count {}
    }

    statement {
      managed_rule_group_statement {
        # https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-amazon
        name        = "AWSManagedRulesAmazonIpReputationList"
        vendor_name = "AWS"
      }
    }

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

  rule {
    name     = "AWSManagedRulesAnonymousIpList"
    priority = 20

    override_action {
      count {}
    }

    statement {
      managed_rule_group_statement {
        # https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-anonymous
        name        = "AWSManagedRulesAnonymousIpList"
        vendor_name = "AWS"
      }
    }

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

  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 30

    override_action {
      count {}
    }

    statement {
      managed_rule_group_statement {
        # https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

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

  (省略)

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "example-web-acl"
    sampled_requests_enabled   = true
  }
}

作成したWeb ACLをCloudFrontに設定します。

resource "aws_cloudfront_distribution" "example_cloudfront" {
  origin {
    domain_name = aws_lb.example_api.dns_name
    origin_id   = aws_lb.example_api.dns_name

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

  enabled         = true
  is_ipv6_enabled = true
- aliases         = ["api.example.com"]
+ aliases         = ["api.example.com"]
+ web_acl_id      = aws_wafv2_web_acl.example_web_acl.arn

  (省略)

ログ出力設定

WAFのログ解析できるように、ログの出力設定を行います。

ログ出力用のS3バケットを作成します。

resource "aws_s3_bucket" "example_aws_waf_logs" {
  bucket = "aws-waf-logs-example"
}

resource "aws_s3_bucket_ownership_controls" "example_aws_waf_logs" {
  bucket = aws_s3_bucket.example_aws_waf_logs.id
  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

resource "aws_s3_bucket_public_access_block" "example_aws_waf_logs" {
  bucket = aws_s3_bucket.example_aws_waf_logs.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Web ACLにログ出力設定のリソースを追加します。

resource "aws_wafv2_web_acl_logging_configuration" "example_web_acl_logging" {
  resource_arn            = aws_wafv2_web_acl.example_web_acl.arn
  log_destination_configs = [aws_s3_bucket.example_aws_waf_logs.arn]
  provider                = aws.us_east_1
}

定期的にログを確認しつつルールを調整

設定したルールで適切に不正なリクエストの検知が行えているか、定期的に確認を行います。

ログ解析には今回もAthenaを利用しました。

ログ用テーブルの作成は以下の公式記事を参考にしました。

docs.aws.amazon.com

以下のクエリで直近1週間でCountになったリクエストを集計できます。

SELECT
    from_unixtime(timestamp / 1000, 'Asia/Tokyo') AS JST,
    labels AS "引っかかったrules",
    httprequest.clientip,
    httprequest.country,
    httprequest.uri,
    httprequest.args,
    httprequest.httpmethod
FROM default.waf_logs,
    UNNEST(nonterminatingmatchingrules) AS t(countedrule),
    UNNEST(rulegrouplist) AS t(rg)
WHERE
    date >= format_datetime(date_add('day', -7, current_timestamp), 'YYYY/MM/dd')
    AND countedrule.action = 'COUNT'
    AND (rg.terminatingrule IS NOT NULL AND rg.terminatingrule.action <> 'ALLOW') -- COUNTを外したらBLOCKされるリクエスト
ORDER BY JST DESC, clientip, httpmethod, uri
LIMIT 100

この際に、正常なリクエストが誤検知されていそうであれば対象のルールについて見直しを行います。

例えば、弊サービスではファイルのアップロードを伴うリクエストについて AWSManagedRulesCommonRuleSet マネージドルールの特定のルールに誤検知されていることが確認されたので以下のように例外を設定しました。

rule {
  name     = "AWSManagedRulesCommonRuleSet"
  priority = 30

  override_action {
    count {}
  }

  statement {
    managed_rule_group_statement {
      # https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs
      name        = "AWSManagedRulesCommonRuleSet"
      vendor_name = "AWS"
+
+     rule_action_override {
+       name = "SizeRestrictions_Cookie_HEADER"
+       action_to_use {
+         allow {}
+       }
+     }
+
+     rule_action_override {
+       name = "SizeRestrictions_BODY"
+       action_to_use {
+         allow {}
+       }
+     }
+
+     rule_action_override {
+       name = "GenericLFI_BODY"
+       action_to_use {
+         allow {}
+       }
+     }
+
+     rule_action_override {
+       name = "CrossSiteScripting_BODY"
+       action_to_use {
+         allow {}
+       }
+     }
+   }
  }

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

マネージドルールのよくある誤検知パターンについては以下の記事が参考になりました。

mazyu36.hatenablog.com

ActionをCountからBlockに変更して実運用開始

一定期間様子を見てルールの調整を行いつつ、問題がなさそうであれば検知されたリクエストを実際に弾くためにActionをCountからBlockに切り替えます。

ここでは各ルールの override_action について、 count から none に変更するだけでOKです。

rule {
  name     = "AWSManagedRulesAmazonIpReputationList"
  priority = 10

  override_action {
-   count {}
+   none {}
  }

(省略)

その後も引き続き定期的なログ解析を行い、意図したリクエストがBlockされていることを確認します。

以下のAthenaクエリで解析を行います。

SELECT
  from_unixtime(MIN(timestamp) / 1000, 'Asia/Tokyo') AS JST,
  ANY_VALUE(httprequest.clientip) AS clientip,
  ANY_VALUE(httprequest.country) AS country,
  ANY_VALUE(httprequest.uri) AS uri,
  ANY_VALUE(httprequest.args) AS args,
  ANY_VALUE(httprequest.httpmethod) AS httpmethod,
  array_join(array_agg(DISTINCT rules.terminatingrule.ruleid), ', ') AS rule_info
FROM default.waf_logs,
  UNNEST(rulegrouplist) AS t(rules)
WHERE
  date >= format_datetime(date_add('day', -7, current_timestamp), 'YYYY/MM/dd/HH')
  AND action = 'BLOCK'
GROUP BY httprequest.requestid
ORDER BY JST DESC
LIMIT 100;

ユーザーエージェントから悪質なBotと判定されたリクエストや、AWSが評価した不審なIPアドレスからのリクエストがBlockされていることが確認できました。

まとめ

今回の記事では、AWSのCloudFront前段にWAFを導入する手順について解説しました。

サービスの規模にもよりますが、月数十ドルで手軽にセキュリティの向上がAWSによって担保されるため、個人的にも非常におすすめのサービスです!


テテマーチでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! herp.careers

エンジニアチームガイドはこちら! tetemarche01.notion.site