<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alexey Baltacov</title>
    <description>The latest articles on DEV Community by Alexey Baltacov (@alexeybaltacov).</description>
    <link>https://dev.to/alexeybaltacov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2826933%2Fab385ce6-4e2c-4759-a55c-4989e5e307cf.png</url>
      <title>DEV Community: Alexey Baltacov</title>
      <link>https://dev.to/alexeybaltacov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexeybaltacov"/>
    <language>en</language>
    <item>
      <title>Putting Amazon Cognito Behind CloudFront Without Making the Origin the Public Path</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:41:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/putting-amazon-cognito-behind-cloudfront-without-making-the-origin-the-public-path-5cij</link>
      <guid>https://dev.to/aws-builders/putting-amazon-cognito-behind-cloudfront-without-making-the-origin-the-public-path-5cij</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rk6raaqxcasyvpu1sli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rk6raaqxcasyvpu1sli.png" alt="CloudFront and WAF protecting a Cognito hosted UI behind a corporate auth domain" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Amazon Cognito hosted UI is convenient: you get OAuth 2.0 endpoints, OIDC discovery, managed login pages, token endpoints, logout endpoints, and federation plumbing without running your own identity provider.&lt;/p&gt;

&lt;p&gt;But the default shape has an awkward property: users and clients talk directly to the Cognito domain.&lt;/p&gt;

&lt;p&gt;For corporate applications, you may want the public entry point to be a company-owned domain on your own CloudFront distribution instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://auth.mycorpdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mycorp-auth.auth.us-east-1.amazoncognito.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is not only cosmetic. During the whole login conversation, users stay on a domain your company owns. The auth flow can be embedded into the corporate site or launched from the application UI without making users feel like they left your environment and landed on an AWS-owned hostname.&lt;/p&gt;

&lt;p&gt;It also gives you the CloudFront edge advantages around the login surface without pretending auth traffic is a caching problem. Users connect to a local AWS edge presence, TLS terminates closer to them, time to first byte can improve for edge-handled responses such as redirects and blocks, and AWS can carry traffic across its own network toward the origin. At the same edge, a CloudFront-scope AWS WAF Web ACL can apply DDoS protection, login protection, rate limits, bot controls, IP reputation, managed rule groups, custom rules, and simple route handling such as &lt;code&gt;/login&lt;/code&gt;, keeping most abusive traffic from ever reaching the Cognito-hosted UI or the regional WAF associated with the user pool.&lt;/p&gt;

&lt;p&gt;This article walks through a practical version of that pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The target shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser / OIDC client
        |
        v
CloudFront distribution: auth.mycorpdomain.com
        |
        | adds X-Edge-Origin-Secret
        v
Cognito hosted UI domain
        |
        v
User pool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojwccxl68wkpyjynudq0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojwccxl68wkpyjynudq0.png" alt="Architecture diagram for protecting Cognito behind CloudFront and WAF" width="800" height="489"&gt;&lt;/a&gt;&lt;br&gt;
There are two WAF layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A CloudFront-scope Web ACL attached to the CloudFront distribution.&lt;/li&gt;
&lt;li&gt;A regional Web ACL associated with the Cognito user pool that only allows requests carrying a secret header added by CloudFront.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also one small edge function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CloudFront Function on viewer response rewrites Cognito &lt;code&gt;Location&lt;/code&gt; headers from the Cognito origin domain back to &lt;code&gt;auth.mycorpdomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one static object:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A recreated &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; document served from S3 or another static origin, because OIDC clients need discovery metadata that points at the public CloudFront hostname.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most user-facing login protection should happen at the CloudFront layer: DDoS protection, login-specific protection, rate limits, path controls, IP reputation, bot controls, geographic policies, managed rule groups, custom rules, and any cheap static redirect for &lt;code&gt;/login&lt;/code&gt;. This is separate from caching. For auth traffic, cache hit ratio is not the point. The value is local edge presence, fast edge decisions, AWS network acceleration toward the origin, and stopping bad traffic before it reaches the Cognito path. The regional Cognito WAF remains important, but its main job in this pattern is origin-bypass protection. It should reject direct requests that did not come through CloudFront.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not only use CloudFront?
&lt;/h2&gt;

&lt;p&gt;CloudFront can add a custom header to origin requests. That is useful, but it does not stop someone from directly calling the Cognito domain unless the origin also enforces the header.&lt;/p&gt;

&lt;p&gt;With many AWS origins, you can make the origin trust CloudFront through a native origin access feature or a resource policy. S3 has Origin Access Control. Some services can restrict access with IAM or resource policies, VPC-only paths, security groups, or other service-native controls.&lt;/p&gt;

&lt;p&gt;Cognito hosted UI does not give you that kind of origin access control. You cannot attach application code to the hosted UI, and you cannot write a resource policy that says "only this CloudFront distribution may call this Cognito domain."&lt;/p&gt;

&lt;p&gt;The practical way to create one-way stickiness from the public CloudFront hostname to Cognito is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront adds &lt;code&gt;X-Edge-Origin-Secret: &amp;lt;random value&amp;gt;&lt;/code&gt; on origin requests sent to Cognito.&lt;/li&gt;
&lt;li&gt;A regional AWS WAF Web ACL on the Cognito user pool allows requests with that exact header.&lt;/li&gt;
&lt;li&gt;Direct calls to the Cognito hosted UI domain do not have the header, so the regional WAF blocks them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 1: create the secret once
&lt;/h2&gt;

&lt;p&gt;Use SSM Parameter Store for the shared secret header value. Set and rotate the value through your secret-management process, not in Terraform source.&lt;/p&gt;

&lt;p&gt;Create or rotate the value outside Terraform, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm put-parameter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; /mycorp/auth/origin-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; SecureString &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 48&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--overwrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform should only read the current value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_parameter"&lt;/span&gt; &lt;span class="s2"&gt;"edge_origin_secret"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/mycorp/auth/origin-secret"&lt;/span&gt;
  &lt;span class="nx"&gt;with_decryption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not put the real header value, a placeholder value, or a managed &lt;code&gt;aws_ssm_parameter&lt;/code&gt; value in the Terraform module. If Terraform owns the value, it is too easy to leak it into source, state workflows, review systems, or logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: configure CloudFront origin headers
&lt;/h2&gt;

&lt;p&gt;Use the Cognito hosted UI domain as a custom origin.&lt;/p&gt;

&lt;p&gt;Example origin values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudFront alias: auth.mycorpdomain.com
Cognito origin:  mycorp-auth.auth.us-east-1.amazoncognito.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform sketch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_distribution"&lt;/span&gt; &lt;span class="s2"&gt;"auth"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;enabled&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;is_ipv6_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;aliases&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"auth.mycorpdomain.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cognito-hosted-ui"&lt;/span&gt;
    &lt;span class="nx"&gt;domain_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth.auth.us-east-1.amazoncognito.com"&lt;/span&gt;

    &lt;span class="nx"&gt;custom_origin_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;http_port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
      &lt;span class="nx"&gt;https_port&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
      &lt;span class="nx"&gt;origin_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https-only"&lt;/span&gt;
      &lt;span class="nx"&gt;origin_ssl_protocols&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TLSv1.2"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;custom_header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"X-Edge-Origin-Secret"&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_origin_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;default_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;target_origin_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cognito-hosted-ui"&lt;/span&gt;
    &lt;span class="nx"&gt;viewer_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-to-https"&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"OPTIONS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"PUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"PATCH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"DELETE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cached_methods&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;cache_policy_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudfront_cache_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth_disabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;origin_request_policy_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudfront_origin_request_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth_all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

    &lt;span class="nx"&gt;function_association&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"viewer-response"&lt;/span&gt;
      &lt;span class="nx"&gt;function_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudfront_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rewrite_cognito_location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;web_acl_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_wafv2_web_acl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudfront_auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;viewer_certificate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;acm_certificate_arn&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_acm_certificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;ssl_support_method&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sni-only"&lt;/span&gt;
    &lt;span class="nx"&gt;minimum_protocol_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TLSv1.2_2021"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;restrictions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;geo_restriction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;restriction_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazon Cognito hosted UI is HTTPS. Keep &lt;code&gt;origin_protocol_policy = "https-only"&lt;/code&gt;. The &lt;code&gt;http_port&lt;/code&gt; argument appears in Terraform because CloudFront custom origins require the field, but this configuration does not send origin traffic to Cognito over HTTP.&lt;/p&gt;

&lt;p&gt;For auth endpoints, caching should usually be disabled or extremely conservative. OAuth requests carry query strings, cookies, state, nonce, code challenges, and other values that should not be collapsed into a shared cached object.&lt;/p&gt;

&lt;p&gt;Example cache policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_cache_policy"&lt;/span&gt; &lt;span class="s2"&gt;"auth_disabled"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-caching-disabled"&lt;/span&gt;
  &lt;span class="nx"&gt;default_ttl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;max_ttl&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;min_ttl&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;parameters_in_cache_key_and_forwarded_to_origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enable_accept_encoding_brotli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;enable_accept_encoding_gzip&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="nx"&gt;cookies_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cookie_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;headers_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;header_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;query_strings_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;query_string_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an origin request policy that forwards what Cognito needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_origin_request_policy"&lt;/span&gt; &lt;span class="s2"&gt;"auth_all"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-forward-all-viewer-values"&lt;/span&gt;

  &lt;span class="nx"&gt;cookies_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cookie_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"all"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;headers_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;header_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allViewer"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;query_strings_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;query_string_behavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"all"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review your own requirements before forwarding every header. This is a simple starting point for hosted UI compatibility, not a universal least-privilege policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: add a CloudFront WAF redirect for &lt;code&gt;/login&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Users like clean URLs. OAuth endpoints do not.&lt;/p&gt;

&lt;p&gt;You can make:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://auth.mycorpdomain.com/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;return a redirect to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://auth.mycorpdomain.com/oauth2/authorize?client_id=...&amp;amp;response_type=code&amp;amp;scope=openid+email+profile&amp;amp;redirect_uri=...&amp;amp;code_challenge=...&amp;amp;code_challenge_method=S256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the redirect target is static enough for your use case, AWS WAF custom responses can do this at the CloudFront Web ACL layer. WAF custom responses can use 3xx status codes and a &lt;code&gt;Location&lt;/code&gt; header. You can also implement the same redirect in a CloudFront Function, but WAF is cheaper for this case because the target remains static as long as the user pool and app client configuration remain unchanged.&lt;/p&gt;

&lt;p&gt;In production, make the redirect target depend on the current AWS state rather than a hand-copied URL. For example, Terraform can read the user pool client and construct the authorize URL from data sources, so each &lt;code&gt;terraform apply&lt;/code&gt; checks the actual app client and domain values.&lt;/p&gt;

&lt;p&gt;Terraform sketch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_wafv2_web_acl"&lt;/span&gt; &lt;span class="s2"&gt;"cloudfront_auth"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-cloudfront"&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CLOUDFRONT"&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-login"&lt;/span&gt;
    &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;custom_response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;response_code&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;

          &lt;span class="nx"&gt;response_header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Location"&lt;/span&gt;
            &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://auth.mycorpdomain.com/oauth2/authorize?client_id=CLIENT_ID&amp;amp;response_type=code&amp;amp;scope=openid+email+profile&amp;amp;redirect_uri=https%3A%2F%2Fapp.mycorpdomain.com%2Fcallback"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;byte_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;field_to_match&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;uri_path&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;positional_constraint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EXACTLY"&lt;/span&gt;
        &lt;span class="nx"&gt;search_string&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/login"&lt;/span&gt;

        &lt;span class="nx"&gt;text_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
          &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-login"&lt;/span&gt;
      &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-cloudfront"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks odd because the WAF action is technically &lt;code&gt;block&lt;/code&gt;, but the custom response is a 302. That is how AWS WAF custom responses work: the rule terminates evaluation and returns your response.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;/login&lt;/code&gt; redirect needs dynamic PKCE generation, dynamic state, tenant selection, or request-specific logic, use application code, a CloudFront Function, or Lambda@Edge instead. WAF redirects are best for simple static redirects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: rewrite Cognito &lt;code&gt;Location&lt;/code&gt; headers
&lt;/h2&gt;

&lt;p&gt;Cognito will often produce redirects that point back at the Cognito hosted UI domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Location: https://mycorp-auth.auth.us-east-1.amazoncognito.com/oauth2/...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If clients enter through CloudFront, keep them there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Location: https://auth.mycorpdomain.com/oauth2/...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudFront Functions can run on viewer response and modify response headers before the response is returned.&lt;/p&gt;

&lt;p&gt;Function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;publicHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth.mycorpdomain.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cognitoHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mycorp-auth.auth.us-east-1.amazoncognito.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cognitoHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;publicHost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_function"&lt;/span&gt; &lt;span class="s2"&gt;"rewrite_cognito_location"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-rewrite-cognito-location"&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudfront-js-2.0"&lt;/span&gt;
  &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Rewrite Cognito hosted UI redirects to the public auth domain"&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;code&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rewrite-cognito-location.js"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep this function intentionally small. It should not parse tokens, make authorization decisions, or become an identity service. Its job is only to keep redirects on the public hostname.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: protect Cognito with a regional WAF
&lt;/h2&gt;

&lt;p&gt;Now attach a regional Web ACL to the Cognito user pool and allow only requests with the secret header.&lt;/p&gt;

&lt;p&gt;The Web ACL should live in the same region as the user pool.&lt;/p&gt;

&lt;p&gt;Keep the responsibility split clear: DDoS protection, login protection, rate limits, bot controls, managed rule groups, custom rules, and other broad controls should run on the CloudFront Web ACL. That lets CloudFront and AWS edge infrastructure handle hostile traffic before it reaches the Cognito service path. The regional Cognito WAF is the final origin-bypass control, not the place where you want internet-scale login floods to arrive first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_wafv2_web_acl"&lt;/span&gt; &lt;span class="s2"&gt;"cognito_origin"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-cognito-origin"&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"REGIONAL"&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-cloudfront-origin-secret"&lt;/span&gt;
    &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;byte_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;field_to_match&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;single_header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x-edge-origin-secret"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;positional_constraint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EXACTLY"&lt;/span&gt;
        &lt;span class="nx"&gt;search_string&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_origin_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

        &lt;span class="nx"&gt;text_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
          &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-cloudfront-origin-secret"&lt;/span&gt;
      &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mycorp-auth-cognito-origin"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Associate it with the user pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_wafv2_web_acl_association"&lt;/span&gt; &lt;span class="s2"&gt;"cognito"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resource_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cognito_user_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;web_acl_arn&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_wafv2_web_acl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cognito_origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test both paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://auth.mycorpdomain.com/oauth2/authorize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should reach Cognito through CloudFront, while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://mycorp-auth.auth.us-east-1.amazoncognito.com/oauth2/authorize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should be blocked by the regional Web ACL because the secret header is missing.&lt;/p&gt;

