こんにちは!開発本部 SINIS for X 開発チームの西野(@fingerEase24)です。
今回はAWSのCloudFrontの前段にWAFを導入する作業を行うにあたっての流れや躓きポイントについて説明します。
なお、インフラ環境についてはTerraformによるIaC管理を行っています。
想定読者
- AWSでのインフラ構築経験がある方
- TerraformやCloudFormationなどによるIaC管理を行ったことがある方
なぜWAFの導入に至ったか
SINIS for X チームでは週次のSRE活動を行っており、サービスの状態を継続的に監視して健全性を保つことに努めています。
その中で定期的にALBへのリクエストがスパイクしていることが確認されており、リクエストログを解析したところ、これが攻撃的なアクセスであることが判明しました。
リクエストは全て失敗しており直接的な被害は受けていなかったものの、余計なリクエストによる負荷の増大も含め、今後実害が出る前に防御策を講じておこうということでCloudFrontの導入と併せてWAF v2の導入に至りました。
CloudFrontの導入については以下の記事で解説していますので、ぜひそちらもご覧ください!
導入手順
導入にあたって、以下の手順で作業を行いました。
- 全てのルールについてActionをCountにしてWeb ACLを作成
- ログ出力設定
- 定期的にログを確認しつつルールを調整
- 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を利用しました。
ログ用テーブルの作成は以下の公式記事を参考にしました。
以下のクエリで直近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 } }
マネージドルールのよくある誤検知パターンについては以下の記事が参考になりました。
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