&lt;p&gt;Do not use a guessable value. The header is not a replacement for OAuth security. It is an origin-bypass control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: recreate &lt;code&gt;openid-configuration&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;OIDC clients discover provider metadata from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/openid-configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cognito user pools, the AWS-owned discovery document normally lives under the issuer host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE/.well-known/openid-configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if clients are configured to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://auth.mycorpdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then discovery must return endpoints that send clients to &lt;code&gt;auth.mycorpdomain.com&lt;/code&gt;, not the raw Cognito hosted UI domain.&lt;/p&gt;

&lt;p&gt;This is not the same as hiding every Cognito identifier. OIDC clients need a real issuer and signing keys. The discovery document, &lt;code&gt;jwks_uri&lt;/code&gt;, and ID token &lt;code&gt;iss&lt;/code&gt; claim can still expose the Cognito issuer host, region, and user pool ID. That is normal for Cognito token validation.&lt;/p&gt;

&lt;p&gt;The goal is narrower and more practical: keep the Cognito hosted UI domain out of the browser routing path, keep OAuth endpoints on the company-owned CloudFront hostname, and block direct use of the hosted UI origin with the regional WAF.&lt;/p&gt;

&lt;p&gt;One practical approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the original Cognito discovery document.&lt;/li&gt;
&lt;li&gt;Create a modified copy.&lt;/li&gt;
&lt;li&gt;Serve it from S3 behind the same CloudFront distribution at &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example original shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mycorp-auth.auth.us-east-1.amazoncognito.com/oauth2/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mycorp-auth.auth.us-east-1.amazoncognito.com/oauth2/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userinfo_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mycorp-auth.auth.us-east-1.amazoncognito.com/oauth2/userInfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end_session_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mycorp-auth.auth.us-east-1.amazoncognito.com/logout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jwks_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE/.well-known/jwks.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id_token_signing_alg_values_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"client_secret_basic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client_secret_post"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claims_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone_number_verified"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modified public copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://auth.mycorpdomain.com/oauth2/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://auth.mycorpdomain.com/oauth2/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userinfo_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://auth.mycorpdomain.com/oauth2/userInfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end_session_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://auth.mycorpdomain.com/logout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jwks_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE/.well-known/jwks.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id_token_signing_alg_values_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"client_secret_basic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client_secret_post"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claims_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email_verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone_number_verified"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;authorization_endpoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;token_endpoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;userinfo_endpoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;end_session_endpoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Any other OAuth endpoint value that uses the Cognito hosted UI domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What not to replace blindly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;issuer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jwks_uri&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Supported algorithms&lt;/li&gt;
&lt;li&gt;Supported claims&lt;/li&gt;
&lt;li&gt;Supported scopes&lt;/li&gt;
&lt;li&gt;Supported auth methods&lt;/li&gt;
&lt;li&gt;Any fields that describe token validation semantics rather than browser routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;issuer&lt;/code&gt; is especially important. Cognito tokens are issued by the Cognito user pool issuer, commonly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cognito-idp.&amp;lt;region&amp;gt;.amazonaws.com/&amp;lt;user_pool_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you change the issuer in discovery but Cognito still signs tokens with the original issuer claim, strict OIDC clients can reject the tokens. Keep issuer behavior aligned with the actual tokens your user pool emits.&lt;/p&gt;

&lt;p&gt;So yes, &lt;code&gt;issuer&lt;/code&gt; and &lt;code&gt;jwks_uri&lt;/code&gt; intentionally reveal that Cognito is the token issuer. They should not reveal the hosted UI origin as the normal authorization, token, userinfo, or logout endpoint after you rewrite those endpoint values, but they do reveal the Cognito user pool identity. Treat this pattern as origin-bypass prevention and corporate-domain continuity, not complete origin secrecy.&lt;/p&gt;

&lt;p&gt;Serve the file from S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_object"&lt;/span&gt; &lt;span class="s2"&gt;"openid_configuration"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth_static&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;".well-known/openid-configuration"&lt;/span&gt;
  &lt;span class="nx"&gt;content_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"openid-configuration.json"&lt;/span&gt;
  &lt;span class="nx"&gt;etag&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filemd5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"openid-configuration.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a CloudFront behavior for the discovery path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;ordered_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path_pattern&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/.well-known/openid-configuration"&lt;/span&gt;
  &lt;span class="nx"&gt;target_origin_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"auth-static"&lt;/span&gt;
  &lt;span class="nx"&gt;viewer_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-to-https"&lt;/span&gt;
  &lt;span class="nx"&gt;allowed_methods&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;cached_methods&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;cache_policy_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudfront_cache_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discovery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JWKS document can usually remain on the Cognito issuer host. Clients validating JWTs should follow &lt;code&gt;jwks_uri&lt;/code&gt; and use the keys from the actual issuer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: route CloudFront behaviors carefully
&lt;/h2&gt;

&lt;p&gt;A useful distribution often has at least two origins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Origin 1: Cognito hosted UI domain
Origin 2: S3 bucket for static discovery overrides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behaviors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/openid-configuration -&amp;gt; S3 static origin
/*                                  -&amp;gt; Cognito origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might also choose to serve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/jwks.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;through CloudFront as a proxy or static copy, but do that carefully. Stale JWKS can break token validation during key rotation. In many cases, leaving &lt;code&gt;jwks_uri&lt;/code&gt; pointed at the Cognito issuer is simpler and safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: monitor the bypass control
&lt;/h2&gt;

&lt;p&gt;The regional Cognito WAF should have very boring metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allowed requests with the secret header.&lt;/li&gt;
&lt;li&gt;Blocked direct requests without the secret header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create alarms for unexpected spikes in blocked direct traffic. They may indicate scanners, old clients, bad documentation, or someone using the origin URL directly.&lt;/p&gt;

&lt;p&gt;Also log enough CloudFront and WAF data to debug OAuth flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront standard or real-time logs&lt;/li&gt;
&lt;li&gt;AWS WAF logs for the CloudFront Web ACL&lt;/li&gt;
&lt;li&gt;AWS WAF logs for the Cognito-origin Web ACL&lt;/li&gt;
&lt;li&gt;Cognito user pool logs where applicable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be careful with auth logs. They can contain sensitive query strings such as OAuth &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt;, or provider-specific values. Set retention and access controls accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;p&gt;The first common failure is forgetting response header rewrites. Login starts at the pretty domain, then one redirect sends the browser back to the Cognito domain.&lt;/p&gt;

&lt;p&gt;The second is changing the OIDC &lt;code&gt;issuer&lt;/code&gt; in discovery without changing the actual token issuer. Many clients validate that &lt;code&gt;issuer&lt;/code&gt; in discovery matches the &lt;code&gt;iss&lt;/code&gt; claim in the token.&lt;/p&gt;

&lt;p&gt;The third is caching auth responses. Do not cache OAuth redirects, token responses, or user-specific hosted UI pages unless you deeply understand the cache key.&lt;/p&gt;

&lt;p&gt;The fourth is rotating the origin secret in only one place. CloudFront and the regional WAF must agree on the value.&lt;/p&gt;

&lt;p&gt;The fifth is assuming the secret header is a complete security boundary. It is only a way to prevent ordinary direct-origin bypass. OAuth client configuration, redirect URI validation, PKCE, token validation, TLS, WAF rules, and monitoring still matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront alias is the public auth hostname.&lt;/li&gt;
&lt;li&gt;CloudFront forwards required query strings, cookies, and headers to Cognito.&lt;/li&gt;
&lt;li&gt;CloudFront adds a high-entropy custom origin header.&lt;/li&gt;
&lt;li&gt;CloudFront WAF handles edge controls such as &lt;code&gt;/login&lt;/code&gt; redirect, DDoS protection, login protection, bot controls, rate limits, managed rule groups, and custom rules.&lt;/li&gt;
&lt;li&gt;CloudFront Function rewrites Cognito &lt;code&gt;Location&lt;/code&gt; headers to the public hostname.&lt;/li&gt;
&lt;li&gt;Regional WAF is associated with the Cognito user pool.&lt;/li&gt;
&lt;li&gt;Regional WAF default action blocks.&lt;/li&gt;
&lt;li&gt;Regional WAF allow rule matches the CloudFront secret header.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; is served from the public hostname.&lt;/li&gt;
&lt;li&gt;Discovery OAuth endpoints point to the public hostname.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;issuer&lt;/code&gt; and &lt;code&gt;jwks_uri&lt;/code&gt; remain compatible with the tokens Cognito actually issues.&lt;/li&gt;
&lt;li&gt;Direct requests to the Cognito hosted UI domain are blocked.&lt;/li&gt;
&lt;li&gt;OAuth flows are tested end to end with a real OIDC client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is not about hiding identity infrastructure as a security trick. OIDC metadata and tokens still identify Cognito as the issuer. It is about making one public auth edge, putting controls there, keeping the hosted UI origin out of the normal browser path, and making direct-origin access boringly unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront custom origin headers: &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CloudFront Functions: &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cognito user pool OIDC endpoints: &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS WAF custom responses and redirects: &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-for-blocked-requests.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-for-blocked-requests.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cognito</category>
      <category>cloudfront</category>
      <category>security</category>
    </item>
    <item>
      <title>Best Practices for Managing AWS WAF with Code</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Thu, 16 Jul 2026 17:57:38 +0000</pubDate>
      <link>https://dev.to/aws-builders/best-practices-for-managing-aws-waf-with-code-3efl</link>
      <guid>https://dev.to/aws-builders/best-practices-for-managing-aws-waf-with-code-3efl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs23tx46r7kozbnleo5ls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs23tx46r7kozbnleo5ls.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
AWS WAF looks simple until it becomes important.&lt;/p&gt;

&lt;p&gt;At first it is a few managed rule groups, an IP allowlist, maybe a rate limit on &lt;code&gt;/login&lt;/code&gt;. Then a real application grows around it. You add exceptions. You tune false positives. You need different behavior for APIs, static assets, GraphQL, login pages, admin paths, mobile clients, partners, scanners, and known noisy endpoints.&lt;/p&gt;

&lt;p&gt;If all of that logic lives as a pile of one-off console edits, the Web ACL eventually becomes a place people are afraid to touch.&lt;/p&gt;

&lt;p&gt;The fix is not only "put it in Terraform." The better fix is to model AWS WAF like a small policy engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rules classify traffic.&lt;/li&gt;
&lt;li&gt;Labels carry facts about a request.&lt;/li&gt;
&lt;li&gt;Cleanup rules make final allow/block decisions.&lt;/li&gt;
&lt;li&gt;Terraform owns the shape, review history, and rollout process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is the pattern I use for WAF-as-code when the ACL is expected to live for years, not weeks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Start with ownership boundaries
&lt;/h2&gt;

&lt;p&gt;The first mistake is putting every WAF object into one giant Terraform state. AWS WAF has several object types with different lifecycles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web ACLs&lt;/li&gt;
&lt;li&gt;Rule groups&lt;/li&gt;
&lt;li&gt;IP sets&lt;/li&gt;
&lt;li&gt;Regex pattern sets&lt;/li&gt;
&lt;li&gt;Logging configuration&lt;/li&gt;
&lt;li&gt;Resource associations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some are global for CloudFront. Some are regional. Some are shared by many ACLs. Some are application-specific.&lt;/p&gt;

&lt;p&gt;A maintainable ownership model makes that visible. For example, you might manage these as separate Terraform states, separate workspaces, or separate pipeline units:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudFront Web ACLs
CloudFront IP sets
Regional Web ACLs per AWS region
Regional rule groups per AWS region
Reusable regex pattern sets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact directory structure is not important. What matters is that one change does not require planning every WAF object in the account.&lt;/p&gt;

&lt;p&gt;For CloudFront-scope WAF resources, remember that AWS WAF uses &lt;code&gt;CLOUDFRONT&lt;/code&gt; scope and the API endpoint is in &lt;code&gt;us-east-1&lt;/code&gt;. Regional Web ACLs live in the region of the protected resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep rule bodies reviewable
&lt;/h2&gt;

&lt;p&gt;Terraform HCL is good for resources, tags, providers, and wiring. Deeply nested WAF statements are not pleasant to review in HCL.&lt;/p&gt;

&lt;p&gt;For larger ACLs, consider separating stable resource metadata from ordered rule logic. That can be done with YAML, JSON, generated HCL, or a purpose-built generator.&lt;/p&gt;

&lt;p&gt;Example metadata shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-edge-acl&lt;/span&gt;
&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CLOUDFRONT&lt;/span&gt;
&lt;span class="na"&gt;default_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;

&lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cloudwatch_metrics_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;sampled_requests_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;metric_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-edge-acl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example rule document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify_login_paths&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app:surface:login&lt;/span&gt;
    &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;byte_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uri_path&lt;/span&gt;
        &lt;span class="na"&gt;positional_constraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STARTS_WITH&lt;/span&gt;
        &lt;span class="na"&gt;search_string&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/login&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cleanup_rate_limited_login&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9000&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;
    &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;label_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LABEL&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app:surface:login&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;label_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LABEL&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app:abuse:rate_limited&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether you render YAML to JSON, generate dynamic HCL, or write the rules directly is an implementation detail. The important part is that reviewers can read the policy as policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Labels are the language of a mature ACL
&lt;/h2&gt;

&lt;p&gt;AWS WAF labels are metadata attached to a request when a rule matches. Later rules can match those labels. Labels remain available until the Web ACL evaluation ends.&lt;/p&gt;

&lt;p&gt;That means a WAF rule does not have to decide the final fate of a request immediately. It can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This request is on a login path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This request matched a noisy scanner signature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This request came from a trusted integration IP set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then later rules can combine those facts.&lt;/p&gt;

&lt;p&gt;AWS also exposes label metrics, which makes labels useful for monitoring, not just enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design label namespaces, not random labels
&lt;/h2&gt;

&lt;p&gt;Random labels become a second naming problem. Treat labels like an internal API.&lt;/p&gt;

&lt;p&gt;A useful label naming shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;owner&amp;gt;:&amp;lt;namespace&amp;gt;:&amp;lt;specific_fact&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In AWS WAF terms, &lt;code&gt;app:surface:&lt;/code&gt; or &lt;code&gt;app:abuse:&lt;/code&gt; is a label namespace, and &lt;code&gt;app:surface:login&lt;/code&gt; is a specific label. That distinction matters because label match statements can match either one exact label or a namespace.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app:surface:login
app:surface:api
app:surface:admin

app:identity:trusted_integration
app:identity:internal_network
app:identity:anonymous

app:abuse:rate_limited
app:abuse:bad_bot
app:abuse:scanner

app:exception:legacy_client
app:exception:known_false_positive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is consistency. You want a reviewer to understand the purpose before reading the statement.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;matched-rule-17
temp_allow
new_api_fix
block_this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app:surface:graphql
app:exception:mobile_v1_encoding
app:abuse:sql_injection_signal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label should describe a fact, not the desired action. &lt;code&gt;app:surface:login&lt;/code&gt; is better than &lt;code&gt;app:allow_login&lt;/code&gt;, because the same fact might be used by allow, count, challenge, and block logic later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use count rules for classification
&lt;/h2&gt;

&lt;p&gt;In a label-first ACL, many early rules should use &lt;code&gt;count&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, this rule identifies login traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"classify-login-paths"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rule_label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:surface:login"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;or_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;byte_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;field_to_match&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;uri_path&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;positional_constraint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STARTS_WITH"&lt;/span&gt;
          &lt;span class="nx"&gt;search_string&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/login"&lt;/span&gt;

          &lt;span class="nx"&gt;text_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;byte_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;field_to_match&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;uri_path&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;positional_constraint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STARTS_WITH"&lt;/span&gt;
          &lt;span class="nx"&gt;search_string&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/oauth2/authorize"&lt;/span&gt;

          &lt;span class="nx"&gt;text_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"classify-login-paths"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule does not allow or block. It adds a fact to the request.&lt;/p&gt;

&lt;p&gt;Later, a rate-based rule might add another fact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"classify-login-rate-limit"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rule_label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:abuse:login_rate_limited"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rate_based_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;limit&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
      &lt;span class="nx"&gt;aggregate_key_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"IP"&lt;/span&gt;

      &lt;span class="nx"&gt;scope_down_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:surface:login"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"classify-login-rate-limit"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have two independent facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request is for the login surface.&lt;/li&gt;
&lt;li&gt;The request is over the login rate threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much easier to reason about than one deeply nested monster rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup rules make the final decision
&lt;/h2&gt;

&lt;p&gt;Cleanup rules are late-priority rules that convert labels into a final action.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-block-rate-limited-login"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9000&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;and_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:surface:login"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:abuse:login_rate_limited"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;not_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
              &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:identity:internal_network"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-block-rate-limited-login"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads like a sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Block login traffic that is rate limited unless it is internal network traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the point. Cleanup rules should be boring to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: managed rules without surprise blocking
&lt;/h2&gt;

&lt;p&gt;Managed rule groups are valuable, but turning them on with blocking actions can be a noisy experience.&lt;/p&gt;

&lt;p&gt;A safer pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the managed rule group.&lt;/li&gt;
&lt;li&gt;Override the rules to &lt;code&gt;count&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Observe labels and sampled requests.&lt;/li&gt;
&lt;li&gt;Add cleanup rules for the labels you are ready to enforce.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sketch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws-managed-common"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

  &lt;span class="nx"&gt;override_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;managed_rule_group_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWSManagedRulesCommonRuleSet"&lt;/span&gt;
      &lt;span class="nx"&gt;vendor_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws-managed-common"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then block only a label namespace that you have reviewed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-block-known-bad-managed-rule-matches"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9100&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;and_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NAMESPACE"&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awswaf:managed:aws:core-rule-set:"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;not_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
              &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:exception:known_false_positive"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-block-known-bad-managed-rule-matches"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important detail: for labels from a different context, such as a managed rule group, match the fully qualified label or namespace that AWS WAF reports. Do not guess. Pull it from sampled requests, logs, or the managed rule group metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: exception rules as labels
&lt;/h2&gt;

&lt;p&gt;Exceptions are where many Web ACLs become unreadable.&lt;/p&gt;

&lt;p&gt;Try this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"classify-known-mobile-client-encoding"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rule_label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:exception:mobile_v1_encoding"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;and_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;byte_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;field_to_match&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;single_header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"user-agent"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;positional_constraint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CONTAINS"&lt;/span&gt;
          &lt;span class="nx"&gt;search_string&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ExampleClient/1."&lt;/span&gt;

          &lt;span class="nx"&gt;text_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LOWERCASE"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;byte_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;field_to_match&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;uri_path&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;positional_constraint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STARTS_WITH"&lt;/span&gt;
          &lt;span class="nx"&gt;search_string&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/api/v1/upload"&lt;/span&gt;

          &lt;span class="nx"&gt;text_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="nx"&gt;type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"classify-known-mobile-client-encoding"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every cleanup rule can decide whether that exception matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;not_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:exception:mobile_v1_encoding"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That beats copying the same exception logic into five different block rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: allow is also a cleanup action
&lt;/h2&gt;

&lt;p&gt;Sometimes the final action is allow.&lt;/p&gt;

&lt;p&gt;For example, an integration endpoint might be default-deny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/integrations/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can model that as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 classify integration path
110 classify trusted integration source IP
9000 allow integration path + trusted source
9010 block integration path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two rules are the cleanup pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-allow-integration-api-from-trusted-networks"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9000&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;and_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:surface:integration_api"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:identity:trusted_integration"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-allow-integration-api-from-trusted-networks"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-block-integration-api"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9010&lt;/span&gt;

  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;label_match_statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LABEL"&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app:surface:integration_api"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudwatch_metrics_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;metric_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cleanup-block-integration-api"&lt;/span&gt;
    &lt;span class="nx"&gt;sampled_requests_enabled&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is explicit. Partner traffic is not "allowed by accident" because a default action happened to be permissive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing existing WAF resources
&lt;/h2&gt;

&lt;p&gt;Many teams start with existing console-managed WAF resources. Do not rewrite them by hand and hope for the best.&lt;/p&gt;

&lt;p&gt;Use an import-first workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the Terraform skeleton.&lt;/li&gt;
&lt;li&gt;Import the existing resource.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform plan&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Preserve live settings and tags unless the change is intentional.&lt;/li&gt;
&lt;li&gt;Repeat until the plan is no-op.&lt;/li&gt;
&lt;li&gt;Make one small intentional change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Terraform versions that support import blocks, the shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_wafv2_web_acl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;web-acl-id&amp;gt;/&amp;lt;web-acl-name&amp;gt;/&amp;lt;scope&amp;gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For classic CLI import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform import aws_wafv2_web_acl.this &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'&amp;lt;web-acl-id&amp;gt;/&amp;lt;web-acl-name&amp;gt;/&amp;lt;scope&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact import ID format varies by resource type. Check the provider docs for each object.&lt;/p&gt;

&lt;p&gt;The principle is more important than the command:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First prove Terraform can reproduce the current object. Then change it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Use placeholders for environment-specific values
&lt;/h2&gt;

&lt;p&gt;WAF policies often need values that should not be hardcoded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP set ARNs&lt;/li&gt;
&lt;li&gt;Regex pattern set ARNs&lt;/li&gt;
&lt;li&gt;SSM parameter values&lt;/li&gt;
&lt;li&gt;Rule group ARNs&lt;/li&gt;
&lt;li&gt;Account-specific prefixes&lt;/li&gt;
&lt;li&gt;Secret header values&lt;/li&gt;
&lt;li&gt;Remote-state outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A clean pattern is to keep placeholders in the rule document and render them at plan time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;classify-cdn-origin-secret&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app:identity:edge&lt;/span&gt;
    &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;byte_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;single_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x-edge-origin-secret&lt;/span&gt;
        &lt;span class="na"&gt;positional_constraint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EXACTLY&lt;/span&gt;
        &lt;span class="na"&gt;search_string&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__EDGE_ORIGIN_SECRET__"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then replace from Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_parameter"&lt;/span&gt; &lt;span class="s2"&gt;"edge_origin_secret"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/example/waf/origin-secret"&lt;/span&gt;
  &lt;span class="nx"&gt;with_decryption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rendered_rules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rules.yaml"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s2"&gt;"__EDGE_ORIGIN_SECRET__"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nonsensitive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ssm_parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_origin_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not commit secrets to source control. If the value must be shared between CloudFront and WAF, store it in something like SSM Parameter Store and have both sides read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule priority is part of the design
&lt;/h2&gt;

&lt;p&gt;Label-based ACLs depend on order.&lt;/p&gt;

&lt;p&gt;One useful convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0000-0999 emergency rules
1000-1999 request surface classification
2000-2999 identity/source classification
3000-3999 managed rule groups in count mode
4000-4999 abuse signal classification
8000-8999 temporary experiments
9000-9999 cleanup allow/block/challenge rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes reviews easier. A cleanup rule at priority &lt;code&gt;1500&lt;/code&gt; should look wrong immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;p&gt;Before merging a WAF change, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this change affect &lt;code&gt;CLOUDFRONT&lt;/code&gt; scope, regional scope, or both?&lt;/li&gt;
&lt;li&gt;Is the Terraform plan no-op except for the intended change?&lt;/li&gt;
&lt;li&gt;Are labels named as facts rather than actions?&lt;/li&gt;
&lt;li&gt;Does every cleanup rule read like a clear sentence?&lt;/li&gt;
&lt;li&gt;Are exceptions modeled once and reused through labels?&lt;/li&gt;
&lt;li&gt;Are secrets and environment-specific values coming from a secure source?&lt;/li&gt;
&lt;li&gt;Are managed rule labels copied from AWS output rather than guessed?&lt;/li&gt;
&lt;li&gt;Is the rollout count-first where false positives are possible?&lt;/li&gt;
&lt;li&gt;Are CloudWatch metrics enabled for the rules that matter?&lt;/li&gt;
&lt;li&gt;Is there a rollback path that does not require console edits?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;The most useful shift is to stop thinking of AWS WAF as a list of independent firewall rules.&lt;/p&gt;

&lt;p&gt;Think of it as a request evaluation pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request
  -&amp;gt; classify surface
  -&amp;gt; classify identity/source
  -&amp;gt; classify managed-rule signals
  -&amp;gt; classify exceptions
  -&amp;gt; cleanup decisions
  -&amp;gt; default action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Labels are the memory of that pipeline. Cleanup rules are where the policy becomes enforcement.&lt;/p&gt;

&lt;p&gt;Once you adopt that shape, AWS WAF gets easier to review, easier to test, and much less scary to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS WAF labels: &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-labels.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/waf/latest/developerguide/waf-labels.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS WAF rules that add labels: &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-add.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-add.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS WAF rules that match labels: &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-match.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-label-match.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS WAF custom responses: &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>Precision Canary Deployments for Static Content: Navigating High-Stakes Tech Migrations</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Wed, 06 May 2026 13:26:02 +0000</pubDate>
      <link>https://dev.to/aws-builders/precision-canary-deployments-for-static-content-navigating-high-stakes-tech-migrations-1nim</link>
      <guid>https://dev.to/aws-builders/precision-canary-deployments-for-static-content-navigating-high-stakes-tech-migrations-1nim</guid>
      <description>&lt;h2&gt;
  
  
  Precision Canary Deployments for Static Content: Beyond the "Big Bang" Release
&lt;/h2&gt;

&lt;p&gt;Big frontend migrations rarely fail because of one obvious bug.&lt;/p&gt;

&lt;p&gt;They fail because production is messy.&lt;/p&gt;

&lt;p&gt;A move from a legacy React SPA to a Next.js static build, a routing-model change, or a full redesign can look straightforward on paper because the site is "just static files." In production, the real risk is the combination of browser state, CDN caching, routing behavior, and customer sessions already in flight.&lt;/p&gt;

&lt;p&gt;That is where a &lt;strong&gt;precision canary&lt;/strong&gt; earns its keep.&lt;/p&gt;

&lt;p&gt;Instead of sending 50% of traffic to a new build and hoping for the best, you can start with &lt;strong&gt;0.1% or even 0.01%&lt;/strong&gt;, observe what happens under real conditions, and expand only when the evidence says you should.&lt;/p&gt;

&lt;p&gt;For engineers, that cuts blast radius. For executives, it turns a risky migration into a controlled rollout with a fast rollback path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Executive summary
&lt;/h2&gt;

&lt;p&gt;A precision canary for static content does four important things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It keeps the &lt;strong&gt;initial risk very small&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It makes the routing decision &lt;strong&gt;before the cache lookup&lt;/strong&gt;, so stable and canary traffic stay separated.&lt;/li&gt;
&lt;li&gt;It avoids cookies by using an &lt;strong&gt;anonymous, deterministic routing key&lt;/strong&gt; such as &lt;strong&gt;JA4&lt;/strong&gt;, then &lt;strong&gt;JA3&lt;/strong&gt;, with a final fallback when neither is available.&lt;/li&gt;
&lt;li&gt;It allows &lt;strong&gt;instant rollback&lt;/strong&gt; by changing one edge decision instead of waiting for DNS behavior to settle.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why static-site migrations still fail in production
&lt;/h2&gt;

&lt;p&gt;Staging environments are useful, but they are not production. They do not contain months of real browser state, cached assets, half-complete sessions, or every unusual client combination that appears on the public internet.&lt;/p&gt;

&lt;p&gt;Here are the common ways large static-site releases fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hidden client state
&lt;/h3&gt;

&lt;p&gt;Real users arrive with old service workers, outdated cached files, old &lt;code&gt;localStorage&lt;/code&gt; data, and long-lived tabs. A new build can be technically correct and still break when it meets that real-world state.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Session fragmentation
&lt;/h3&gt;

&lt;p&gt;If a user lands on the new version for one request and the old version for the next, state and routing assumptions can collide. The result is often a broken session, confusing UI behavior, or JavaScript errors that never appeared in testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Routing surprises
&lt;/h3&gt;

&lt;p&gt;A migration from hash routing to history-based routing changes the rules. A page that works during client-side navigation can still fail on refresh if the origin or edge does not return the expected fallback document.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Business impact arrives before technical certainty
&lt;/h3&gt;

&lt;p&gt;When a public site supports revenue, leads, sign-ins, or brand trust, the cost of a bad release is not just a higher bug count. It is lost conversion, increased support load, and rollback stress at the worst possible moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DNS weighting is not the best fit here
&lt;/h2&gt;

&lt;p&gt;A common first idea is to split traffic at the DNS layer with Route 53 weighted records.&lt;/p&gt;

&lt;p&gt;That can work in some scenarios, but it is a weak fit for high-stakes frontend migrations because it sits outside the request path, where the decision really needs to happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route 53 weighting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to understand&lt;/li&gt;
&lt;li&gt;Native AWS capability&lt;/li&gt;
&lt;li&gt;Fine for coarse traffic shifts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rollback is slower because DNS behavior is not instantly consistent everywhere&lt;/li&gt;
&lt;li&gt;It cannot easily support tester overrides with headers&lt;/li&gt;
&lt;li&gt;It does not naturally keep a single viewer pinned to a deterministic version during the migration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why CloudFront Functions is the better control point
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;CloudFront Functions&lt;/strong&gt;, the routing decision happens at the edge on the request itself.&lt;/p&gt;

&lt;p&gt;That gives you better operational control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instant rollback&lt;/strong&gt; by changing edge logic or configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very fine rollout precision&lt;/strong&gt; using basis points&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA and executive preview paths&lt;/strong&gt; through explicit overrides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better routing consistency&lt;/strong&gt; without relying on cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Important trade-off: AWS notes that CloudFront Functions run on every viewer request, while Lambda@Edge origin-request logic runs only on cache misses. For highly cacheable workloads, Lambda@Edge can be more cost-efficient. For this article, the recommendation stays with CloudFront Functions because the main goal is &lt;strong&gt;fast, precise rollout control&lt;/strong&gt; during a risky migration, not minimizing function invocations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp7a08791j7gu197mxtm4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp7a08791j7gu197mxtm4.png" alt="Precision canary architecture" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep &lt;strong&gt;one CloudFront distribution&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Define &lt;strong&gt;two origins&lt;/strong&gt;: stable and canary.&lt;/li&gt;
&lt;li&gt;Run a &lt;strong&gt;viewer-request CloudFront Function&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Decide which origin to use before the cache lookup.&lt;/li&gt;
&lt;li&gt;Add a small header such as &lt;code&gt;x-canary-origin&lt;/code&gt; so the cache key stays isolated between stable and canary objects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This matters because without cache isolation, CloudFront can serve a cached canary object to a stable user, or vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  The routing principle: deterministic, anonymous, cookie-free
&lt;/h2&gt;

&lt;p&gt;For this use case, the goal is not personal identity. The goal is &lt;strong&gt;consistent traffic assignment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A good order of precedence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit header override&lt;/strong&gt; for QA or controlled previews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit query override&lt;/strong&gt; for one-off testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JA4 fingerprint&lt;/strong&gt; when available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JA3 fingerprint&lt;/strong&gt; if JA4 is not available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback hash&lt;/strong&gt; using &lt;code&gt;user-agent + viewer.ip&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives you a cookie-free strategy that is still stable enough for production rollout control.&lt;/p&gt;

&lt;p&gt;This article assumes the site is always served over &lt;strong&gt;HTTPS&lt;/strong&gt;. That matters because JA4 and JA3 are derived from the TLS handshake and are only available on HTTPS requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical decision flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ag8p1coe1nfqso5nan2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ag8p1coe1nfqso5nan2.png" alt="Decision flow for cookie-free canary routing" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why basis points matter
&lt;/h2&gt;

&lt;p&gt;Percentages are often too coarse.&lt;/p&gt;

&lt;p&gt;For a large migration, &lt;strong&gt;1% can already represent a meaningful amount of real traffic&lt;/strong&gt;. Using &lt;strong&gt;basis points (BPS)&lt;/strong&gt; gives you a much finer dial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 BPS = 0.01%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10 BPS = 0.1%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;100 BPS = 1%&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That lets you run a true smoke test in production before the rollout is visible to a meaningful share of customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  CloudFront Function example
&lt;/h2&gt;

&lt;p&gt;The following function shows the core pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manual overrides first&lt;/li&gt;
&lt;li&gt;deterministic cookie-free routing second&lt;/li&gt;
&lt;li&gt;cache isolation always&lt;/li&gt;
&lt;li&gt;stable fallback when viewer fingerprints are missing&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Important: the sample below assumes JavaScript runtime 2.0 because origin selection helper methods require it.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudfront&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;OLD_ORIGIN_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;origin-production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;NEW_ORIGIN_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;origin-canary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Basis points: 1 = 0.01%, 10 = 0.1%, 100 = 1%&lt;/span&gt;
  &lt;span class="na"&gt;CANARY_BPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ES5-safe deterministic 32-bit hash for bucketing&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stableHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2166136261&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickRoutingKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Prefer JA4, then JA3. Both are anonymous TLS-derived fingerprints.&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ja4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudfront-viewer-ja4-fingerprint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ja4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja4:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ja4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ja3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudfront-viewer-ja3-fingerprint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ja3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja3:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ja3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Final fallback: still deterministic, but less stable than JA4/JA3&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewer&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uaip:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyManualOverride&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;querystring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="c1"&gt;// 1) QA header override&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;qaVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-qa-version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qaVersion&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qaVersion&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2) One-off query override&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;forceCanary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;querystring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove the test flag so it does not accidentally affect cache behavior&lt;/span&gt;
    &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querystring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;forceCanary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;viewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;useNewOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyManualOverride&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useNewOrigin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;routingKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pickRoutingKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stableHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routingKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;useNewOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CANARY_BPS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Keep stable and canary cache entries isolated&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-canary-origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;useNewOrigin&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;old&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useNewOrigin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectRequestOriginById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEW_ORIGIN_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectRequestOriginById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OLD_ORIGIN_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this code gets right
&lt;/h2&gt;

&lt;p&gt;This version is designed to align with the operational goals in this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  It does not use cookies
&lt;/h3&gt;

&lt;p&gt;That keeps the mechanism lightweight and avoids introducing a separate persistence layer just to keep users on the same version.&lt;/p&gt;

&lt;h3&gt;
  
  
  It prefers the most stable anonymous signal available
&lt;/h3&gt;

&lt;p&gt;JA4 first, then JA3, gives you a better routing key than raw IP alone. When those fingerprints are unavailable, the function still behaves predictably.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Header-name note: AWS documentation presents these as &lt;code&gt;CloudFront-Viewer-JA4-Fingerprint&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-JA3-Fingerprint&lt;/code&gt;, but inside a CloudFront Functions event object you access them in lowercase as &lt;code&gt;cloudfront-viewer-ja4-fingerprint&lt;/code&gt; and &lt;code&gt;cloudfront-viewer-ja3-fingerprint&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  It keeps cache entries separated
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;x-canary-origin&lt;/code&gt; request header is the critical separator. Include this header in the cache key so stable and canary content never share the same cached object.&lt;/p&gt;

&lt;h3&gt;
  
  
  It supports controlled previews
&lt;/h3&gt;

&lt;p&gt;Executives, QA, or launch managers can view the canary safely using a header or a one-off query switch without changing the default rollout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Required distribution configuration
&lt;/h2&gt;

&lt;p&gt;The code is only part of the solution. The distribution settings matter just as much.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Two origins
&lt;/h3&gt;

&lt;p&gt;Define one origin for the current production build and one for the canary build.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Viewer-request CloudFront Function
&lt;/h3&gt;

&lt;p&gt;Associate the function with the cache behavior that serves the site.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Enable the CloudFront-generated fingerprint headers
&lt;/h3&gt;

&lt;p&gt;CloudFront needs to add the viewer fingerprint headers so the function can inspect them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CloudFront-Viewer-JA4-Fingerprint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CloudFront-Viewer-JA3-Fingerprint&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These headers are available to CloudFront Functions and Lambda@Edge, but only for &lt;strong&gt;HTTPS&lt;/strong&gt; requests. AWS also notes that these TLS-related headers can be added to an &lt;strong&gt;origin request policy&lt;/strong&gt;, but not to a &lt;strong&gt;cache policy&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cache policy includes &lt;code&gt;x-canary-origin&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is what keeps stable and canary cache objects isolated.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Query-string handling is deliberate
&lt;/h3&gt;

&lt;p&gt;If you allow the &lt;code&gt;?canary=1&lt;/code&gt; switch for testing, either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove it in the function, as shown above, or&lt;/li&gt;
&lt;li&gt;make sure it is not part of the cache key&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. SPA or app-style routing fallback is explicit
&lt;/h3&gt;

&lt;p&gt;If the migration changes routing behavior, make sure refreshes and deep links return the correct document.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Validate viewer fingerprint availability in your environment
&lt;/h3&gt;

&lt;p&gt;JA3 and JA4 are excellent routing inputs when exposed to the function path, but you should validate their availability in your exact CloudFront setup before making them your only signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout plan that works for both engineers and executives
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3wx5ynbf31f5m2mwfb6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3wx5ynbf31f5m2mwfb6.png" alt="Rollout stages for precision canary deployment" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical rollout plan looks like this:&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: 0.1% smoke test
&lt;/h3&gt;

&lt;p&gt;Purpose: prove routing, cache isolation, and basic stability.&lt;/p&gt;

&lt;p&gt;Watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unexpected 404s&lt;/li&gt;
&lt;li&gt;JavaScript startup failures&lt;/li&gt;
&lt;li&gt;broken asset loading&lt;/li&gt;
&lt;li&gt;obvious support noise&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 2: 1% early validation
&lt;/h3&gt;

&lt;p&gt;Purpose: confirm that the new build survives real traffic patterns.&lt;/p&gt;

&lt;p&gt;Watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client-side error rates&lt;/li&gt;
&lt;li&gt;performance regressions&lt;/li&gt;
&lt;li&gt;login or session complaints&lt;/li&gt;
&lt;li&gt;routing edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 3: 5% confidence build
&lt;/h3&gt;

&lt;p&gt;Purpose: compare business and operational metrics against the stable path.&lt;/p&gt;

&lt;p&gt;Watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conversion rate&lt;/li&gt;
&lt;li&gt;bounce rate&lt;/li&gt;
&lt;li&gt;task completion&lt;/li&gt;
&lt;li&gt;error budgets&lt;/li&gt;
&lt;li&gt;support tickets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 4: 25% operational proof
&lt;/h3&gt;

&lt;p&gt;Purpose: verify team readiness, dashboards, alerts, and rollback confidence under broader load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 5: 100% cutover
&lt;/h3&gt;

&lt;p&gt;Promote only when the migration risk is low enough for the new path to become the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure during the canary
&lt;/h2&gt;

&lt;p&gt;For technical teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;404 rate by path&lt;/li&gt;
&lt;li&gt;JavaScript error rate&lt;/li&gt;
&lt;li&gt;asset load failures&lt;/li&gt;
&lt;li&gt;Core Web Vitals or equivalent performance signals&lt;/li&gt;
&lt;li&gt;origin error rate and CDN cache behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For business stakeholders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conversion or lead generation&lt;/li&gt;
&lt;li&gt;checkout or sign-in completion&lt;/li&gt;
&lt;li&gt;bounce rate&lt;/li&gt;
&lt;li&gt;support ticket volume&lt;/li&gt;
&lt;li&gt;incident count during the rollout window&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Important caveats
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JA3 and JA4 are not identity
&lt;/h3&gt;

&lt;p&gt;They are useful routing signals, not person-level identifiers. Their role here is to keep similar requests on the same side of the rollout decision, not to identify an individual user.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTPS matters
&lt;/h3&gt;

&lt;p&gt;JA3 and JA4 come from the TLS &lt;code&gt;Client Hello&lt;/code&gt;, so they are relevant only for HTTPS traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cache key is the safety line
&lt;/h3&gt;

&lt;p&gt;If you remember only one implementation detail, remember this one: &lt;strong&gt;do not mix stable and canary objects in the same cache key&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep rollback boring
&lt;/h3&gt;

&lt;p&gt;The best rollout is the one that can be reversed in seconds without debate. Edge-based selection gives you that option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A canary deployment for static content is not just a safer release tactic.&lt;/p&gt;

&lt;p&gt;For major web migrations, it is a way to separate &lt;strong&gt;technical uncertainty&lt;/strong&gt; from &lt;strong&gt;business risk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By making the decision at the edge, using deterministic cookie-free routing, and isolating cache entries correctly, you can move from a fragile big-bang launch to a controlled rollout that both engineers and executives can support.&lt;/p&gt;

&lt;p&gt;That is the real value: fewer surprises, faster rollback, and better evidence before full cutover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;Special thanks to &lt;a href="https://www.linkedin.com/in/moti-moskovich-1b5b635/" rel="noopener noreferrer"&gt;@Moti Moskovich&lt;/a&gt; for his contribution to this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/helper-functions-origin-modification.html" rel="noopener noreferrer"&gt;Amazon CloudFront helper methods for origin modification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html" rel="noopener noreferrer"&gt;Add CloudFront request headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html" rel="noopener noreferrer"&gt;CloudFront Functions event structure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cache-key-understand-cache-policy.html" rel="noopener noreferrer"&gt;Understand cache policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html" rel="noopener noreferrer"&gt;Cache content based on request headers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>s3</category>
      <category>canary</category>
    </item>
    <item>
      <title>Protect Your Web Site with AWS WAF and CloudFront</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Sun, 22 Mar 2026 12:58:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/protect-your-web-site-with-aws-waf-and-cloudfront-2p8c</link>
      <guid>https://dev.to/aws-builders/protect-your-web-site-with-aws-waf-and-cloudfront-2p8c</guid>
      <description>&lt;h2&gt;
  
  
  Protect Your On-Premises Website with AWS WAF and Amazon CloudFront
&lt;/h2&gt;

&lt;p&gt;Protecting web applications—regardless of where they run—with a scalable, highly available, and cost-effective edge layer is a cornerstone of modern architecture. This article walks through a hands-on proof of concept (PoC) that leverages AWS WAF for security, CloudFront for global caching and performance, and a serverless pipeline for on-the-fly HTTP context modification.&lt;/p&gt;

&lt;p&gt;You’ll learn how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enhance availability and reduce origin load by caching static and dynamic content at the edge.&lt;/li&gt;
&lt;li&gt;Secure traffic with AWS WAF rules before it ever reaches CloudFront.&lt;/li&gt;
&lt;li&gt;Offload requests from the origin to minimize connectivity and costs.&lt;/li&gt;
&lt;li&gt;Modify any part of the HTTP request or response—including headers, query strings, and body—using a Lambda-powered proxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sensitive data and IP addresses in screenshots have been blurred.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; I understand this guide—demonstrating how to proxy and modify HTTP content—might resemble instructions used by phishing site creators. That was never my intention; these techniques are meant solely for legitimate security, performance, and reliability purposes.&lt;/p&gt;

&lt;p&gt;Traditional WAF appliances and CDNs can be expensive, rigid, and complex to manage—particularly when you need to retrofit existing on-premises applications. By combining AWS WAF and Amazon CloudFront, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge Security:&lt;/strong&gt; AWS WAF blocks malicious requests at AWS’s global PoPs, shielding your origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Caching:&lt;/strong&gt; CloudFront serves cached content from locations closest to your users, reducing latency and origin hits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin Offload:&lt;/strong&gt; Less network traffic and compute on your servers, lowering TCO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-the-Fly HTTP Manipulation:&lt;/strong&gt; Inject, strip, or rewrite headers and body content without touching origin code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll demonstrate these capabilities using a simple IP-lookup service (&lt;code&gt;noc.co.il&lt;/code&gt;) to keep the PoC lean and focused on edge mechanics.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Evaluating Origin Compatibility
&lt;/h2&gt;

&lt;p&gt;For this PoC, we chose a simple IP-lookup service (&lt;code&gt;noc.co.il&lt;/code&gt;) to isolate edge mechanics. Our goals were to verify that the origin would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Respond over HTTPS without client interruptions.&lt;/li&gt;
&lt;li&gt;Trust and reflect standard proxy headers (&lt;code&gt;X-Forwarded-For&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Parse header values as plain text (no validation), allowing us to spoof or inject values.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.1 Initial Compatibility Test
&lt;/h3&gt;

&lt;p&gt;We first obtained the &lt;code&gt;curl&lt;/code&gt; command via the browser’s &lt;strong&gt;Copy as cURL&lt;/strong&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://noc.co.il/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: text/html'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compressed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this returned the expected HTML shell of the IP lookup page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;--compressed&lt;/span&gt; https://noc.co.il/
HTTP/1.1 200 OK
Content-Type: text/html&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UTF-8
...
&amp;lt;html&amp;gt;My IP: 203.0.113.10&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 Testing &lt;code&gt;X-Forwarded-For&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Next, we injected a fake IP via the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header to see if the service honors it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Forwarded-For: 198.51.100.24"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compressed&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://noc.co.il/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The response body included both our real IP and &lt;code&gt;198.51.100.24&lt;/code&gt;, confirming the header was trusted.&lt;/li&gt;
&lt;li&gt;No header validation errors occurred.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.3 Spoofing Non-existent Addresses
&lt;/h3&gt;

&lt;p&gt;To verify lack of validation, we added another bogus IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Forwarded-For: 198.51.100.24, 300.300.300.300"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://noc.co.il/ | &lt;span class="nb"&gt;tee &lt;/span&gt;response.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspecting &lt;code&gt;response.html&lt;/code&gt; in a browser showed the exact header text echoed in the page—demonstrating the origin parses the header as raw text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidebar:&lt;/strong&gt; Because CloudFront automatically appends &lt;code&gt;X-Forwarded-For&lt;/code&gt;, this origin behavior is crucial for forwarding the true client IP downstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Solution Architecture Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Original Architecture
&lt;/h3&gt;

&lt;p&gt;In the initial approach, I fronted the origin directly with AWS WAF and CloudFront, without any additional proxy layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy3namj1ejohgjpvk7n9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy3namj1ejohgjpvk7n9.png" alt="Original architecture diagram" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Clients -&amp;gt; AWS WAF -&amp;gt; CloudFront -&amp;gt; Original Site (noc.co.il)&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS WAF:&lt;/strong&gt; Enforces IP allow-lists and managed security rules at AWS edge locations before traffic reaches CloudFront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront:&lt;/strong&gt; Distributes and caches content globally, forwarding only necessary headers to the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original Site:&lt;/strong&gt; The unmodified &lt;code&gt;noc.co.il&lt;/code&gt; endpoint, responding directly to CloudFront requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Final Architecture (With API Gateway Proxy)
&lt;/h3&gt;

&lt;p&gt;To overcome response-body limitations, I introduced an API Gateway/Lambda proxy between CloudFront and the origin. The Lambda function serves as a reverse proxy for dynamic HTML, while static assets like &lt;code&gt;favicon.ico&lt;/code&gt; are served directly from S3 via a separate behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz93gyapuhhipwo0kqac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz93gyapuhhipwo0kqac.png" alt="Final architecture with API Gateway proxy" width="800" height="752"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AWS WAF:&lt;/strong&gt; Applies IP filters and managed rules at the AWS edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront:&lt;/strong&gt; Uses two cache behaviors:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/favicon.ico&lt;/code&gt; -&amp;gt; &lt;strong&gt;S3 origin&lt;/strong&gt; for static assets with optimized TTLs.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/*&lt;/code&gt; -&amp;gt; &lt;strong&gt;API Gateway&lt;/strong&gt; for dynamic HTML via the reverse-proxy Lambda.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway (REST API Reverse Proxy Mode):&lt;/strong&gt; Receives viewer requests for dynamic content and invokes the Lambda reverse-proxy function.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lambda Function (Reverse Proxy):&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fetch:&lt;/strong&gt; Makes an HTTP request to &lt;code&gt;https://noc.co.il&lt;/code&gt; including path and query parameters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modify:&lt;/strong&gt; Reads and parses the HTML response body.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Return:&lt;/strong&gt; Sends the modified HTML back through API Gateway and CloudFront while preserving the original status code and headers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;S3 Origin:&lt;/strong&gt; Serves the custom &lt;code&gt;favicon.ico&lt;/code&gt; directly from edge caches, bypassing API Gateway.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Detailed Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Certificate Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate a Let’s Encrypt certificate for your custom domain (&lt;code&gt;noc.ittools.net&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9036rorh8x8wj3tb60bp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9036rorh8x8wj3tb60bp.png" alt="Certificate generation screenshot" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Import it into ACM in &lt;code&gt;us-east-1&lt;/code&gt; for CloudFront.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm965xzy5smjod8e6j1ga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm965xzy5smjod8e6j1ga.png" alt="ACM import screenshot" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 AWS WAF Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP Sets:&lt;/strong&gt; Define trusted IPv4/IPv6 addresses such as office and datacenter ranges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkgr5f8ta7u0es0bhj3b7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkgr5f8ta7u0es0bhj3b7.png" alt="AWS WAF IP set screenshot 1" width="800" height="707"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt="AWS WAF IP set screenshot 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web ACL:&lt;/strong&gt; Allow traffic from the IP sets and set the default action to &lt;strong&gt;Block&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76vsnikwkasubyo4r5ok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76vsnikwkasubyo4r5ok.png" alt="AWS WAF Web ACL screenshot" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate-based rules (optional):&lt;/strong&gt; Throttle anomalous request spikes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.3 CloudFront Distribution
&lt;/h3&gt;

&lt;p&gt;Configure the CloudFront distribution with the cache behavior and origin request policy that match your proxy design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flcox8stde414asnx98mu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flcox8stde414asnx98mu.png" alt="CloudFront distribution screenshot" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attach your AWS WAF Web ACL to this distribution.&lt;/li&gt;
&lt;li&gt;Enable response headers policies to add HSTS, CSP, and custom headers at the edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.4 Reducing Origin Connectivity
&lt;/h3&gt;

&lt;p&gt;Caching at CloudFront dramatically cuts down on requests to your origin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static assets (images, CSS, JS) are served from edge caches for configurable TTLs.&lt;/li&gt;
&lt;li&gt;Dynamic content hits origin only on cache misses—adjustable via cache keys (headers, cookies, query strings).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This offload not only improves performance but reduces bandwidth and compute costs on your servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 Full HTTP Context Access and Modification
&lt;/h3&gt;

&lt;p&gt;Neither CloudFront Functions nor Lambda@Edge allow modifying response bodies. Additionally, since both CloudFront and API Gateway automatically append their own &lt;code&gt;X-Forwarded-For&lt;/code&gt; entries, we need a reverse proxy to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the original HTML from &lt;code&gt;https://noc.co.il&lt;/code&gt; while preserving query strings and path.&lt;/li&gt;
&lt;li&gt;Inject a custom &lt;code&gt;&amp;lt;link rel="icon" href="/favicon.ico" /&amp;gt;&lt;/code&gt; into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Strip the two trailing IP addresses added by CloudFront and API Gateway from any comma-separated IP lists embedded in the HTML body.&lt;/li&gt;
&lt;li&gt;Return the modified HTML through API Gateway and CloudFront while preserving the origin’s headers and status codes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To achieve this, configure API Gateway in REST API reverse-proxy mode and invoke a Lambda function like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;http.client&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlencode&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration
&lt;/span&gt;&lt;span class="n"&gt;REMOVE_IP_COUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REMOVE_IP_COUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;FAVICON_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAVICON_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/favicon.ico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORIGIN_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORIGIN_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noc.co.il&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORIGIN_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORIGIN_PATH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Parse client request info
&lt;/span&gt;    &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;client_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;qs_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queryStringParameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;query_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;qs_params&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="c1"&gt;# 2) Build the upstream path
&lt;/span&gt;    &lt;span class="n"&gt;origin_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ORIGIN_PATH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;client_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;origin_path&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;query_str&lt;/span&gt;

    &lt;span class="c1"&gt;# 3) Copy headers except Host
&lt;/span&gt;    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# 4) Fetch from the real origin
&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTTPSConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ORIGIN_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getresponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;body_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 5) Decode HTML
&lt;/span&gt;    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body_bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 6) Inject favicon link right after &amp;lt;head&amp;gt;
&lt;/span&gt;    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&amp;lt;head\b[^&amp;gt;]*&amp;gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\1&amp;lt;link rel=\"icon\" href=\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FAVICON_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\" type=\"image/png\"&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 7) Remove the last N IPv4s from every comma-separated list
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_trailing_ips&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s*,\s*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;REMOVE_IP_COUNT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;REMOVE_IP_COUNT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(?:\d{1,3}\.){3}\d{1,3}(?:\s*,\s*(?:\d{1,3}\.){3}\d{1,3})+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;strip_trailing_ips&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 8) Build the API Gateway Lambda-proxy response
&lt;/span&gt;    &lt;span class="n"&gt;out_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getheader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_bytes&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isBase64Encoded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern gives you full control over outgoing responses without touching the origin code.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Final Results
&lt;/h2&gt;

&lt;p&gt;Below is a comparison of the original IP lookup page and the modified version, showing the injected favicon and response changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Original Site
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczczj4iqzkncmxyx9olq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczczj4iqzkncmxyx9olq.png" alt="Original site screenshot" width="748" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Modified Site
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0edtx7ug0zs2udpj9w0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0edtx7ug0zs2udpj9w0q.png" alt="Modified site screenshot" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. High Availability, Security, and CDN Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DDoS Mitigation:&lt;/strong&gt; AWS WAF integrates with AWS Shield at no extra cost for CloudFront distributions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Failover:&lt;/strong&gt; Edge caches can continue serving content even if the origin is briefly unreachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Efficiency:&lt;/strong&gt; Pay only for WAF rule evaluations, Lambda invocations, and data transfer—no upfront hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.1 Amazon CloudFront as a CDN
&lt;/h3&gt;

&lt;p&gt;Amazon CloudFront is AWS’s global content delivery network, designed to accelerate both static and dynamic content by caching it at edge locations worldwide. Key benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduced latency:&lt;/strong&gt; Delivers content from the closest edge location to end users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable cache behaviors:&lt;/strong&gt; Control TTLs, forwarded headers, query strings, and cookies per path pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic content support:&lt;/strong&gt; Forward dynamic requests to any HTTP origin and selectively cache or bypass content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security integrations:&lt;/strong&gt; AWS WAF, TLS termination at the edge, and Origin Access Control for private S3 buckets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidation and versioning:&lt;/strong&gt; Invalidate cache on demand or use versioned URLs for instant updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay-as-you-go pricing:&lt;/strong&gt; Based on data transfer, request counts, and invalidation usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.2 Origin Failover with Multiple Origins
&lt;/h3&gt;

&lt;p&gt;To maximize availability, CloudFront supports &lt;strong&gt;Origin Groups&lt;/strong&gt;—a primary/secondary origin configuration that automatically fails over if the primary origin becomes unhealthy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary origin:&lt;/strong&gt; Your default origin, such as API Gateway or a custom origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary origin:&lt;/strong&gt; A fallback endpoint, such as another region, an on-premises endpoint, or a static S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks:&lt;/strong&gt; CloudFront monitors the primary origin using configurable HTTP(S) health checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failover logic:&lt;/strong&gt; On health-check failure, requests are routed to the secondary origin with minimal latency impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configuration steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the CloudFront distribution, create two origins.&lt;/li&gt;
&lt;li&gt;Define an origin group linking both origins and specify health-check parameters.&lt;/li&gt;
&lt;li&gt;Update cache behaviors to use the origin group as the target.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This failover capability ensures that even if your primary backend experiences downtime, CloudFront can seamlessly serve content from an alternate source.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Conclusion
&lt;/h2&gt;

&lt;p&gt;By fronting any website—on-premises or in the cloud—with AWS WAF and CloudFront, you achieve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enhanced availability through aggressive edge caching and failover.&lt;/li&gt;
&lt;li&gt;Robust security with customizable WAF rules at every PoP.&lt;/li&gt;
&lt;li&gt;Minimal origin load via cache offload and selective forwarding.&lt;/li&gt;
&lt;li&gt;Complete HTTP context manipulation using CloudFront Functions, Lambda@Edge, or, in very specific cases, API Gateway and Lambda for dynamic transformations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Normally the API Gateway + Lambda part is not required, since many HTML body changes can be made on the origin web server directly and these workarounds are unnecessary.&lt;/p&gt;

&lt;p&gt;This architecture transforms legacy and modern applications alike into resilient, secure, and performant cloud-edge services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: Edge Compute Options — CloudFront Functions vs Lambda@Edge
&lt;/h2&gt;

&lt;p&gt;AWS CloudFront provides two serverless compute options at the edge—CloudFront Functions and Lambda@Edge—each tailored to different use cases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffiv83lo09rtuwbprcpcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffiv83lo09rtuwbprcpcn.png" alt="Edge compute options screenshot" width="800" height="906"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Parallels to F5 iRules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;F5 iRules allow TCP, UDP, and HTTP inspection and manipulation on traditional load balancers.&lt;/li&gt;
&lt;li&gt;CloudFront Functions provide similar lightweight header- and URL-based logic at the edge, executing JavaScript within milliseconds.&lt;/li&gt;
&lt;li&gt;Lambda@Edge extends these capabilities with richer runtimes and more advanced traffic steering and content transformation.&lt;/li&gt;
&lt;li&gt;Both CloudFront serverless options and F5 iRules enable granular control over HTTP flows, but CloudFront offloads compute to a global CDN with pay-as-you-go billing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront Functions cannot perform network calls or access request bodies.&lt;/li&gt;
&lt;li&gt;Lambda@Edge offers more runtime and compute but incurs higher latency and cost for short-lived tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This appendix clarifies compute options independently of the PoC’s specific Lambda reverse-proxy implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: Pricing Notes
&lt;/h2&gt;

&lt;p&gt;The original LinkedIn article includes AWS WAF and CloudFront pricing examples. Before publishing on DEV, verify current pricing in the official AWS pricing pages because service prices can change over time. The original article listed example monthly Web ACL, rule, request, data transfer, request, invalidation, and certificate price points. (&lt;a href="https://www.linkedin.com/pulse/protect-your-web-site-aws-waf-cloudfront-alexey-baltacov-60lgf/?trackingId=J10c9QI%2F7GB7OGQ44uQEVw%3D%3D" rel="noopener noreferrer"&gt;linkedin.com&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/waf/pricing/" rel="noopener noreferrer"&gt;AWS WAF pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/cloudfront/pricing/" rel="noopener noreferrer"&gt;Amazon CloudFront pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/waf/" rel="noopener noreferrer"&gt;AWS WAF Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/" rel="noopener noreferrer"&gt;Amazon CloudFront Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/high_availability_origin_failover.html" rel="noopener noreferrer"&gt;CloudFront origin failover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-edge.html" rel="noopener noreferrer"&gt;Lambda@Edge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html" rel="noopener noreferrer"&gt;CloudFront Functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>waf</category>
      <category>cloudfront</category>
      <category>security</category>
    </item>
    <item>
      <title>AWS WAF Rate Limiting Based on Origin Response</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Sun, 22 Mar 2026 11:24:42 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-waf-rate-limiting-based-on-origin-response-1h5j</link>
      <guid>https://dev.to/aws-builders/aws-waf-rate-limiting-based-on-origin-response-1h5j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllugmyu3dkpwg3wjzfv9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fllugmyu3dkpwg3wjzfv9.png" alt="AWS WAF Rate Limiting Based on Origin Response"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You have a public website fronted by Amazon CloudFront that serves static files from S3. Customers access these files via direct URLs and must be able to download any file at any time without interference. At the same time, you want to stop malicious actors from crawling your entire bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; Prevent automated scanning of all URLs while still allowing legitimate customers unlimited downloads of the specific files they need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint:&lt;/strong&gt; No user login or authentication. Files are freely downloadable, so you cannot simply gate them behind a sign-in flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Plain AWS WAF Rate Limiting Is Not Enough
&lt;/h2&gt;

&lt;p&gt;AWS WAF lets you define rate-limit rules keyed by source IP or by fingerprinting mechanisms such as JA3 and JA4. In theory, you could set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;low limit&lt;/strong&gt; such as 10 requests per minute, which blocks scanners effectively but risks blocking legitimate high-throughput customers.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;high limit&lt;/strong&gt;, which lets scanners creep through, especially if attackers distribute requests across IPs or devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is an uncomfortable trade-off: too low hurts real users, too high fails to stop attackers.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS WAF's View of Origin Responses and ATP Rules
&lt;/h2&gt;

&lt;p&gt;By default, custom AWS WAF rules only inspect request attributes. They do not know whether your origin returned &lt;code&gt;200 OK&lt;/code&gt; or &lt;code&gt;404 Not Found&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The only built-in AWS WAF rules that inspect responses are the &lt;strong&gt;Account Takeover Prevention (ATP)&lt;/strong&gt; managed rules. Those require you to map login fields and are designed for authentication endpoints, not static file downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Lambda@Edge Alone Cannot Solve It
&lt;/h2&gt;

&lt;p&gt;Lambda@Edge runs per request and has no built-in shared global state. It cannot maintain counters across all executions, so by itself it cannot enforce a global request threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Hybrid Approach: WAF + Lambda@Edge
&lt;/h2&gt;

&lt;p&gt;You can combine WAF's global counting capabilities with Lambda@Edge's ability to modify HTTP responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Primary WAF Rate-Limit Rule (Soft Threshold)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; Rate-based statement, for example 10 requests per 5 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; &lt;code&gt;Count&lt;/code&gt; (not &lt;code&gt;Block&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom response header:&lt;/strong&gt; Insert &lt;code&gt;X-RateLimit-Exceeded: true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS WAF prefixes custom header names with &lt;code&gt;x-amzn-waf-&lt;/code&gt;. So if you specify &lt;code&gt;X-RateLimit-Exceeded&lt;/code&gt;, downstream components will see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x-amzn-waf-x-ratelimit-exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Secondary WAF Rate-Limit Rule (Hard Threshold)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; Rate-based statement with a much higher threshold, for example 1,000 requests per 5 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; &lt;code&gt;Block&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This immediately stops heavy-volume attacks at the WAF layer, prevents excessive Lambda@Edge invocations, and reduces cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Lambda@Edge Function (Origin Response Trigger)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger:&lt;/strong&gt; CloudFront Origin Response event
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// AWS WAF prefixes headers with x-amzn-waf-&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-amzn-waf-x-ratelimit-exceeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;429&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;statusDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too Many Requests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Rate Limit Reached&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Please try again later.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Associate and Deploy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Attach your Web ACL, containing both rate-limit rules and any ATP group you choose to use, to the CloudFront distribution.&lt;/li&gt;
&lt;li&gt;Deploy the Lambda@Edge function through the CloudFront console or the AWS CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Legitimate Access
&lt;/h3&gt;

&lt;p&gt;Repeatedly fetch an existing file. The soft-limit counter will increment, but users will still receive the file until the hard threshold is crossed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scanning Attempts
&lt;/h3&gt;

&lt;p&gt;Request many non-existent URLs. Errors quickly hit the soft threshold, causing your custom &lt;code&gt;429&lt;/code&gt; response page. Extreme traffic volumes hit the hard threshold and are blocked at WAF before Lambda@Edge runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo video
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/FtCMrJetjWo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of This Pattern
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precision:&lt;/strong&gt; Rate limits are tied to actual &lt;code&gt;Not Found&lt;/code&gt; or error responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Experience:&lt;/strong&gt; Legitimate customers getting &lt;code&gt;200 OK&lt;/code&gt; responses are not blocked unless they truly exceed your thresholds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Efficiency:&lt;/strong&gt; High-volume attacks are stopped before Lambda@Edge runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS-native design:&lt;/strong&gt; Uses AWS WAF, CloudFront, and Lambda@Edge without adding external state stores or proxy layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/waf/latest/APIReference/API_RateBasedStatement.html" rel="noopener noreferrer"&gt;AWS WAF Rate-based rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/waf/latest/APIReference/API_CustomHTTPHeader.html" rel="noopener noreferrer"&gt;AWS WAF Custom HTTP headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-for-blocked-requests.html" rel="noopener noreferrer"&gt;AWS WAF Custom responses for Block actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-atp.html" rel="noopener noreferrer"&gt;AWS WAF Fraud Control ATP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on LinkedIn on May 8, 2025.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>waf</category>
      <category>cloudfront</category>
      <category>security</category>
    </item>
    <item>
      <title>Making Amazon Bedrock AgentCore Gateway Accessible (Only Through CloudFront)</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Mon, 16 Feb 2026 19:21:51 +0000</pubDate>
      <link>https://dev.to/aws-builders/making-amazon-bedrock-agentcore-gateway-accessible-only-through-cloudfront-ha1</link>
      <guid>https://dev.to/aws-builders/making-amazon-bedrock-agentcore-gateway-accessible-only-through-cloudfront-ha1</guid>
      <description>&lt;p&gt;tags: aws, architecture, genai, security, AWScommunity, AWScommunityBuilders&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6wp6js60t0avbdylojqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6wp6js60t0avbdylojqo.png" alt="aws architecture genai cloud security" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Amazon Bedrock AgentCore Gateway makes it straightforward to expose GenAI-powered APIs.&lt;/p&gt;

&lt;p&gt;AWS provides an official pattern for attaching a custom domain using CloudFront:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Client -&amp;gt; CloudFront -&amp;gt; AgentCore Gateway&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Official documentation:&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-custom-domains.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-custom-domains.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For many use cases, this approach is sufficient.&lt;/p&gt;

&lt;p&gt;But what if the requirement is stronger?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This endpoint must only be reachable through a specific CloudFront distribution.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is where architectural boundaries matter.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Hidden Exposure Problem
&lt;/h2&gt;

&lt;p&gt;Even when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront is placed in front&lt;/li&gt;
&lt;li&gt;AWS WAF is attached&lt;/li&gt;
&lt;li&gt;A custom domain is configured&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying AgentCore Gateway service endpoint remains publicly accessible.&lt;/p&gt;

&lt;p&gt;CloudFront functions as a reverse proxy, not a strict isolation boundary.&lt;/p&gt;

&lt;p&gt;AgentCore supports resource-based policies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/resource-based-policies.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/resource-based-policies.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These policies primarily control &lt;strong&gt;who&lt;/strong&gt; can invoke the gateway (IAM principals, AWS accounts, and supported condition keys).&lt;/p&gt;

&lt;p&gt;This model works well for internal service-to-service communication.&lt;/p&gt;

&lt;p&gt;However, for public-facing APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;External consumers do not authenticate using IAM.&lt;/li&gt;
&lt;li&gt;Authentication typically relies on OAuth, JWT, API keys, or custom headers.&lt;/li&gt;
&lt;li&gt;The endpoint must remain internet-reachable for legitimate users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, even if legitimate traffic flows through CloudFront, the underlying Gateway endpoint may still be reachable directly from the internet unless additional controls are introduced.&lt;/p&gt;

&lt;p&gt;IAM-based restrictions do not remove that exposure because the service endpoint itself remains public.&lt;/p&gt;

&lt;p&gt;For environments with strict ingress isolation requirements, this becomes a critical gap.&lt;/p&gt;


&lt;h2&gt;
  
  
  Compare This to S3 + CloudFront (OAI / OAC)
&lt;/h2&gt;

&lt;p&gt;Amazon S3 provides a native enforcement primitive:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only CloudFront can access this bucket.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With CloudFront Origin Access Identity (OAI) or Origin Access Control (OAC), an S3 bucket can be made private and restricted to a specific CloudFront distribution.&lt;/p&gt;

&lt;p&gt;Example S3 bucket policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCloudFrontAccessOnly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E123ABC456XYZ"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-private-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bucket is not public.&lt;/li&gt;
&lt;li&gt;Direct internet access fails.&lt;/li&gt;
&lt;li&gt;Only the CloudFront identity is permitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgentCore does not currently provide an equivalent isolation construct.&lt;/p&gt;

&lt;p&gt;An additional boundary must therefore be introduced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture for Enforcing CloudFront-Only Access
&lt;/h2&gt;

&lt;p&gt;To enforce CloudFront-only reachability, the following architecture can be implemented:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzg0eu0w4u8cfh5jglszi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzg0eu0w4u8cfh5jglszi.png" alt="Architecture for Enforcing CloudFront-Only Access" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public entry point: &lt;code&gt;ext-clients-api-57x9abc.mycompany.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Required Host header downstream: &lt;code&gt;gw-abc123.gateway.bedrock-agentcore.us-east-1.amazonaws.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Enforcing CloudFront-Only at the Network Layer
&lt;/h2&gt;

&lt;p&gt;The Application Load Balancer (ALB) is publicly addressable, but it does not need to be publicly reachable.&lt;/p&gt;

&lt;p&gt;Inbound traffic can be restricted using &lt;strong&gt;AWS Managed Prefix Lists for CloudFront&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/working-with-aws-managed-prefix-lists.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/userguide/working-with-aws-managed-prefix-lists.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS maintains a managed prefix list containing CloudFront origin-facing IP ranges.&lt;/p&gt;

&lt;p&gt;Attach this managed prefix list to the ALB security group and allow inbound HTTPS only from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;com.amazonaws.global.cloudfront.origin-facing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only CloudFront edge locations can establish TCP connections to the ALB.&lt;/li&gt;
&lt;li&gt;Direct internet traffic to the ALB is blocked at the security group level.&lt;/li&gt;
&lt;li&gt;IP spoofing cannot bypass the restriction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without CloudFront, the ALB is not reachable.&lt;/p&gt;

&lt;p&gt;This creates a true network enforcement boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where AWS WAF and DDoS Protection Fit
&lt;/h2&gt;

&lt;p&gt;This design enables layered protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudFront (Edge Layer)
&lt;/h3&gt;

&lt;p&gt;CloudFront is automatically protected by &lt;strong&gt;AWS Shield Standard&lt;/strong&gt;, which provides DDoS mitigation at the edge.&lt;/p&gt;

&lt;p&gt;AWS WAF at CloudFront can provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate-based rules (anti-abuse and scraping protection)&lt;/li&gt;
&lt;li&gt;IP reputation filtering&lt;/li&gt;
&lt;li&gt;Geo restrictions&lt;/li&gt;
&lt;li&gt;AWS Managed Rule Groups&lt;/li&gt;
&lt;li&gt;AWS Bot Control&lt;/li&gt;
&lt;li&gt;Account Takeover Prevention (ATP)&lt;/li&gt;
&lt;li&gt;Authentication field inspection&lt;/li&gt;
&lt;li&gt;Header validation&lt;/li&gt;
&lt;li&gt;Payload size limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Malicious traffic can be filtered before reaching the infrastructure.&lt;/p&gt;




&lt;h3&gt;
  
  
  ALB (Ingress Layer)
&lt;/h3&gt;

&lt;p&gt;AWS WAF can also be attached to the ALB to enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification of a secret header injected by CloudFront&lt;/li&gt;
&lt;li&gt;Strict Host header validation&lt;/li&gt;
&lt;li&gt;Additional managed rule groups&lt;/li&gt;
&lt;li&gt;Authentication flow inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if the ALB DNS name is discovered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct connection attempts fail due to managed prefix list restrictions.&lt;/li&gt;
&lt;li&gt;Spoofed requests fail due to header validation and WAF rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This results in three enforcement layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Edge (Shield + WAF)&lt;/li&gt;
&lt;li&gt;Network (managed prefix list restriction)&lt;/li&gt;
&lt;li&gt;Application (ALB WAF + listener rules)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Technical Challenge: TLS vs Host Header
&lt;/h2&gt;

&lt;p&gt;When placing an ALB in front of AgentCore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The ALB must terminate TLS using a certificate for a custom domain, for example:&lt;br&gt;
&lt;code&gt;ext-clients-api-57x9abc.mycompany.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Downstream routing may require a different HTTP &lt;code&gt;Host&lt;/code&gt; header, for example:&lt;br&gt;
&lt;code&gt;gw-abc123.gateway.bedrock-agentcore.us-east-1.amazonaws.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These belong to different protocol layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS SNI hostname controls certificate validation.&lt;/li&gt;
&lt;li&gt;HTTP Host header controls routing logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgentCore does not support accepting arbitrary custom domains inbound.&lt;/p&gt;

&lt;p&gt;This mismatch must be resolved before traffic reaches AgentCore.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: CloudFront Origin Modification
&lt;/h2&gt;

&lt;p&gt;CloudFront Functions allow modification of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;domainName&lt;/code&gt; (origin connection hostname for TLS/SNI)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hostHeader&lt;/code&gt; (HTTP Host header sent to the origin)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudfront&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN_DOMAIN_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ext-clients-api-57x9abc.mycompany.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OVERRIDE_HOST_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gw-abc123.gateway.bedrock-agentcore.us-east-1.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRequestOrigin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN_DOMAIN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hostHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OVERRIDE_HOST_HEADER&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS validation against the ALB certificate.&lt;/li&gt;
&lt;li&gt;ALB routing based on the required Host header.&lt;/li&gt;
&lt;li&gt;External clients to interact only with the custom domain.&lt;/li&gt;
&lt;li&gt;AgentCore to remain accessible only through the controlled CloudFront -&amp;gt; ALB -&amp;gt; PrivateLink path.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Outcome
&lt;/h2&gt;

&lt;p&gt;This architecture provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom domain support&lt;/li&gt;
&lt;li&gt;Edge-level DDoS protection (Shield Standard)&lt;/li&gt;
&lt;li&gt;Advanced WAF protections (rate limiting, ATP, bot control, managed rules)&lt;/li&gt;
&lt;li&gt;Network-level CloudFront-only enforcement&lt;/li&gt;
&lt;li&gt;Ingress validation at ALB&lt;/li&gt;
&lt;li&gt;Private connectivity via VPC endpoint&lt;/li&gt;
&lt;li&gt;Strong ingress isolation for GenAI workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default AWS pattern is well suited for standard deployments.&lt;/p&gt;

&lt;p&gt;When strict CloudFront-only reachability is required, an architectural boundary is necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudFront + managed prefix lists + ALB + PrivateLink + layered WAF provides that boundary.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>security</category>
      <category>agentcoregateway</category>
    </item>
    <item>
      <title>AWS WAF Intro Guide</title>
      <dc:creator>Alexey Baltacov</dc:creator>
      <pubDate>Thu, 06 Feb 2025 23:00:31 +0000</pubDate>
      <link>https://dev.to/alexeybaltacov/aws-waf-intro-guide-25a3</link>
      <guid>https://dev.to/alexeybaltacov/aws-waf-intro-guide-25a3</guid>
      <description>&lt;p&gt;With this guide you will not become an AWS WAF expert, but will get a bit of taste about its capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Introduction and Overview of AWS WAF
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AWS Web Application Firewall (WAF)&lt;/strong&gt; is a managed security service designed to help protect your web applications or APIs from common web exploits and bots. AWS WAF lets you create rules to monitor (count), block, or allow traffic based on criteria such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP addresses&lt;/li&gt;
&lt;li&gt;HTTP headers&lt;/li&gt;
&lt;li&gt;URI paths&lt;/li&gt;
&lt;li&gt;Request size&lt;/li&gt;
&lt;li&gt;SQL injection attempts&lt;/li&gt;
&lt;li&gt;Cross-Site Scripting (XSS) patterns&lt;/li&gt;
&lt;li&gt;Request rate (rate-based rules)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Additionally&lt;/strong&gt;, when you use AWS WAF, you automatically get the protections of &lt;strong&gt;AWS Shield Standard&lt;/strong&gt; &lt;strong&gt;at no additional cost&lt;/strong&gt;. &lt;strong&gt;AWS Shield Standard&lt;/strong&gt; is a built-in DDoS protection service for AWS resources (including CloudFront and Route 53) that mitigates common network and transport layer attacks. Together, AWS WAF and AWS Shield Standard provide a layered defense approach: WAF focuses on Layer 7 (application layer) threats, while Shield Standard covers volumetric and SYN flood-type attacks (Layers 3 and 4).&lt;/p&gt;

&lt;h2&gt;
  
  
  1.1 Resources Protected by AWS WAF
&lt;/h2&gt;

&lt;p&gt;You can attach AWS WAF to the following AWS resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon CloudFront (for global distribution and caching)&lt;/li&gt;
&lt;li&gt;Amazon API Gateway (REST APIs)&lt;/li&gt;
&lt;li&gt;Application Load Balancer (ALB)&lt;/li&gt;
&lt;li&gt;AWS AppSync (GraphQL APIs)&lt;/li&gt;
&lt;li&gt;Amazon Cognito user pools&lt;/li&gt;
&lt;li&gt;AWS App Runner service&lt;/li&gt;
&lt;li&gt;AWS Verified Access instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, when you use &lt;strong&gt;CloudFront&lt;/strong&gt; with &lt;strong&gt;AWS WAF&lt;/strong&gt;, you can protect &lt;strong&gt;any external or internal origin behind CloudFront&lt;/strong&gt;, including &lt;strong&gt;on-premises servers or third-party cloud environments&lt;/strong&gt;. CloudFront acts as a secure reverse proxy, letting you apply WAF inspection rules to all inbound requests before they reach your origin, regardless of whether that origin is hosted inside or outside of AWS.&lt;/p&gt;

&lt;p&gt;Attaching WAF to these resources allows you to inspect and filter incoming requests based on rules you define (custom rules) or rules you enable from AWS Managed Rule Sets, all while benefiting from the baseline DDoS protections of AWS Shield Standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Applying AWS WAF: CloudFront vs. Other Resources
&lt;/h2&gt;

&lt;h2&gt;
  
  
  2.1 AWS WAF with CloudFront
&lt;/h2&gt;

&lt;p&gt;When integrated with CloudFront, AWS WAF inspects and can block traffic at the edge, before it reaches your origin. Key advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global Edge Presence&lt;/strong&gt;: Requests are processed at CloudFront edge locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN and Caching&lt;/strong&gt;: CloudFront’s CDN reduces latency and offloads your origin by caching content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Reverse Proxy&lt;/strong&gt;: By placing CloudFront in front of your application (whether it’s hosted within AWS, on-premises, or another cloud), you effectively use it as a secure reverse proxy that can filter traffic globally.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS Shield Standard&lt;/strong&gt;: Automatically protects CloudFront distributions (and other supported AWS resources) against most common DDoS attacks at no extra cost.&lt;br&gt;
If your application needs more nuanced logic than WAF alone can provide, you can extend functionality using:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CloudFront Functions&lt;/strong&gt;: Lightweight JavaScript functions for short-running tasks (up to 1ms) like inspecting headers or rewriting URIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda@Edge&lt;/strong&gt;: A more robust solution if you need to read or modify request/response bodies, implement complex authentication flows, or handle sophisticated transformations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;_&lt;strong&gt;Important:&lt;/strong&gt; If AWS WAF blocks a request at the edge, CloudFront Functions and Lambda@Edge will not trigger for that request.&lt;br&gt;
_&lt;/p&gt;

&lt;h2&gt;
  
  
  2.2 AWS WAF with Regional Services
&lt;/h2&gt;

&lt;p&gt;AWS WAF can also operate at a regional level by attaching to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application Load Balancer (ALB)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon API Gateway&lt;/strong&gt; (REST APIs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS AppSync&lt;/strong&gt; (GraphQL APIs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Cognito&lt;/strong&gt; user pools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS App Runner&lt;/strong&gt; service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Verified Access&lt;/strong&gt; instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach is suitable for applications or microservices that do not require global edge distribution or caching. Key differences from the CloudFront approach include:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Latency and Caching
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront + WAF&lt;/strong&gt;: Security measures at the edge, lower latency, potential cost savings from caching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional Services + WAF&lt;/strong&gt;: Traffic is inspected regionally without inherent CDN caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Scope and Customization
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; can use &lt;strong&gt;CloudFront Functions&lt;/strong&gt; or &lt;strong&gt;Lambda@Edge&lt;/strong&gt; for advanced transformations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional Services&lt;/strong&gt; can still use custom or managed WAF rules, but do not benefit from global caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Protecting External Origins
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront + WAF&lt;/strong&gt; can protect &lt;strong&gt;non-AWS origins&lt;/strong&gt; by pointing the distribution to external servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional services&lt;/strong&gt; typically secure traffic within AWS unless you design a specific pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. AWS Managed Rules
&lt;/h2&gt;

&lt;p&gt;AWS Managed Rules are pre-configured, continuously updated rule sets provided by AWS to protect against a wide range of threats. By enabling these managed rules in your AWS WAF, you can quickly cover many common web attacks without having to define every pattern or behavior manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Coverage for OWASP Top 10
&lt;/h3&gt;

&lt;p&gt;Many AWS Managed Rules are designed to help address &lt;strong&gt;OWASP Top 10&lt;/strong&gt; vulnerabilities, such as:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Injection&lt;/strong&gt; (SQLi, NoSQL, Command Injection)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;AWSManagedRulesSQLiRuleSet&lt;/strong&gt; identifies and blocks SQL injection attempts.&lt;/li&gt;
&lt;li&gt;Common malicious payloads and known injection vectors are covered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Broken Authentication and Session Management
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Combined with &lt;strong&gt;Account Takeover Prevention (ATP)&lt;/strong&gt;, WAF can detect brute force or credential stuffing attempts on login endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Sensitive Data Exposure
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Managed Rules can flag suspicious payloads or unusual request patterns that might indicate data exfiltration (in conjunction with rate-based rules and SSL/TLS).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  XML External Entities (XXE)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Certain AWS Managed Rule Groups include checks for XML-based threats and malicious entity references.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Broken Access Control
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;While some aspects of access control are application-specific, AWS Managed Rules can detect attempts to bypass security controls using known exploit patterns, suspicious HTTP methods, or path tampering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Security Misconfiguration
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Common Rule Sets automatically update to address vulnerabilities in common frameworks and platforms, reducing the risk of misconfiguration exposure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cross-Site Scripting (XSS)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWSManagedRulesCommonRuleSet&lt;/strong&gt; includes signatures for detecting malicious scripts and XSS payloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Insecure Deserialization
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Some advanced Managed Rule Groups check for known malicious serialization formats and exploit payloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Using Components with Known Vulnerabilities
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;AWS regularly updates the Managed Rule Sets to address newly disclosed CVEs, helping you keep pace with emerging threats.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Insufficient Logging and Monitoring
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Although logging itself is not a “rule,” AWS WAF integrates with S3, CloudWatch, and Kinesis Firehose for monitoring. Suspicious requests are clearly tagged and logged, aiding your threat detection strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Examples of AWS Managed Rule Sets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWSManagedRulesCommonRuleSet&lt;/strong&gt;: Covers a broad range of general vulnerabilities, including XSS, HTTP request smuggling, and some injection signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSManagedRulesSQLiRuleSet&lt;/strong&gt;: Specifically targets SQL injection patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSManagedRulesAmazonIpReputationList&lt;/strong&gt;: Blocks or labels requests from known malicious IP addresses or botnets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSManagedRulesLinuxRuleSet&lt;/strong&gt; (or similar specialized sets): Target vulnerabilities specific to Linux-based systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.3 Complementing Managed Rules with Custom Logic
&lt;/h3&gt;

&lt;p&gt;While AWS Managed Rules provide excellent baseline coverage, you should &lt;strong&gt;monitor&lt;/strong&gt; traffic in “Count” mode initially to identify potential false positives and tune your rules. You can then add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom rules for application-specific logic (e.g., blocking certain user agents or disallowed query parameters).&lt;/li&gt;
&lt;li&gt;Rate-based rules to mitigate brute force or scraping attempts.&lt;/li&gt;
&lt;li&gt;Account Takeover Prevention (ATP) for deeper login endpoint protection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This &lt;strong&gt;layered approach&lt;/strong&gt; (managed rules plus custom refinements) helps ensure robust, OWASP-aligned protection without sacrificing application availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. AWS WAF Fraud Control – Account Takeover Prevention (ATP)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Overview of ATP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Account Takeover Prevention (ATP)&lt;/strong&gt; is part of &lt;strong&gt;AWS WAF Fraud Control&lt;/strong&gt;, designed to detect and mitigate unauthorized login attempts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analyzes Login Attempts&lt;/strong&gt;: Monitors login traffic to identify suspicious behavior (credential stuffing, brute force).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk-Based Scoring&lt;/strong&gt;: ATP assigns risk scores to login attempts based on factors like repeated login failures or IP reputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with WAF&lt;/strong&gt;: You can create WAF rules to allow, count, block, or require CAPTCHA on requests flagged by ATP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By spotting anomalies in login patterns, ATP helps prevent account takeover incidents before they escalate.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Benefits of Using Email Addresses as Usernames with ATP
&lt;/h3&gt;

&lt;p&gt;When combining ATP with email-based usernames (your application should use usernames in email format), you gain several security and operational advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Easier Parsing and Validation: An email format (&lt;a href="mailto:username@domain.com"&gt;username@domain.com&lt;/a&gt;) is consistent, enabling simpler checks for malicious or disposable domains.&lt;/li&gt;
&lt;li&gt;Domain Intelligence: ATP or custom WAF rules can label or block logins from suspicious or disposable email domains.&lt;/li&gt;
&lt;li&gt;Reduced Guesswork: Attackers must guess valid email addresses, which can be less trivial than typical username formats like admin or test.&lt;/li&gt;
&lt;li&gt;Improved Logging: Email addresses provide a clearly identifiable username in the logs for investigating suspicious activity.&lt;/li&gt;
&lt;li&gt;Easier MFA and Recovery: Users often prefer logging in with an email address, simplifying password resets and integrating MFA flows.&lt;/li&gt;
&lt;li&gt;Leverage Compromised Credentials Databases:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;AWS obtains threat intelligence including email/password pairs from public data breaches to help detect credential stuffing or attempts using known compromised credentials.&lt;/li&gt;
&lt;li&gt;Because users commonly reuse email addresses across different services, ATP can cross-reference these compromised credential sets more effectively when your primary username field is an email.&lt;/li&gt;
&lt;li&gt;This synergy helps AWS WAF quickly flag suspicious login attempts, especially when an attacker attempts known leaked email/password pairs from security breaches.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Best Practices and Well-Architected Approach for Building Your Rule Base
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Start with Managed Rules, Then Layer Custom Rules
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline Protections (Managed Rules)&lt;/strong&gt;: Quickly address common threats (SQLi, XSS, bad bots) by enabling relevant AWS Managed Rule Sets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Rules&lt;/strong&gt;: Tailor rules to your application’s business logic (e.g., block suspicious headers, limit request sizes, or restrict certain paths).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-Based Rules&lt;/strong&gt;: Mitigate DDoS or brute-force attacks by capping requests from a single IP address over a specified period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS Challenge and Captcha&lt;/strong&gt;: Implement JS challenge and captcha in your application in order to reduce DDoS by automated browsers, CLI  and other tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.2 “Count” Mode + Labels, Then Block
&lt;/h3&gt;

&lt;p&gt;A major best practice is to start all rules in “Count” mode and add labels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure each rule to Count rather than block initially.&lt;/li&gt;
&lt;li&gt;Assign a label to requests that match the rule (e.g., SuspiciousSQLi, XSSAttempt).&lt;/li&gt;
&lt;li&gt;Observe logs to verify these matches are legitimate threats or anomalies.&lt;/li&gt;
&lt;li&gt;Implement a final, catch-all blocking rule that blocks requests carrying labels deemed malicious.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach helps reduce false positives while ensuring you have visibility into what would be blocked before actively cutting off traffic as well as centralized control over custom block page headers and responses&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Extend with CloudFront Functions or Lambda@Edge
&lt;/h3&gt;

&lt;p&gt;In cases where AWS WAF rules do not cover a specific use case or you need more dynamic logic, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront Functions&lt;/strong&gt;: Lightweight JavaScript for short-running tasks (up to 1 ms) like inspecting headers, rewriting URIs, or adding custom headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda@Edge&lt;/strong&gt;: A more robust solution for reading/modifying request or response bodies, implementing complex authentication flows, or dynamic transformations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: If WAF blocks a request, CloudFront Functions or Lambda@Edge will not run for that request.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Align with AWS Well-Architected Security Pillar
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Design your WAF rule base according to AWS’s Well-Architected Security Pillar:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identification&lt;/strong&gt;: Label and log events to understand potential threats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proactive Protection&lt;/strong&gt;: Use a combination of managed and custom rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least Privilege&lt;/strong&gt;: Only allow known safe patterns or domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt;: Deploy and manage WAF using Infrastructure as Code (CloudFormation, Terraform) for consistency and repeatability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Rule Groups and Rule Labels
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 Rule Groups
&lt;/h3&gt;

&lt;p&gt;A Rule Group is a container for one or more WAF rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Managed Rule Groups&lt;/strong&gt;: Quick to enable, provide coverage for broad classes of attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Rule Groups&lt;/strong&gt;: Let you organize your own rules by function (e.g., IP restrictions, user-agent blocks, country blocks).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  6.2 Rule Labels
&lt;/h4&gt;

&lt;p&gt;When a request matches a rule, WAF can attach labels for tracking or later decision-making. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;SQLiDetected&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;BadBotTraffic&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;DisallowedCountry&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can create a final rule that blocks requests carrying specific labels. Labels also appear in WAF logs, helping you analyze and correlate different rule matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Custom Responses in WAF Rule Actions
&lt;/h2&gt;

&lt;p&gt;AWS WAF supports custom responses for Block actions. Instead of returning a generic 403 Forbidden, you can define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom HTTP Status&lt;/strong&gt; Code (e.g., 403, 404, 502, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Headers&lt;/strong&gt; (e.g., to convey an error reason to the client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom HTML or text Body&lt;/strong&gt; for the response
This feature is useful when you want to provide a branded or instructive error page, or add extra troubleshooting information for the user. Since the request is already blocked, no further edge functions (CloudFront Functions or Lambda@Edge) will execute for that request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: You might choose to return a 403 status with a short HTML body:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{&lt;br&gt;
  "Name": "MyWebACL",&lt;br&gt;
  "Scope": "CLOUDFRONT",&lt;br&gt;
  "DefaultAction": { "Allow": {} },&lt;br&gt;
  "Rules": [&lt;br&gt;
    {&lt;br&gt;
      "Name": "BlockWithCustomResponse",&lt;br&gt;
      "Priority": 1,&lt;br&gt;
      "Statement": {&lt;br&gt;
        "ByteMatchStatement": {&lt;br&gt;
          "FieldToMatch": { "SingleHeader": { "Name": "x-bad-header" } },&lt;br&gt;
          "PositionalConstraint": "EXACTLY",&lt;br&gt;
          "SearchString": "bad-value",&lt;br&gt;
          "TextTransformations": [{ "Priority": 0, "Type": "NONE" }]&lt;br&gt;
        }&lt;br&gt;
      },&lt;br&gt;
      "Action": {&lt;br&gt;
        "Block": {&lt;br&gt;
          "CustomResponse": {&lt;br&gt;
            "ResponseCode": 403,&lt;br&gt;
            "CustomResponseBodyKey": "CustomHTMLResponse",&lt;br&gt;
            "ResponseHeaders": [&lt;br&gt;
              { "Name": "X-Custom-Error", "Value": "BlockedByWAF" }&lt;br&gt;
            ]&lt;br&gt;
          }&lt;br&gt;
        }&lt;br&gt;
      },&lt;br&gt;
      "VisibilityConfig": {&lt;br&gt;
        "SampledRequestsEnabled": true,&lt;br&gt;
        "CloudWatchMetricsEnabled": true,&lt;br&gt;
        "MetricName": "BlockWithCustomResponseRule"&lt;br&gt;
      }&lt;br&gt;
    }&lt;br&gt;
  ],&lt;br&gt;
  "CustomResponseBodies": {&lt;br&gt;
    "CustomHTMLResponse": {&lt;br&gt;
      "ContentType": "TEXT_HTML",&lt;br&gt;
      "Content": "&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Access Denied&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Your request has been blocked.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;"&lt;br&gt;
    }&lt;br&gt;
  },&lt;br&gt;
  "VisibilityConfig": {&lt;br&gt;
    "SampledRequestsEnabled": true,&lt;br&gt;
    "CloudWatchMetricsEnabled": true,&lt;br&gt;
    "MetricName": "MyACLMetric"&lt;br&gt;
  }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this example, any request including a header &lt;strong&gt;x-bad-header&lt;/strong&gt;: &lt;strong&gt;bad-value&lt;/strong&gt; is blocked, returning a &lt;strong&gt;403&lt;/strong&gt; code with a custom HTML body.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Limitations
&lt;/h2&gt;

&lt;p&gt;While AWS WAF is powerful and flexible, you should be aware of some important constraints and limitations:&lt;/p&gt;

&lt;h4&gt;
  
  
  Rule Capacity (WCU – WAF Capacity Units)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Each rule (or statement within a rule) consumes a certain number of WCUs based on its complexity. For example, ByteMatchStatement or IP match conditions generally have lower WCU usage, while Regex match statements and complex logical operators can use more.&lt;/li&gt;
&lt;li&gt;There is a maximum WCU limit for each Web ACL. If your rules exceed that limit, you cannot add them all to the same Web ACL. You may need to simplify your rules or optimize your rule groups.&lt;/li&gt;
&lt;li&gt;Monitor your rule WCU usage (in the AWS console or via the CLI) to ensure you stay within allowed capacity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Regex Complexity
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Very complex regex patterns can quickly use up available WCUs. Simplify your patterns or break them into multiple statements where possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Logging Latency
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;WAF logs are almost real time when using Cloudwatch and typically appear with a short delay in S3 or Kinesis&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Regional vs. Global
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;WAF on CloudFront is global; on ALB, API Gateway, etc., it’s regional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cost
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Each rule, managed rule group, and request passing through WAF contributes to your monthly bill. Monitor usage carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Inspection Header Size
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;WAF regional inspects up to a certain size for request headers/body (e.g., first 16 KB). &lt;/li&gt;
&lt;li&gt;WAF Global (Cloudfront) supports up to 64 KB headers/body inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Requests exceeding this limit may not be fully inspected.&lt;/p&gt;

&lt;h4&gt;
  
  
  Blocking Halts Edge Functions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;If a request is blocked by WAF, CloudFront Functions or Lambda@Edge will not run for that request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Practical Information about Working with Labels
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Defining Labels&lt;/strong&gt;: Specify labels in your WAF rule actions (console or CLI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combining Labels&lt;/strong&gt;: Multiple rules can label the same request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match on Labels&lt;/strong&gt;: A final rule can check if a request has any suspicious labels and block it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt;: Inspect WAF logs to see which labels are assigned and confirm the accuracy of your rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Protecting Resources Outside AWS Using CloudFront
&lt;/h2&gt;

&lt;p&gt;One advantage of &lt;strong&gt;CloudFront + WAF&lt;/strong&gt; is the ability to protect &lt;strong&gt;external (non-AWS) origins&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a CloudFront distribution that points to your on-premises or third-party cloud origin.&lt;/li&gt;
&lt;li&gt;Attach a WAF Web ACL to that distribution.&lt;/li&gt;
&lt;li&gt;Enable caching for static or cacheable content to reduce load on the external origin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Secure Reverse Proxy&lt;/strong&gt;: CloudFront, acting as a secure reverse proxy, inspects all traffic with WAF at the edge before forwarding it to your external servers.&lt;br&gt;
This setup provides global edge security and performance benefits even if your origin is not hosted on AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. AWS Managed Protections for OWASP Top 10
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The OWASP Top 10&lt;/strong&gt; is a standard awareness document outlining the most critical web application security risks. AWS WAF addresses several OWASP Top 10 vulnerabilities via AWS Managed Rules and additional features:&lt;/p&gt;

&lt;h4&gt;
  
  
  Injection
&lt;/h4&gt;

&lt;p&gt;(SQLi, NoSQL, Command Injection)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWSManagedRulesSQLiRuleSet&lt;/strong&gt; helps block SQL injection attempts.&lt;br&gt;
Custom rules can detect command injection patterns.&lt;br&gt;
Broken Authentication and Session Management&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ATP&lt;/strong&gt; helps mitigate brute force or credential stuffing.&lt;br&gt;
Rate-based rules can address repeated login attempts.&lt;br&gt;
Sensitive Data Exposure&lt;/p&gt;

&lt;p&gt;WAF can block suspicious payloads that may indicate data exfiltration.&lt;br&gt;
Combine with SSL/TLS for encrypted data in transit.&lt;/p&gt;

&lt;h4&gt;
  
  
  XML External Entities (XXE)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom or third-party&lt;/strong&gt; rules detect malicious XML patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Broken Access Control
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;WAF custom rules can enforce strict path or header-based controls.&lt;/li&gt;
&lt;li&gt;Labels help track role-based or token-based access attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Security Misconfiguration
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;AWS Managed Rule Sets automatically update to address new vulnerabilities.&lt;/li&gt;
&lt;li&gt;Custom rules detect unusual HTTP methods or suspicious headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cross-Site Scripting (XSS)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;AWSManagedRulesCommonRuleSet&lt;/strong&gt; includes patterns for typical XSS vectors.&lt;/p&gt;

&lt;h4&gt;
  
  
  Insecure Deserialization
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Advanced rule sets can detect malicious serialization formats.&lt;/li&gt;
&lt;li&gt;Using Components with Known Vulnerabilities&lt;/li&gt;
&lt;li&gt;Regular updates to AWS Managed Rules help protect against newly disclosed CVEs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Insufficient Logging and Monitoring
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;WAF logging (to S3, CloudWatch, or Kinesis) provides robust, centralized monitoring.&lt;/li&gt;
&lt;li&gt;Combine with Athena or OpenSearch for advanced log analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Pricing Model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  12.1 AWS WAF Pricing
&lt;/h3&gt;

&lt;p&gt;AWS WAF pricing typically includes four main components:&lt;/p&gt;

&lt;h4&gt;
  
  
  Web ACLs
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;You pay a monthly charge for each web ACL that you create (e.g., around USD $5 per month per web ACL, depending on the region).&lt;/li&gt;
&lt;li&gt;Rules&lt;/li&gt;
&lt;li&gt;Each rule (custom or managed) in your web ACL adds a small monthly cost - often USD $1 per rule per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Rule Groups
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Enabling an AWS Managed Rule Group or a third-party rule group from AWS Marketplace can incur a monthly subscription (for the group itself), plus the cost for each rule inside the group if applicable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Request Volume
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;You pay for each million web requests inspected by AWS WAF. The typical rate is around USD $0.60–$1.00 per million requests (exact cost varies by region).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Important:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Prices &lt;strong&gt;vary&lt;/strong&gt; by AWS Region.&lt;/li&gt;
&lt;li&gt;Third-party managed rule groups may have higher subscription costs than AWS-managed rule sets.&lt;/li&gt;
&lt;li&gt;Overuse of large or complex rules can drive up monthly costs. Monitor usage carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  12.2 CloudFront Pricing
&lt;/h3&gt;

&lt;p&gt;Amazon CloudFront has its own separate billing model. Main cost factors include:&lt;/p&gt;

&lt;h4&gt;
  
  
  Data Transfer Out
&lt;/h4&gt;

&lt;p&gt;You pay for data transferred from CloudFront edge locations to your users. Rates vary by geographic region and volume.&lt;/p&gt;

&lt;h4&gt;
  
  
  Requests
&lt;/h4&gt;

&lt;p&gt;You pay for HTTPS/HTTP requests served by CloudFront. Pricing can be on the order of USD $0.01 per 10,000 requests in many regions.&lt;/p&gt;

&lt;h4&gt;
  
  
  CloudFront Functions / Lambda@Edge
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront Functions&lt;/strong&gt;: Billed by the number of function invocations (e.g., $0.10 per 1 million invocations).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda@Edge&lt;/strong&gt;: Billed by the number of requests plus compute time (GB-seconds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimization Tip: Caching effectively in CloudFront can drastically reduce the number of origin fetches, which lowers data transfer costs and can also reduce Lambda@Edge invocations. However, caching does not reduce the number of WAF inspections, as every client request that hits CloudFront is still processed by WAF (unless previously blocked or allowed by another mechanism).&lt;/p&gt;

&lt;h2&gt;
  
  
  13. AWS WAF vs. Incapsula (Imperva) vs. Cloudflare vs. AWS WAF with CloudFront
&lt;/h2&gt;

&lt;p&gt;Below is a high-level comparison table, including a separate row for AWS WAF with CloudFront:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91imq2jtn7utk431nte0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91imq2jtn7utk431nte0.png" alt=" " width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Key Takeaways:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;AWS WAF (Regional Services): Ideal for apps that don’t require global edge caching or are purely regional.&lt;/li&gt;
&lt;li&gt;AWS WAF with CloudFront: Global edge distribution, built-in CDN and caching, extends protection to external origins. Additionally, combining AWS WAF with CloudFront Functions and Lambda@Edge provides extended customization flexibility, allowing deep control over each and every HTTP parameter within the session, from rewriting headers to injecting custom logic for advanced routing or security checks.&lt;/li&gt;
&lt;li&gt;Incapsula &amp;amp; Cloudflare: Third-party global security/CDN solutions with their own pricing, feature sets, and potential integration complexities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  14. Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AWS WAF&lt;/strong&gt; is a powerful, flexible solution for safeguarding &lt;strong&gt;both AWS-hosted and external web applications&lt;/strong&gt;. With managed rules, custom rules, labels, and a layered “count then block” strategy, you can tailor your defenses while minimizing false positives. Integrating &lt;strong&gt;CloudFront&lt;/strong&gt; adds global edge coverage, caching, and the option to use &lt;strong&gt;CloudFront Functions&lt;/strong&gt; or &lt;strong&gt;Lambda@Edge&lt;/strong&gt; for advanced logic.&lt;/p&gt;

&lt;p&gt;Furthermore, &lt;strong&gt;AWS Shield Standard&lt;/strong&gt; is included at no additional cost, protecting your AWS resources from common network and transport-layer DDoS attacks, while WAF focuses on application-layer threats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account Takeover Prevention (ATP)&lt;/strong&gt; further boosts security by analyzing login attempts for suspicious behavior, making it especially effective when users log in with email addresses. This consistency aids in detection and labeling of potentially malicious traffic and benefits from AWS’s threat intelligence on compromised credentials from public data breaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Implementation Steps&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable AWS Managed Rules for immediate protection against common attacks, including OWASP Top 10 vulnerabilities.&lt;/li&gt;
&lt;li&gt;Add Custom Rules to address your application’s unique security needs.&lt;/li&gt;
&lt;li&gt;Leverage Rule Labels and test in “Count” mode before enforcing blocks.&lt;/li&gt;
&lt;li&gt;Adopt a Final Blocking Rule that triggers on suspicious labels (e.g., SQLiDetected, ATPHighRisk).&lt;/li&gt;
&lt;li&gt;Use CloudFront as a secure reverse proxy to protect both AWS and external origins, taking advantage of edge caching.&lt;/li&gt;
&lt;li&gt;Implement ATP to mitigate unauthorized login attempts, benefiting from standardized email-based usernames and compromised credential checks.&lt;/li&gt;
&lt;li&gt;Monitor Limitations such as WAF Capacity Units (WCU), inspection header size, and overall request volume to ensure your rules function effectively at scale.&lt;/li&gt;
&lt;li&gt;Use Custom Responses in block actions for branded or instructive error pages.&lt;/li&gt;
&lt;li&gt;Leverage AWS Shield Standard for built-in DDoS protection, covering the network and transport layers at no extra cost.&lt;/li&gt;
&lt;li&gt;Manage Costs by monitoring the number of web ACLs, rules, and request volume. Optimize with CloudFront caching to reduce origin fetches.&lt;/li&gt;
&lt;li&gt;Regularly Review OWASP Top 10 and confirm your AWS Managed Rule Sets (and custom rules) align with emerging vulnerabilities.
By following these best practices and leveraging AWS WAF’s advanced features, you can establish a well-architected, scalable, and cost-effective defense against evolving threats- both within and beyond the AWS ecosystem.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
