<?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: nishikawaakira</title>
    <description>The latest articles on DEV Community by nishikawaakira (@nishikawaakira).</description>
    <link>https://dev.to/nishikawaakira</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%2F1330403%2F7ec182ca-5ffc-440a-bd5d-512ee1d134b7.jpg</url>
      <title>DEV Community: nishikawaakira</title>
      <link>https://dev.to/nishikawaakira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nishikawaakira"/>
    <language>en</language>
    <item>
      <title>Mitigating Session Hijacking with JA4 Session Binding and AWS WAF Dynamic Label Interpolation</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Sat, 18 Jul 2026 20:19:25 +0000</pubDate>
      <link>https://dev.to/aws-builders/mitigating-session-hijacking-with-ja4-session-binding-and-aws-waf-dynamic-label-interpolation-13cl</link>
      <guid>https://dev.to/aws-builders/mitigating-session-hijacking-with-ja4-session-binding-and-aws-waf-dynamic-label-interpolation-13cl</guid>
      <description>&lt;p&gt;So, how are you protecting your applications against session hijacking?&lt;/p&gt;

&lt;p&gt;For many web applications, the harsh reality is that once a session cookie is stolen, the game is basically over. With infostealer malware now exfiltrating cookies along with everything else, the assumption that “possession of the cookie equals proof of identity” is gradually becoming less reliable.&lt;/p&gt;

&lt;p&gt;In this article, I will show how to use AWS WAF Dynamic Label Interpolation to pass a JA4 TLS fingerprint to your application and bind it to the user’s session. With this design, even if an attacker steals a session cookie, the application can detect its use when the attacker’s TLS stack differs from that of the legitimate user.&lt;/p&gt;

&lt;p&gt;I previously wrote about mitigating session hijacking with an “&lt;a href="https://dev.to/aws-builders/implementing-a-ja4h-equivalent-fingerprint-with-amazon-cloudfront-functions-to-mitigate-bot-traffic-4jbm"&gt;Impossible Location&lt;/a&gt;” check. You can think of this article as a location-independent version of that approach.&lt;/p&gt;

&lt;p&gt;The other articles in this series are listed below. This article is also written to stand on its own, so you can continue reading without them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Article 1:&lt;/strong&gt; &lt;a href="https://dev.to/aws-builders/implementing-a-ja4h-equivalent-fingerprint-with-amazon-cloudfront-functions-to-mitigate-bot-traffic-4jbm"&gt;Implementing a JA4H-Equivalent Fingerprint with Amazon CloudFront Functions to Mitigate Bot Traffic&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article 2:&lt;/strong&gt; &lt;a href="https://dev.to/aws-builders/building-a-false-positive-resilient-challenge-architecture-with-aws-waf-dynamic-label-interpolation-e96"&gt;Building a False-Positive-Resilient Challenge Architecture with AWS WAF Dynamic Label Interpolation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Forwarding JA4 to the Application: A Recap of Article 2
&lt;/h2&gt;

&lt;p&gt;JA4 is a fingerprint of the client implementation calculated from the TLS ClientHello. Chrome, curl, and Python’s &lt;code&gt;requests&lt;/code&gt; library produce different values because they use different TLS stacks.&lt;/p&gt;

&lt;p&gt;The important point is that a JA4 fingerprint cannot be reproduced merely by stealing a cookie. JA4 reflects characteristics of the TLS stack itself, so producing the same value requires the attacker to imitate the victim’s TLS implementation as well.&lt;/p&gt;

&lt;p&gt;AWS WAF Dynamic Label Interpolation lets you forward JA4 to the origin with a single &lt;code&gt;Count&lt;/code&gt; rule. No CloudFront origin request policy configuration is required for this path.&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forward-ja4"&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="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A condition that matches all requests"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Count"&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;"CustomRequestHandling"&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;"InsertHeaders"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ja4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:ja4:}"&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;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;The origin receives the value in the &lt;code&gt;x-amzn-waf-ja4&lt;/code&gt; header. Because AWS WAF inserts this header, the application can treat it as a server-side signal that the client cannot directly spoof—provided that there is no route to the origin that bypasses WAF.&lt;/p&gt;

&lt;p&gt;If clients can reach the origin directly, the header becomes little more than a self-declared value. The origin must therefore reject traffic that does not come through CloudFront.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Session Binding
&lt;/h2&gt;

&lt;p&gt;The basic implementation is simple: record the JA4 fingerprint in the session after a successful login, then compare it with the fingerprint observed on subsequent requests.&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="c1"&gt;// After a successful login&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onLoginSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boundFingerprints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&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-amzn-waf-ja4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerprintAlerts&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Authentication middleware&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifySession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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-amzn-waf-ja4&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;bound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boundFingerprints&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;bound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Known fingerprint&lt;/span&gt;

    &lt;span class="c1"&gt;// Unknown fingerprint: treat it as a risk signal rather than blocking immediately&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerprintAlerts&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isHighRiskOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerprintAlerts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/reauth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Add it to bound after successful reauthentication&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;next&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;At this point, you may be thinking: “Why use &lt;code&gt;bound.includes(current)&lt;/code&gt;? Why not bind the session to one exact value with &lt;code&gt;bound === current&lt;/code&gt;?”&lt;/p&gt;

&lt;p&gt;Good catch. That question leads directly to the main topic of this article: strict equality can incorrectly reject legitimate users.&lt;/p&gt;

&lt;h2&gt;
  
  
  JA4 Can Change Even for the Same Client
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/FoxIO-LLC/ja4" rel="noopener noreferrer"&gt;The FoxIO README&lt;/a&gt; includes several JA4 fingerprints produced by the same Chrome client under different conditions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;section a  section b    section c
t13d1516h2_8daaf6152771_02713d6af862   (TCP)
q13d0312h3_55b375c5d22e_06cda9e17597   (QUIC = HTTP/3)
t13d1517h2_8daaf6152771_b0da82dd1658   (pre-shared key = session resumption)
t13d1517h2_8daaf6152771_b1ff8ab2d16f   (no key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are at least three sources of variation.&lt;/p&gt;

&lt;h3&gt;
  
  
  TLS session resumption with a PSK
&lt;/h3&gt;

&lt;p&gt;When a client reconnects using session resumption, the ClientHello includes the &lt;code&gt;pre_shared_key&lt;/code&gt; extension. This changes the extension count in the &lt;code&gt;a&lt;/code&gt; section and also changes the &lt;code&gt;c&lt;/code&gt; section. In the example above, the relevant value changes from &lt;code&gt;1516&lt;/code&gt; to &lt;code&gt;1517&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP/2 (&lt;code&gt;h2&lt;/code&gt;) versus HTTP/3 (&lt;code&gt;h3&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;HTTP/3 runs over QUIC, so the protocol marker changes from &lt;code&gt;t&lt;/code&gt; to &lt;code&gt;q&lt;/code&gt;, and the &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, and &lt;code&gt;c&lt;/code&gt; sections can all change. Modern browsers can move between HTTP/2 and HTTP/3, so it is normal for a single user to produce more than one family of JA4 fingerprints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser updates
&lt;/h3&gt;

&lt;p&gt;A browser update can change the TLS stack and therefore change the JA4 fingerprint. This may not happen with every release, but over a sufficiently long period it is inevitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Behavior
&lt;/h2&gt;

&lt;p&gt;To verify this behavior, I configured an AWS WAF &lt;code&gt;Count&lt;/code&gt; rule to insert &lt;code&gt;${awswaf:ja4:}&lt;/code&gt;, then used a CloudFront Function to return the observed value as JSON. I collected and compared JA4 fingerprints from the same machine under different conditions.&lt;/p&gt;

&lt;p&gt;First, I changed the HTTP version while using the same curl binary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;JA4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl --http1.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t13d3012h1_1d37bd780c83_882d495ac381&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl --http2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t13d3012h2_1d37bd780c83_882d495ac381&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl --http3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;q13d0312h3_55b375c5d22e_c0efc70deb60&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The HTTP/1.1 and HTTP/2 values are identical except for the ALPN field, &lt;code&gt;h1&lt;/code&gt; versus &lt;code&gt;h2&lt;/code&gt;. When the connection switches to HTTP/3 over QUIC, however, the leading transport marker changes from &lt;code&gt;t&lt;/code&gt; to &lt;code&gt;q&lt;/code&gt;, and all three JA4 sections change.&lt;/p&gt;

&lt;p&gt;In other words, even the same curl binary can produce a different fingerprint when the transport path changes.&lt;/p&gt;

&lt;p&gt;Next, I forced Chrome to use TCP with HTTP/2 and compared a full TLS handshake with resumed sessions. The procedure was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fully quit and restart Chrome.&lt;/li&gt;
&lt;li&gt;Access the site once to produce a full handshake.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;chrome://net-internals/#sockets&lt;/code&gt; and select &lt;strong&gt;Flush socket pools&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Reload the page to trigger session resumption.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;JA4&lt;/th&gt;
&lt;th&gt;Numeric portion of the &lt;code&gt;a&lt;/code&gt; section&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full handshake, first request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t13d1516h2_8daaf6152771_806a8c22fdea&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1516&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session resumption, first attempt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t13d1517h2_8daaf6152771_a87ad97598a9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1517&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session resumption, second attempt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t13d1517h2_8daaf6152771_a87ad97598a9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1517&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session resumption, third attempt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t13d1517h2_8daaf6152771_a87ad97598a9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1517&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even though this was the same browser and the same application session, session resumption changed the numeric value in the &lt;code&gt;a&lt;/code&gt; section from &lt;code&gt;1516&lt;/code&gt; to &lt;code&gt;1517&lt;/code&gt;, and the &lt;code&gt;c&lt;/code&gt; section changed as well. This is because the resumed ClientHello contains one additional &lt;code&gt;pre_shared_key&lt;/code&gt; extension. The final two digits in that part of the &lt;code&gt;a&lt;/code&gt; section represent the extension count.&lt;/p&gt;

&lt;p&gt;Interestingly, the transition from &lt;code&gt;t13d1516..._8daaf6152771&lt;/code&gt; to &lt;code&gt;t13d1517..._8daaf6152771&lt;/code&gt; matches the Chrome example in the FoxIO README through both the &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; sections.&lt;/p&gt;

&lt;p&gt;These results reveal three facts that directly affect session-binding design.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The &lt;code&gt;b&lt;/code&gt; section is relatively stable within the same transport family
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;b&lt;/code&gt; section is the hash of the cipher-suite list. In my tests, it remained unchanged across HTTP/1.1 and HTTP/2 and across full and resumed TCP handshakes: &lt;code&gt;8daaf6152771&lt;/code&gt; for Chrome and &lt;code&gt;1d37bd780c83&lt;/code&gt; for curl.&lt;/p&gt;

&lt;p&gt;Switching to QUIC can change it because QUIC uses TLS 1.3 and presents a different cipher-suite profile. Even so, the &lt;code&gt;b&lt;/code&gt; section was the most stable part of JA4 within a given transport family.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The variation converges on a small number of values
&lt;/h3&gt;

&lt;p&gt;For the TCP case, the fingerprint changed once between the full handshake and the first PSK-based resumption, then remained stable on the second and third resumed connections.&lt;/p&gt;

&lt;p&gt;This suggests that remembering two values—a full-handshake fingerprint and a resumed-session fingerprint—may be sufficient to recognize that client’s normal TCP behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. QUIC reduces distinguishing power
&lt;/h3&gt;

&lt;p&gt;QUIC is limited to TLS 1.3 and therefore has fewer cipher-suite combinations. In my measurements, Chrome over HTTP/3 produced a fingerprint beginning with &lt;code&gt;q13d0311h3_55b375c5d22e_...&lt;/code&gt;, whose &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; sections were very similar to those produced by curl over HTTP/3.&lt;/p&gt;

&lt;p&gt;With HTTP/3, the &lt;code&gt;c&lt;/code&gt; section or an additional signal such as JA4H may be necessary to distinguish clients reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the Findings into a Practical Design
&lt;/h2&gt;

&lt;p&gt;Based on these observations, a practical session-binding design looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not require one exact JA4 value.&lt;/strong&gt; Otherwise, legitimate users may be rejected whenever TLS session resumption occurs or the browser switches between HTTP/2 and HTTP/3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain a set of previously observed fingerprints and combine it with a risk score.&lt;/strong&gt; An unknown value should increase risk rather than cause an immediate block. Require reauthentication only before high-risk operations such as money transfers, password changes, email-address changes, or account deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When using a relaxed comparison, compare JA4 section by section.&lt;/strong&gt; JA4 is designed so that similar client fingerprints remain structurally comparable. For example, you can give greater weight to a matching &lt;code&gt;b&lt;/code&gt; section while allowing expected variation in &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simply being able to notice that a stolen cookie is being used from a different TLS stack is already valuable. Adding too many conditions, on the other hand, can quickly drown the system in false positives. The balance matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-Up Authentication with ATP Signals
&lt;/h2&gt;

&lt;p&gt;Another authentication-related use case for Dynamic Label Interpolation is AWS WAF Account Takeover Prevention, or ATP.&lt;/p&gt;

&lt;p&gt;ATP inspects login requests and emits labels for signals such as compromised credential use and credential-stuffing activity.&lt;/p&gt;

&lt;p&gt;Previously, forwarding these signals required a separate rule for each label. With Dynamic Label Interpolation, you can forward the entire namespace instead.&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forward-atp-signals"&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="nl"&gt;"LabelMatchStatement"&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;"Scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NAMESPACE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"awswaf:managed:aws:atp:signal:"&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;"Action"&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;"Count"&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;"CustomRequestHandling"&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;"InsertHeaders"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"atp-signals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:managed:aws:atp:signal:}"&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;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;The application receives the values in &lt;code&gt;x-amzn-waf-atp-signals&lt;/code&gt;. If the header contains &lt;code&gt;credential_compromised&lt;/code&gt;, for example, the application could allow the login to complete but immediately require the user to reset the password.&lt;/p&gt;

&lt;p&gt;This is not simply “blocking with WAF.” It turns WAF detections into inputs for adaptive or step-up authentication, allowing the application to strengthen the authentication flow according to the observed risk.&lt;/p&gt;

&lt;p&gt;One caveat: ATP is a paid managed rule group with subscription and request-based charges. You should also confirm the current label names and behavior in the official documentation before deploying this in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS WAF Dynamic Label Interpolation can forward JA4 to the application, allowing a session to be associated with a TLS fingerprint. An attacker may steal a cookie, but reproducing the victim’s TLS fingerprint requires substantially more effort.&lt;/li&gt;
&lt;li&gt;JA4 can change even for the same client because of TLS session resumption, HTTP/2-to-HTTP/3 switching, and browser updates. Binding a session to one exact JA4 value can therefore reject legitimate users.&lt;/li&gt;
&lt;li&gt;A set of known fingerprints, a risk score, and reauthentication before high-risk operations appear to be a practical middle ground.&lt;/li&gt;
&lt;li&gt;ATP detections can be used not only to block requests, but also to drive stronger authentication and account-recovery controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next article, I will cover the reverse direction: a feedback loop in which the application reports a malicious client back to the edge so that subsequent requests can be blocked there.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>waf</category>
    </item>
    <item>
      <title>Building a False-Positive-Resilient Challenge Architecture with AWS WAF Dynamic Label Interpolation</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Wed, 15 Jul 2026 22:19:27 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-false-positive-resilient-challenge-architecture-with-aws-waf-dynamic-label-interpolation-e96</link>
      <guid>https://dev.to/aws-builders/building-a-false-positive-resilient-challenge-architecture-with-aws-waf-dynamic-label-interpolation-e96</guid>
      <description>&lt;p&gt;In May 2026, AWS WAF introduced a feature called &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-dynamic-label-interpolation.html" rel="noopener noreferrer"&gt;&lt;strong&gt;dynamic label interpolation&lt;/strong&gt;&lt;/a&gt;. Did you catch the announcement?&lt;/p&gt;

&lt;p&gt;It may look like a small addition at first, but I think it offers an elegant way to address one of the hardest parts of operating bot defenses: dealing with false positives.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show how to use dynamic label interpolation to build a challenge architecture that gives legitimate users caught by a false positive a self-service way to report the issue and regain access.&lt;/p&gt;

&lt;p&gt;Anyone who operates bot protection with a WAF has probably wrestled with false positives. Legitimate users can occasionally be misclassified as bots, which often leads teams to avoid enabling blocking altogether. &lt;/p&gt;

&lt;p&gt;The architecture described in this article takes a different approach: instead of treating a bot-detection signal as a final verdict, it redirects suspicious clients to a challenge and allows legitimate users to recover on their own. In practice, this can virtually eliminate false-positive blocking caused by bot detection, even when the underlying detection is not perfect. This design extends the architecture from &lt;a href="https://dev.to/aws-builders/implementing-a-ja4h-equivalent-fingerprint-with-amazon-cloudfront-functions-to-mitigate-bot-traffic-4jbm"&gt;my previous article&lt;/a&gt;, in which clients flagged by JA4H-based detection are redirected to a challenge page rather than blocked immediately. However, this article is written to stand on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Dynamic Label Interpolation?
&lt;/h2&gt;

&lt;p&gt;AWS WAF rules can attach labels to requests. Managed rule groups such as Bot Control also emit a large number of labels, including values such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;awswaf:managed:aws:bot-control:bot:category:scraping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously, forwarding these label values to the origin or including them in a response required a separate rule for each label and a statically defined header value.&lt;/p&gt;

&lt;p&gt;Dynamic label interpolation changes that. With the &lt;code&gt;${namespace:}&lt;/code&gt; syntax, AWS WAF can resolve labels attached to a request at evaluation time and embed the resulting values in custom headers or response bodies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request label:        awswaf:managed:aws:bot-control:bot:category:scraping
Configured value:     ${awswaf:managed:aws:bot-control:bot:category:}
Resolved value:       scraping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resolution rules are straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If exactly one label matches the namespace, AWS WAF returns the final value.&lt;/li&gt;
&lt;li&gt;If multiple labels match, AWS WAF removes the namespace prefix and returns a comma-separated list, such as &lt;code&gt;scraping,advertising&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If no labels match, the placeholder resolves to an empty string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also use built-in values called &lt;strong&gt;synthetic labels&lt;/strong&gt;. Unlike ordinary labels, which are attached to a request as rules evaluate it, synthetic labels resolve information already associated with the request itself, such as the client IP address, TLS handshake fingerprint, or the request ID assigned by AWS WAF.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Synthetic label&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${awswaf:request_id:}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AWS WAF request ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${awswaf:ip:}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client IP address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${awswaf:ja3:}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JA3 TLS fingerprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${awswaf:ja4:}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JA4 TLS fingerprint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Constraints to Understand Before You Design
&lt;/h2&gt;

&lt;p&gt;Before looking at the use cases, let’s review the constraints that affect the architecture. Missing these details can lead to avoidable rework later.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Custom request headers can be inserted only by Allow, Count, CAPTCHA, and Challenge actions
&lt;/h3&gt;

&lt;p&gt;A Block action can return only a custom response: a status code, response headers, and a response body.&lt;/p&gt;

&lt;p&gt;This is logical when you think about it. “Block the request while also forwarding a header to the origin” is inherently contradictory, because a blocked request never reaches the origin.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inserted request headers receive the &lt;code&gt;x-amzn-waf-&lt;/code&gt; prefix
&lt;/h3&gt;

&lt;p&gt;Many AWS WAF users will already be familiar with this behavior. If you configure a rule to insert a header named &lt;code&gt;bot-category&lt;/code&gt;, the origin receives it as:&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-bot-category
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prefix is not added to response headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. With CAPTCHA and Challenge actions, requests without a valid token are not forwarded to the origin
&lt;/h3&gt;

&lt;p&gt;AWS WAF returns the challenge response immediately. Custom request headers are inserted only for requests that pass token inspection and are forwarded.&lt;/p&gt;

&lt;p&gt;Conversely, the origin can treat the presence of such a header as evidence that the request has already passed the AWS WAF challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Each string value can contain at most 10 placeholders
&lt;/h3&gt;

&lt;p&gt;The eleventh and subsequent placeholders are not resolved. They are emitted literally as &lt;code&gt;${...}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This limit applies per string value rather than to the entire request, so you can work around it by splitting the values across multiple headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Interpolating custom labels requires fully qualified names
&lt;/h3&gt;

&lt;p&gt;In a rule definition, you can attach a custom label using a short name such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app:tier:enterprise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, an interpolation reference must include the full Web ACL context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${awswaf:ACCOUNT_ID:webacl:WEBACL_NAME:app:tier:}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Label match statements can use the short local namespace, so the two mechanisms are asymmetric in this respect.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. A namespace with no matching labels resolves to an empty string
&lt;/h3&gt;

&lt;p&gt;Remember to handle the case where the header exists but its value is empty. At the origin, that will usually represent an unclassified request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Case 1: Embed &lt;code&gt;request_id&lt;/code&gt; in the Block Page to Create a False-Positive Feedback Loop
&lt;/h2&gt;

&lt;p&gt;False positives are unavoidable in bot mitigation operations.&lt;/p&gt;

&lt;p&gt;Security products and corporate proxies may rewrite headers and change a client fingerprint. A legitimate user may also happen to use the same client implementation as one listed as malicious. In either case, a real user can be caught by a blocking rule.&lt;/p&gt;

&lt;p&gt;When that happens, the report from the user is often no more specific than, “I cannot access the site.” Identifying which request matched which rule can then take a significant amount of time.&lt;/p&gt;

&lt;p&gt;A simple improvement is to embed the AWS WAF request ID directly in the block page.&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;"CustomResponseBodies"&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;"BlockPage"&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;"Content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Access has been blocked.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Request ID: ${awswaf:request_id:}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;IP: ${awswaf:ip:}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;If you believe this is an error, contact support and include the Request ID shown above."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ContentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEXT_PLAIN"&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;The user only needs to copy the Request ID into the support ticket. The support team can then search the AWS WAF logs for that ID and immediately identify the request, the rule that matched, and every label attached to it.&lt;/p&gt;

&lt;p&gt;What used to be “half a day investigating a possible false positive” becomes a single log query. Once the request is confirmed as legitimate, you can exempt the relevant fingerprint or signature and close the case.&lt;/p&gt;

&lt;p&gt;This may sound like a modest improvement, but it represents an important shift in design philosophy: &lt;strong&gt;make false-positive recovery part of the operational workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Eliminating false positives entirely is unrealistic. It is often more productive to invest in minimizing the cost of recovery when they inevitably occur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Case 2: Carry the Fingerprint into the Challenge Page
&lt;/h2&gt;

&lt;p&gt;An outright block can still damage user trust. In my previous article, I introduced an architecture in which suspicious clients are redirected to a challenge page rather than blocked immediately. Once they pass the challenge, a clearance cookie allows subsequent requests through.&lt;/p&gt;

&lt;p&gt;Dynamic label interpolation strengthens that architecture in two places.&lt;/p&gt;

&lt;h3&gt;
  
  
  During redirection: interpolate values into the &lt;code&gt;Location&lt;/code&gt; header
&lt;/h3&gt;

&lt;p&gt;A custom response from a Block action can specify a status code and response headers. Interpolation also works in the &lt;code&gt;Location&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;When redirecting a suspicious request to a challenge page with a &lt;code&gt;302&lt;/code&gt;, you can include the fingerprint and request ID observed at that moment in the query string.&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"suspicious-to-challenge"&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="nl"&gt;"LabelMatchStatement"&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;"Scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NAMESPACE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"awswaf:managed:aws:bot-control:bot:category:"&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;"Action"&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;"Block"&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;"CustomResponse"&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;"ResponseCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ResponseHeaders"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/challenge?fp=${awswaf:ja4:}&amp;amp;rid=${awswaf:request_id:}"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no-store"&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;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;The challenge page now knows, from the start, which fingerprint and which request caused the redirect.&lt;/p&gt;

&lt;p&gt;You can aggregate challenge impressions and completion rates by fingerprint. This enables data-driven hygiene checks on your fingerprint list—for example, “This fingerprint has a 99% challenge pass rate, so we should remove it from the suspicious list.”&lt;/p&gt;

&lt;p&gt;There is one important security caveat: the &lt;code&gt;fp&lt;/code&gt; query parameter can be modified by the client. &lt;strong&gt;Do not use it for authorization decisions&lt;/strong&gt;. Limit its use to logging and correlation analysis.&lt;/p&gt;

&lt;p&gt;The authoritative fingerprint used for authorization must be observed again during challenge verification, as described next.&lt;/p&gt;

&lt;h3&gt;
  
  
  After success: issue a clearance cookie bound to the fingerprint
&lt;/h3&gt;

&lt;p&gt;Validate the challenge—using Turnstile, reCAPTCHA, or another mechanism—at the origin. When validation succeeds, issue a clearance cookie protected by an HMAC signature.&lt;/p&gt;

&lt;p&gt;Include the JA4 value observed during verification in the signed cookie. The origin can obtain a trusted value from the &lt;code&gt;CloudFront-Viewer-JA4-Fingerprint&lt;/code&gt; header forwarded through an origin request policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clearance = base64(ja4 + "|" + expiry) + "." + HMAC-SHA256(ja4 + "|" + expiry, secret)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For subsequent requests, validate the cookie in a CloudFront Function on the &lt;code&gt;viewer-request&lt;/code&gt; event. In addition to checking the signature and expiration time, compare the JA4 stored in the cookie with the JA4 of the current request.&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="c1"&gt;// Example clearance validation in CloudFront Functions (cloudfront-js-2.0)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&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;hasValidClearance&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;c&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;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clearance&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;ja4&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="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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&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="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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&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;split&lt;/span&gt;&lt;span class="p"&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="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;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;2&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bytesFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "ja4|expiry"&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;sig&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;|&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="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&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;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&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;// Expired&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fields&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="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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                            &lt;span class="c1"&gt;// JA4 matches&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents an attacker from stealing only the clearance cookie and reusing it from a different client with a different TLS stack.&lt;/p&gt;

&lt;p&gt;Stealing a cookie is one thing. Accurately reproducing the associated TLS fingerprint at the same time is a substantially harder problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evaluation-Order Trap
&lt;/h2&gt;

&lt;p&gt;As I mentioned in the previous article, AWS WAF evaluates a CloudFront request before CloudFront Functions runs.&lt;/p&gt;

&lt;p&gt;The responsibilities in this architecture are therefore divided as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS WAF:&lt;/strong&gt; Redirects suspicious requests to the challenge flow with &lt;code&gt;Block + 302&lt;/code&gt;, and interpolates ordinary and synthetic labels. AWS WAF cannot calculate an HMAC, so it can check only whether the cookie is present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront Functions:&lt;/strong&gt; Cryptographically validates the clearance cookie by checking its HMAC and matching its JA4 value. This is where requests carrying a cookie that AWS WAF allowed through are authenticated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin:&lt;/strong&gt; Verifies the challenge and issues the clearance cookie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AWS WAF rule should be structured as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Suspicious label is present AND clearance cookie is absent -&amp;gt; 302 redirect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requests that have a clearance cookie are allowed through AWS WAF and validated by CloudFront Functions.&lt;/p&gt;

&lt;p&gt;An attacker who knows the cookie name can get past the AWS WAF condition by sending a fake cookie, but the function immediately rejects an invalid value. The result is a two-stage defense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Actual Behavior
&lt;/h2&gt;

&lt;p&gt;A few details could not be established conclusively from the documentation alone, so I created a test Web ACL and verified them directly.&lt;/p&gt;

&lt;p&gt;The test configuration included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Count rule that inserts headers whose values contain synthetic labels&lt;/li&gt;
&lt;li&gt;Two rules that attach different custom labels under the same namespace&lt;/li&gt;
&lt;li&gt;A Block rule that returns a &lt;code&gt;302&lt;/code&gt; with interpolation in the &lt;code&gt;Location&lt;/code&gt; header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For observation, I used a probe CloudFront Function on the &lt;code&gt;viewer-request&lt;/code&gt; event that returns all request headers as JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;rules.json&lt;/code&gt;
&lt;/h3&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"label-one"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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="nl"&gt;"ByteMatchStatement"&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;"SearchString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FieldToMatch"&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;"SingleHeader"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x-waf-test"&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;"PositionalConstraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXACTLY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TextTransformations"&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;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NONE"&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;span class="nl"&gt;"RuleLabels"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app:test:one"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Count"&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;"VisibilityConfig"&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;"SampledRequestsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CloudWatchMetricsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MetricName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"labelone"&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;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"label-two"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&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="nl"&gt;"ByteMatchStatement"&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;"SearchString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FieldToMatch"&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;"SingleHeader"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x-waf-test"&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;"PositionalConstraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXACTLY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TextTransformations"&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;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NONE"&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;span class="nl"&gt;"RuleLabels"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app:test:two"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Count"&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;"VisibilityConfig"&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;"SampledRequestsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CloudWatchMetricsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MetricName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"labeltwo"&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;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forward-signals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&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="nl"&gt;"SizeConstraintStatement"&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;"FieldToMatch"&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;"SingleHeader"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x-waf-test"&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;"ComparisonOperator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TextTransformations"&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;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NONE"&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;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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Count"&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;"CustomRequestHandling"&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;"InsertHeaders"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-ja4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:ja4:}"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-rid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:request_id:}"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-ip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:ip:}"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-multi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:ACCOUNT_ID:webacl:WEBACL_NAME:app:test:}"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-nomatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${awswaf:ACCOUNT_ID:webacl:WEBACL_NAME:app:nothing:}"&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;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;"VisibilityConfig"&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;"SampledRequestsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CloudWatchMetricsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MetricName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forwardsignals"&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;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"challenge-redirect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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="nl"&gt;"ByteMatchStatement"&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;"SearchString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FieldToMatch"&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;"SingleHeader"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x-waf-test"&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;"PositionalConstraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXACTLY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TextTransformations"&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;"Priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NONE"&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;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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Block"&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;"CustomResponse"&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;"ResponseCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ResponseHeaders"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/challenge?fp=${awswaf:ja4:}&amp;amp;rid=${awswaf:request_id:}"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no-store"&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;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;"VisibilityConfig"&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;"SampledRequestsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CloudWatchMetricsEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MetricName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"challengeredirect"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Because this is a Count rule, the headers should be inserted and the&lt;/span&gt;
&lt;span class="c"&gt;# synthetic-label values should be available.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://dxxxx.cloudfront.net/probe"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-waf-test: 1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="s1"&gt;'.headers[] | select(.name | startswith("x-amzn-waf"))'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"x-amzn-waf-test-ja4"&lt;/span&gt;,     &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;"t13d4907h2_0d8feac7bc37_7395dae3b2f3"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"x-amzn-waf-test-rid"&lt;/span&gt;,     &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;"F1CXnajGxmUxPQ5e_..."&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"x-amzn-waf-test-ip"&lt;/span&gt;,      &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;"2400:xxxx:..."&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"x-amzn-waf-test-multi"&lt;/span&gt;,   &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"x-amzn-waf-test-nomatch"&lt;/span&gt;, &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Verify comma-separated resolution. Two Count rules attach app:test:one&lt;/span&gt;
&lt;span class="c"&gt;# and app:test:two when x-waf-test is "multi". This shows how&lt;/span&gt;
&lt;span class="c"&gt;# ${...:app:test:} resolves both labels.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; ... &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-waf-test: multi"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'...test-multi...'&lt;/span&gt;
one,two

&lt;span class="c"&gt;# Verify interpolation in the Location header of a Block custom response.&lt;/span&gt;
&lt;span class="c"&gt;# Because this is a block rule, the response should redirect to the challenge.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-sv&lt;/span&gt; ... &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-waf-test: block"&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^&amp;lt; location'&lt;/span&gt;
&amp;lt; location: /challenge?fp&lt;span class="o"&gt;=&lt;/span&gt;t13d4907h2_0d8feac7bc37_7395dae3b2f3&amp;amp;rid&lt;span class="o"&gt;=&lt;/span&gt;SHCiGoKo...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests confirmed the following behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synthetic labels resolve correctly
&lt;/h3&gt;

&lt;p&gt;All three tested synthetic labels—&lt;code&gt;ja4&lt;/code&gt;, &lt;code&gt;request_id&lt;/code&gt;, and &lt;code&gt;ip&lt;/code&gt;—resolved successfully.&lt;/p&gt;

&lt;p&gt;Interpolation in the &lt;code&gt;Location&lt;/code&gt; header also produced a working redirect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple labels resolve to a comma-separated list
&lt;/h3&gt;

&lt;p&gt;The two labels resolved as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one,two
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real Bot Control deployment, for example, if a single request receives both &lt;code&gt;signal:non_browser_user_agent&lt;/code&gt; and &lt;code&gt;signal:automated_browser&lt;/code&gt;, then &lt;code&gt;${...signal:}&lt;/code&gt; resolves to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;non_browser_user_agent,automated_browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A namespace with no matches still inserts the header with an empty value
&lt;/h3&gt;

&lt;p&gt;AWS WAF does not omit the header entirely. The receiving component does not need to test whether the header exists; it only needs to check whether the value is empty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Headers inserted by AWS WAF are visible to CloudFront Functions on &lt;code&gt;viewer-request&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The probe itself ran as a CloudFront Function, so the fact that these headers appeared in its output directly demonstrates that the function could access them.&lt;/p&gt;

&lt;p&gt;This last finding significantly expands the available design space.&lt;/p&gt;

&lt;p&gt;Because the order is AWS WAF evaluation followed by CloudFront Functions, AWS WAF can pass JA4 to the function without an origin request policy. Bot Control category labels can also drive branching logic in the function—for example, selecting different levels of challenge difficulty.&lt;/p&gt;

&lt;p&gt;The JA4 value used for clearance-cookie validation can be delivered through the same path.&lt;/p&gt;

&lt;p&gt;As a bonus, I compared JA4 values over HTTP/1.1 and HTTP/2. The results were &lt;code&gt;t13d4907h1_...&lt;/code&gt; and &lt;code&gt;t13d4907h2_...&lt;/code&gt;: only the single ALPN(Application-Layer Protocol Negotiation) character differed, while the rest of the fingerprint remained identical.&lt;/p&gt;

&lt;p&gt;This is a neat real-world demonstration of how ALPN is encoded in the &lt;code&gt;a&lt;/code&gt; section of the JA4 format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic label interpolation lets AWS WAF resolve classification signals from ordinary labels, along with synthetic values such as &lt;code&gt;request_id&lt;/code&gt;, IP address, JA3, and JA4, into headers and response bodies at evaluation time.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;${awswaf:request_id:}&lt;/code&gt; to a block page turns false-positive investigation into a single log search. Because false positives cannot be eliminated completely, design for lower recovery cost.&lt;/li&gt;
&lt;li&gt;Interpolation in the &lt;code&gt;Location&lt;/code&gt; header can carry a fingerprint into the challenge page. Because query-string values can be tampered with, use them only for logging and reacquire the authoritative value during verification.&lt;/li&gt;
&lt;li&gt;Binding and signing a clearance cookie to JA4 creates a challenge architecture that cannot be bypassed merely by stealing the cookie.&lt;/li&gt;
&lt;li&gt;Headers inserted by AWS WAF are visible to CloudFront Functions on &lt;code&gt;viewer-request&lt;/code&gt;. This lets you pass JA4 and AWS WAF classification signals to an edge function without an origin request policy.&lt;/li&gt;
&lt;li&gt;Account for the constraints up front: Block actions cannot insert request headers, inserted headers receive the &lt;code&gt;x-amzn-waf-&lt;/code&gt; prefix, each string allows at most 10 placeholders, custom labels require fully qualified interpolation names, and unmatched namespaces resolve to an empty string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a Request ID to an existing Web ACL’s block page takes about five minutes. That is a great place to start.&lt;/p&gt;

&lt;p&gt;Used effectively, dynamic label interpolation could move us toward a future where false positives are no longer something we have to fear when operating bot detection with AWS WAF. Give it a try.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>waf</category>
    </item>
    <item>
      <title>Implementing a JA4H-Equivalent Fingerprint with Amazon CloudFront Functions to Mitigate Bot Traffic</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Tue, 14 Jul 2026 23:11:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/implementing-a-ja4h-equivalent-fingerprint-with-amazon-cloudfront-functions-to-mitigate-bot-traffic-4jbm</link>
      <guid>https://dev.to/aws-builders/implementing-a-ja4h-equivalent-fingerprint-with-amazon-cloudfront-functions-to-mitigate-bot-traffic-4jbm</guid>
      <description>&lt;p&gt;JA3 and JA4 fingerprints have been showing up more and more often in discussions about bot mitigation and client identification. Amazon CloudFront exposes TLS-based JA4 as a CloudFront-generated header, but it does not provide the HTTP-request-based JA4H fingerprint.&lt;/p&gt;

&lt;p&gt;That made me wonder: “Why not implement it ourselves with CloudFront Functions?” So I did. There are a few gotchas and design considerations you need to be aware of, and I’ll cover those as well.&lt;/p&gt;

&lt;p&gt;In this article, I’ll first demonstrate the limitation through hands-on testing, then work around it with the &lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt; header and show how to calculate a JA4H-equivalent fingerprint entirely within CloudFront Functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is JA4?
&lt;/h2&gt;

&lt;p&gt;JA4+ is a suite of network fingerprints published by FoxIO and positioned as a successor to JA3. The best-known member is JA4, which is calculated from the TLS ClientHello. It generates a short string that is nearly unique to a particular client implementation, based on factors such as the TLS version, whether SNI is present, cipher suites, and extensions.&lt;/p&gt;

&lt;p&gt;The key idea behind JA4 is that the same implementation tends to produce the same value. Chrome, &lt;code&gt;curl&lt;/code&gt;, and Python’s &lt;code&gt;requests&lt;/code&gt;, for example, each have distinct characteristics in their TLS stacks and therefore produce different JA4 fingerprints.&lt;/p&gt;

&lt;p&gt;A User-Agent string can be spoofed freely, but accurately reproducing every detail of a TLS handshake is considerably harder. This makes it possible to detect inconsistencies such as a client claiming to be Chrome in its User-Agent while presenting a JA4 fingerprint associated with Python.&lt;/p&gt;

&lt;p&gt;In addition to TLS-based JA4, the JA4+ suite includes several other variants, such as JA4H, which is calculated from HTTP request headers, and JA4S, which fingerprints the server side of a TLS connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amazon CloudFront, JA4, and CloudFront KVS
&lt;/h2&gt;

&lt;p&gt;CloudFront announced support for JA3 in 2022 and JA4 in October 2024. By adding &lt;code&gt;CloudFront-Viewer-JA4-Fingerprint&lt;/code&gt; to an origin request policy, you can receive the precomputed JA4 value as a request header.&lt;/p&gt;

&lt;p&gt;This header is also available to a CloudFront Function running on the &lt;code&gt;viewer-request&lt;/code&gt; event. Combined with CloudFront KeyValueStore (KVS), it lets you operate a blocklist of known malicious fingerprints entirely at the edge.&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;kvs&lt;/span&gt; &lt;span class="o"&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;kvs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ja4&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="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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kvs&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="nx"&gt;ja4&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="c1"&gt;// Example: "block"&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;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&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;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&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;Forbidden&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// get() throws when the key does not exist.&lt;/span&gt;
            &lt;span class="c1"&gt;// An unregistered fingerprint is not known to be malicious, so do nothing.&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;p&gt;KVS can be updated immediately through the console or API. For example, you can analyze suspicious requests in existing logs, register their fingerprint values in KVS, and begin blocking them without redeploying the function.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is JA4H?
&lt;/h2&gt;

&lt;p&gt;JA4H is a fingerprint calculated from the HTTP request itself. It consists of four parts joined by underscores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JA4H = &amp;lt;a&amp;gt;_&amp;lt;b&amp;gt;_&amp;lt;c&amp;gt;_&amp;lt;d&amp;gt;
Example: ge11cn05enus_974ebe531c03_000000000000_000000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;JA4H_a: The first two characters of the HTTP method + two digits representing the HTTP version + whether a Cookie is present (&lt;code&gt;c&lt;/code&gt;/&lt;code&gt;n&lt;/code&gt;) + whether a Referer is present (&lt;code&gt;r&lt;/code&gt;/&lt;code&gt;n&lt;/code&gt;) + a two-digit header count excluding Cookie and Referer + the first four characters derived from Accept-Language&lt;/li&gt;
&lt;li&gt;JA4H_b: The first 12 hexadecimal characters of a SHA-256 hash over the comma-separated header names in their original transmission order, excluding Cookie and Referer&lt;/li&gt;
&lt;li&gt;JA4H_c: A hash of the sorted, comma-separated Cookie names&lt;/li&gt;
&lt;li&gt;JA4H_d: A hash of the sorted, comma-separated Cookie &lt;code&gt;name=value&lt;/code&gt; pairs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important part is &lt;strong&gt;JA4H_b&lt;/strong&gt;. Browsers and HTTP clients have implementation-specific habits in the order in which they send headers, so that order becomes useful identifying information.&lt;/p&gt;

&lt;p&gt;Conversely, calculating JA4H accurately requires both the original header order and header information that has not been rewritten along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing JA4H in CloudFront Functions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Recovering header order and original casing with &lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;It turns out that the CloudFront Functions event object alone is not enough to calculate JA4H.&lt;/p&gt;

&lt;p&gt;As the official documentation states, “Header names are converted to ASCII-lowercase in the event object.” In addition, &lt;code&gt;headers&lt;/code&gt; is an object keyed by header name, so it does not expose the order in which headers were sent. I confirmed this experimentally: changing the transmission order did not change the order visible in the event object.&lt;/p&gt;

&lt;p&gt;This is where the CloudFront request header &lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt; comes in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Contains the viewer's header names in the order requested, separated by a colon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt; contains the header names sent by the viewer, in the order CloudFront received them, separated by colons. The value is not affected by the event object’s lowercase normalization. In my tests, it contained the information required for JA4H_b:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s --http1.1 ... -H "X-Test-ZZZ: 1" -H "x-test-aaa: 2"
→ Host:User-Agent:Accept:X-Test-ZZZ:x-test-aaa

$ curl -s --http1.1 ... -H "x-test-aaa: 2" -H "X-Test-ZZZ: 1"
→ Host:User-Agent:Accept:x-test-aaa:X-Test-ZZZ

$ curl -s --http1.1 ... -H "x-dup: 1" -H "X-Test-ZZZ: 1" -H "x-dup: 2"
→ Host:User-Agent:Accept:x-dup:X-Test-ZZZ:x-dup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the request is sent over HTTP/2, the value becomes fully lowercase, such as &lt;code&gt;host:user-agent:accept:...&lt;/code&gt;. That is expected because HTTP/2 requires header field names to be lowercase.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt; is not included in the managed &lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt; origin request policy, so you need to create a custom policy and add it explicitly. You should also add &lt;code&gt;CloudFront-Viewer-Http-Version&lt;/code&gt;, because the HTTP version required for JA4H_a is not included in the event object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudfront create-origin-request-policy &lt;span class="nt"&gt;--origin-request-policy-config&lt;/span&gt; &lt;span class="s1"&gt;'{
  "Name": "ja4h-policy",
  "HeadersConfig": {
    "HeaderBehavior": "allViewerAndWhitelistCloudFront",
    "Headers": {"Quantity": 2, "Items": [
      "CloudFront-Viewer-Header-Order",
      "CloudFront-Viewer-Http-Version"]}
  },
  "CookiesConfig": {"CookieBehavior": "all"},
  "QueryStringsConfig": {"QueryStringBehavior": "all"}
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two caveats.&lt;/p&gt;

&lt;p&gt;First, the header value is truncated at 7,680 characters, so the resulting JA4H value may be inaccurate for requests containing an extreme number of headers.&lt;/p&gt;

&lt;p&gt;Second, the preservation of header-name casing is behavior I observed experimentally; it is not explicitly guaranteed in the documentation. Keep that assumption in mind when relying on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;CloudFront Functions runtime 2.0 includes the &lt;code&gt;crypto&lt;/code&gt; module, so SHA-256 can be calculated directly inside the 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="c1"&gt;// JA4H for CloudFront Functions (runtime: cloudfront-js-2.0)&lt;/span&gt;
&lt;span class="c1"&gt;// Requires CloudFront-Viewer-Header-Order and&lt;/span&gt;
&lt;span class="c1"&gt;// CloudFront-Viewer-Http-Version in the origin request policy.&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&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;ZERO12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;000000000000&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;sha12&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;12&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;req&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&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-header-order&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="nx"&gt;h&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-header-order&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;httpVer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&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-http-version&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="nx"&gt;h&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-http-version&lt;/span&gt;&lt;span class="dl"&gt;'&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="s1"&gt;1.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;// Exclude Cookie and Referer while preserving transmission order,&lt;/span&gt;
    &lt;span class="c1"&gt;// casing, and duplicate header names.&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;order&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&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;l&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cookie&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;l&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;referer&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;// ---- JA4H_a ----&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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="nf"&gt;slice&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// GET -&amp;gt; ge&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpVer&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="s1"&gt;.&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// 1.1 -&amp;gt; 11, 2.0 -&amp;gt; 20&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;ver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;ver&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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;cookieKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&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;hasCookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cookieKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;c&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;n&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;hasReferer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referer&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;r&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;n&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtered&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="mi"&gt;99&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;countStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;count&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;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0000&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;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accept-language&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accept-language&lt;/span&gt;&lt;span class="dl"&gt;'&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;split&lt;/span&gt;&lt;span class="p"&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="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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&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="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="nf"&gt;toLowerCase&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="sr"&gt;/-/g&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="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;4&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ver&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hasCookie&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hasReferer&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;countStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ---- JA4H_b: hash header names in transmission order ----&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filtered&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="nf"&gt;sha12&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtered&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;ZERO12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ---- JA4H_c / JA4H_d: sort and hash Cookie names / name=value pairs ----&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cnames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;cpairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nx"&gt;cookieKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;n&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;cnames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;cpairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;m&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;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;cnames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;cpairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;c&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;span class="nx"&gt;cnames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;cpairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&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;c12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cnames&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="nf"&gt;sha12&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cnames&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;ZERO12&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;d12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cpairs&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="nf"&gt;sha12&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cpairs&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;ZERO12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;req&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-ja4h&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;a&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;b&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;c12&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;d12&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;req&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;Duplicate Cookies can be retrieved from the &lt;code&gt;multiValue&lt;/code&gt; array. Because JA4H_c and JA4H_d hash sorted values, their original order is not needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the Output with the Reference Implementation
&lt;/h2&gt;

&lt;p&gt;Some details of JA4H_a—such as exactly which headers are included in the count and how Accept-Language is normalized—are described only briefly in the FoxIO specification. That makes differences between implementations relatively easy to introduce.&lt;/p&gt;

&lt;p&gt;Before deploying this in production, compare the output with FoxIO’s reference implementation. The repository includes a Rust CLI named &lt;code&gt;ja4&lt;/code&gt; that analyzes pcap files and emits the various JA4+ fingerprints. It uses &lt;code&gt;tshark&lt;/code&gt; internally, and prebuilt binaries are available from the repository’s Releases page.&lt;/p&gt;

&lt;p&gt;The important point is that JA4H requires plaintext HTTP headers. An HTTPS packet capture cannot be analyzed as-is because the headers are encrypted.&lt;/p&gt;

&lt;p&gt;A convenient workaround is to send an equivalent request over plaintext HTTP to a local server and capture it there. &lt;code&gt;curl&lt;/code&gt; sends the same header names when invoked with the same command-line options. The Host value will differ, but that does not affect JA4H_b because it hashes header names rather than values.&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="c"&gt;# Install tshark and obtain the ja4 binary (macOS example)&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; wireshark
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /Applications/Wireshark.app/Contents/MacOS/tshark /usr/local/bin/tshark

&lt;span class="c"&gt;# Start a local plaintext HTTP server and capture traffic&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000 &amp;amp;
&lt;span class="nb"&gt;sudo &lt;/span&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; lo0 &lt;span class="nt"&gt;-w&lt;/span&gt; /tmp/ja4h.pcap &lt;span class="s1"&gt;'port 8000'&lt;/span&gt; &amp;amp;

&lt;span class="c"&gt;# Send a request with the same method, HTTP version, headers, and Cookies&lt;/span&gt;
&lt;span class="c"&gt;# as the request sent to CloudFront&lt;/span&gt;
curl &lt;span class="nt"&gt;--http1&lt;/span&gt;.1 http://localhost:8000/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Language: ja"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Test-ZZZ: 1"&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"session=abc"&lt;/span&gt;

&lt;span class="c"&gt;# Extract JA4H and compare it with x-ja4h from CloudFront Functions&lt;/span&gt;
./ja4 /tmp/ja4h.pcap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the results differ, it helps to add temporary debug output to the function that exposes JA4H_a and &lt;code&gt;b_input&lt;/code&gt;, the string before hashing. That makes it much easier to identify which component is responsible for the mismatch.&lt;/p&gt;

&lt;p&gt;To validate an HTTPS request without recreating it over plaintext HTTP, another option is to record session keys with &lt;code&gt;SSLKEYLOGFILE&lt;/code&gt;, decrypt the capture in Wireshark, and inspect it with the JA4 plugin.&lt;/p&gt;

&lt;p&gt;A quick licensing note: TLS-based JA4 is licensed under the BSD 3-Clause License, while JA4+ methods including JA4H use the FoxIO License 1.1. Internal use for protecting your own services is permitted, but offering the functionality as part of a commercial product requires an OEM license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing False Positives with a Challenge Page
&lt;/h2&gt;

&lt;p&gt;Immediately blocking every request that matches a JA4 or JA4H fingerprint can be risky.&lt;/p&gt;

&lt;p&gt;Security products, VPNs, and proxies may reorder headers. When that order differs from normal browser behavior, a legitimate user’s fingerprint may no longer match the fingerprint normally produced by their browser, leading to false positives or false negatives.&lt;/p&gt;

&lt;p&gt;JA4H also intentionally groups clients using the same implementation. As a result, many legitimate users running the same browser version may share the same fingerprint. A mistake in blocklist management could therefore block every one of them at once.&lt;/p&gt;

&lt;p&gt;Instead of returning a 403 immediately, I recommend redirecting suspicious JA4H fingerprints to a challenge page. A human user can complete the verification step, and requests carrying proof of successful completion can then pass through normally.&lt;/p&gt;

&lt;p&gt;The action for each fingerprint can be stored in KVS, with the &lt;code&gt;viewer-request&lt;/code&gt; function deciding how to handle it:&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;kvs&lt;/span&gt; &lt;span class="o"&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;kvs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ja4h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeJa4h&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="c1"&gt;// JA4H calculation logic&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate an expiring, HMAC-signed clearance Cookie.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasValidClearance&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;ja4h&lt;/span&gt;&lt;span class="p"&gt;))&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;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-ja4h&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;ja4h&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;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kvs&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="nx"&gt;ja4h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Unregistered = allow */&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;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;challenge&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;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;302&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;location&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;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;/challenge?return=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&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;uri&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;cache-control&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;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;no-store&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="p"&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;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&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;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&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;Forbidden&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="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-ja4h&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;ja4h&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;The contents of &lt;code&gt;/challenge&lt;/code&gt; depend on your requirements. The simplest option is a JavaScript challenge that merely confirms the client can execute JavaScript, because many basic bots cannot.&lt;/p&gt;

&lt;p&gt;For stronger verification, validate Cloudflare Turnstile or reCAPTCHA at the origin and issue an HMAC-signed clearance Cookie after successful completion. Runtime 2.0’s &lt;code&gt;crypto&lt;/code&gt; module also supports HMAC, so subsequent Cookie validation can remain entirely at the edge.&lt;/p&gt;

&lt;p&gt;You might be wondering whether AWS WAF’s Challenge or CAPTCHA actions could handle this instead. On CloudFront, however, WAF evaluation runs before CloudFront Functions. That means a WAF rule cannot use an &lt;code&gt;x-ja4h&lt;/code&gt; header added by the function during the same request.&lt;/p&gt;

&lt;p&gt;When routing decisions must be based on JA4H, the function itself also needs to perform the challenge redirect. As an aside, AWS WAF has a &lt;code&gt;HeaderOrder&lt;/code&gt; match component, so WAF alone can still perform checks based only on header order.&lt;/p&gt;

&lt;p&gt;A practical rollout would look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Observe JA4H values for all requests by recording &lt;code&gt;x-ja4h&lt;/code&gt; in the origin access logs.&lt;/li&gt;
&lt;li&gt;Identify fingerprints that appear disproportionately in attack traffic.&lt;/li&gt;
&lt;li&gt;Register those fingerprints in KVS with the &lt;code&gt;challenge&lt;/code&gt; action.&lt;/li&gt;
&lt;li&gt;Promote only fingerprints that are clearly malicious to &lt;code&gt;block&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt; lets you recover header order, casing, and duplicate names with high fidelity. The event object alone cannot do this because it lowercases header names and loses their transmission order.&lt;/li&gt;
&lt;li&gt;Combined with &lt;code&gt;CloudFront-Viewer-Http-Version&lt;/code&gt;, it allows a JA4H-equivalent fingerprint to be calculated inline inside CloudFront Functions.&lt;/li&gt;
&lt;li&gt;Because legitimate users can share the same JA4H value, a staged response using KVS and a challenge page is safer than blocking immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudFront may eventually expose JA4H as another generated viewer header. Until then, this is one way to implement comparable fingerprinting yourself.&lt;/p&gt;

&lt;p&gt;Give it a try—and, as always with fingerprints, observe first and block carefully.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
    </item>
    <item>
      <title>Implementing Impossible Location Detection with Amazon CloudFront Functions + KVS</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:20:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/implementing-impossible-location-detection-with-amazon-cloudfront-functions-kvs-5a1n</link>
      <guid>https://dev.to/aws-builders/implementing-impossible-location-detection-with-amazon-cloudfront-functions-kvs-5a1n</guid>
      <description>&lt;p&gt;This article is the CloudFront Functions + CloudFront KeyValueStore (KVS) version of the previous article, Implementing Impossible Location Detection Without Application Changes Using Amazon CloudFront + Lambda@Edge.&lt;/p&gt;

&lt;p&gt;If you have not read the Lambda@Edge version yet, I recommend reading it first. It will make the differences in this article much easier to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why CloudFront Functions?
&lt;/h2&gt;

&lt;p&gt;In the previous Lambda@Edge version, I implemented Impossible Location detection without modifying the application.&lt;/p&gt;

&lt;p&gt;However, Lambda@Edge has several limitations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lambda@Edge limitation&lt;/th&gt;
&lt;th&gt;CloudFront Functions version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; is not available in Viewer Request&lt;/td&gt;
&lt;td&gt;Available in Viewer Request, with the right Origin Request Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CachingDisabled&lt;/code&gt; is required for protected paths&lt;/td&gt;
&lt;td&gt;Cacheable responses can still be protected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment to &lt;code&gt;us-east-1&lt;/code&gt; is required&lt;/td&gt;
&lt;td&gt;No Lambda@Edge regional deployment constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda@Edge costs $0.60 per 1 million requests&lt;/td&gt;
&lt;td&gt;CloudFront Functions cost $0.10 per 1 million requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;However, CloudFront Functions also require one important trade-off.&lt;/p&gt;

&lt;p&gt;With this approach, the application needs to write location information to CloudFront KeyValueStore when login succeeds.&lt;/p&gt;

&lt;p&gt;In other words, unlike the Lambda@Edge version, this version requires a small application-side change.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;In the Lambda@Edge version, an Origin Response Lambda automatically detected successful login responses and set a signed cookie.&lt;/p&gt;

&lt;p&gt;In the CloudFront Functions version, the application writes the viewer’s country code and ASN to &lt;strong&gt;CloudFront KeyValueStore (KVS)&lt;/strong&gt; when login succeeds.&lt;/p&gt;

&lt;p&gt;After that, a CloudFront Function checks the KVS entry on each protected request.&lt;/p&gt;

&lt;h3&gt;
  
  
  At Login
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fubdlfljdkthtrearq8z1.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%2Fubdlfljdkthtrearq8z1.png" alt="At Login" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When login succeeds, the application reads the &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; headers and writes the session hash, country, and ASN to KVS.&lt;/p&gt;

&lt;p&gt;This is the main difference from the Lambda@Edge version.&lt;/p&gt;

&lt;p&gt;In the Lambda@Edge version, the edge layer automatically detects successful login responses.&lt;br&gt;
In this version, the application explicitly writes the login-time location to KVS.&lt;/p&gt;
&lt;h3&gt;
  
  
  When Accessing Protected Pages
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnggo3mdylfjys6jh05k2.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%2Fnggo3mdylfjys6jh05k2.png" alt="When Accessing Protected Pages" width="799" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The biggest advantage is that verification is completed at &lt;strong&gt;Viewer Request&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;CloudFront Functions can access &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; during the Viewer Request phase, so we do not need to wait until Origin Request.&lt;/p&gt;

&lt;p&gt;This means &lt;code&gt;CachingDisabled&lt;/code&gt; is no longer required for protected cache behaviors.&lt;/p&gt;

&lt;p&gt;That is a major advantage.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture Difference from the Lambda@Edge Version
&lt;/h2&gt;

&lt;p&gt;The difference between the two versions can be summarized like this:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y7b9lolctfcf34tpgjp.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%2F4y7b9lolctfcf34tpgjp.png" alt="Architecture Difference from the Lambda@Edge Version" width="800" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Lambda@Edge version is useful when you cannot modify the application.&lt;/p&gt;

&lt;p&gt;The CloudFront Functions + KVS version is useful when you can add a small amount of login-side logic and want better cost, latency, and cache compatibility.&lt;/p&gt;


&lt;h2&gt;
  
  
  Origin Request Policy Is Still Required
&lt;/h2&gt;

&lt;p&gt;There is one important catch.&lt;/p&gt;

&lt;p&gt;For CloudFront Functions to read &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; at Viewer Request, the behavior must have an Origin Request Policy that includes these headers.&lt;/p&gt;

&lt;p&gt;If the Origin Request Policy is not configured correctly, the headers will not be available to the CloudFront Function.&lt;/p&gt;

&lt;p&gt;Depending on how you implement error handling, this may result in either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fail open, where requests are allowed without validation&lt;/li&gt;
&lt;li&gt;fail closed, where all users are blocked or redirected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; cannot be added through a Cache Policy. It must be handled through an Origin Request Policy.&lt;/p&gt;

&lt;p&gt;In this sample, I use the managed policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AllViewerExceptHostHeader
b689b0a8-53d0-40ab-baf2-68738e2966ac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CloudFront Functions runtime 2.0, these headers can be accessed like this:&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;h&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;headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country: &lt;/span&gt;&lt;span class="dl"&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;h&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-country&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="nx"&gt;h&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-country&lt;/span&gt;&lt;span class="dl"&gt;'&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="s1"&gt;undefined&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asn: &lt;/span&gt;&lt;span class="dl"&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;h&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-asn&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="nx"&gt;h&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-asn&lt;/span&gt;&lt;span class="dl"&gt;'&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="s1"&gt;undefined&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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deployment, I recommend testing header delivery with a small validation function like this before enabling the full protection logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Default Protection Mode
&lt;/h2&gt;

&lt;p&gt;This implementation uses a default protection model.&lt;/p&gt;

&lt;p&gt;Instead of listing every protected path, we list only public paths.&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ilg"&lt;/span&gt; &lt;span class="p"&gt;{&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;"your-org/impossible-location-guard-cf/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;public_paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/assets/*"&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;Everything else is protected by default.&lt;/p&gt;

&lt;p&gt;This follows the same principle as “default deny” in firewall design.&lt;/p&gt;

&lt;p&gt;New pages are automatically protected unless they are explicitly listed as public paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Implementation Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Handling SPA and API Requests
&lt;/h3&gt;

&lt;p&gt;Returning &lt;code&gt;302&lt;/code&gt; redirects for API calls can break SPA behavior.&lt;/p&gt;

&lt;p&gt;If the frontend uses &lt;code&gt;fetch&lt;/code&gt; or XHR, it may receive the login page HTML and then fail when trying to parse it as JSON.&lt;/p&gt;

&lt;p&gt;Recommended approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return &lt;code&gt;302&lt;/code&gt; for browser page requests&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;401&lt;/code&gt; for API requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, requests with &lt;code&gt;Accept: application/json&lt;/code&gt; or paths under &lt;code&gt;/api/*&lt;/code&gt; should usually receive &lt;code&gt;401 Unauthorized&lt;/code&gt; instead of a redirect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validate KVS Writes Carefully
&lt;/h2&gt;

&lt;p&gt;Writing to KVS is the responsibility of the application.&lt;/p&gt;

&lt;p&gt;Therefore, the application must write to KVS only after login succeeds.&lt;/p&gt;

&lt;p&gt;If the application writes location data on failed login attempts, an attacker may be able to poison session data.&lt;/p&gt;

&lt;p&gt;The rule is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only write to KVS after successful authentication.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Using ASN As Well
&lt;/h2&gt;

&lt;p&gt;ASN validation can also be enabled.&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ilg"&lt;/span&gt; &lt;span class="p"&gt;{&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;"your-org/impossible-location-guard-cf/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;check_asn&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;Country-level detection is useful, but it is not always enough.&lt;/p&gt;

&lt;p&gt;For example, an attacker may access the application from the same country as the victim by using a cloud provider or proxy in that country.&lt;/p&gt;

&lt;p&gt;By also checking ASN, you may be able to detect suspicious access even when the country code is the same.&lt;/p&gt;

&lt;p&gt;However, ASN changes can also happen during legitimate usage, such as when a user switches between mobile data and Wi-Fi.&lt;/p&gt;

&lt;p&gt;For that reason, &lt;code&gt;check_asn&lt;/code&gt; is disabled by default and should be enabled based on your service’s risk tolerance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reauthentication Instead of Hard Blocking
&lt;/h2&gt;

&lt;p&gt;As with the Lambda@Edge version, I recommend redirecting users to reauthentication instead of hard-blocking them.&lt;/p&gt;

&lt;p&gt;If a legitimate user is completely blocked due to a false positive, they may feel that the service is broken or unreliable.&lt;/p&gt;

&lt;p&gt;On the other hand, if they are simply asked to log in again, they may think their session expired.&lt;/p&gt;

&lt;p&gt;When suspicious access is detected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redirect the user to &lt;code&gt;/login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The user logs in again&lt;/li&gt;
&lt;li&gt;The KVS entry is updated with the current country and ASN&lt;/li&gt;
&lt;li&gt;Access resumes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One advantage of the KVS version is that the KVS entry can be deleted on logout.&lt;/p&gt;

&lt;p&gt;This enables immediate session-level invalidation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Terraform
&lt;/h3&gt;

&lt;p&gt;In the Lambda@Edge version, protected paths required &lt;code&gt;CachingDisabled&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the CloudFront Functions version, the function runs at Viewer Request. This means the location check is still executed even when the response is served from cache.&lt;/p&gt;

&lt;p&gt;This is one of the biggest advantages of the CloudFront Functions version.&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ilg"&lt;/span&gt; &lt;span class="p"&gt;{&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;"your-org/impossible-location-guard-cf/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;

  &lt;span class="nx"&gt;project_name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myapp"&lt;/span&gt;
  &lt;span class="nx"&gt;session_cookie_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"session_id"&lt;/span&gt;
  &lt;span class="nx"&gt;redirect_path&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;public_paths&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/assets/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;check_asn&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;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;"main"&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;"app"&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;"DELETE"&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;"PATCH"&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;"PUT"&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="s2"&gt;"4135ea2d-6df8-44a3-9df3-4b5a84be39ad"&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="s2"&gt;"b689b0a8-53d0-40ab-baf2-68738e2966ac"&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-request"&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ilg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudfront_function_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;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;"/assets/*"&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;"s3"&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="s2"&gt;"658327ea-f89d-4fab-a63d-7e88639e58f6"&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;The application runtime role also needs permission to write to KVS.&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_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"app_kvs_write"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ilg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kvs_write_policy_arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Application-Side KVS Write
&lt;/h2&gt;

&lt;p&gt;Here is an example using the AWS SDK for JavaScript v3.&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;CloudFrontKeyValueStoreClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DescribeKeyValueStoreCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;PutKeyCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&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;@aws-sdk/client-cloudfront-keyvaluestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&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;crypto&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CloudFrontKeyValueStoreClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KVS_ARN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ILG_KVS_ARN&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;sessionKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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-country&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="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;asn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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-asn&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="dl"&gt;''&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;let&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;retries&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ETag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DescribeKeyValueStoreCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;KvsARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KVS_ARN&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;KvsARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KVS_ARN&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="nf"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;asn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;IfMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ETag&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&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;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;retries&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session ID should not be used directly as the KVS key.&lt;/p&gt;

&lt;p&gt;If someone has permission to read KVS entries, using the raw session ID as the key would expose session IDs.&lt;/p&gt;

&lt;p&gt;Hashing the session ID reduces that risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Login Flow Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;registerLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;httpOnly&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="na"&gt;secure&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="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authentication failed&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="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;If you use an external identity provider, such as an OIDC provider, this approach can still work as long as the callback URL goes through CloudFront.&lt;/p&gt;

&lt;p&gt;The request flow would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser -&amp;gt; CloudFront -&amp;gt; application callback endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudFront adds &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt;, and the callback handler can call &lt;code&gt;registerLocation&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Logout Handling
&lt;/h2&gt;

&lt;p&gt;The KVS version can delete the session’s location entry when the user logs out.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DeleteKeyCommand&lt;/span&gt; &lt;span class="p"&gt;}&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;@aws-sdk/client-cloudfront-keyvaluestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onLogout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ETag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DescribeKeyValueStoreCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;KvsARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KVS_ARN&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeleteKeyCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;KvsARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KVS_ARN&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="nf"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;IfMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ETag&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the advantages of the KVS version.&lt;/p&gt;

&lt;p&gt;It enables immediate invalidation at the session level.&lt;/p&gt;




&lt;h2&gt;
  
  
  BFF Architecture
&lt;/h2&gt;

&lt;p&gt;This approach also works with a BFF, or Backend for Frontend, architecture.&lt;/p&gt;

&lt;p&gt;If the BFF calls a downstream authentication API, make sure the CloudFront behavior for the BFF uses an Origin Request Policy such as &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That way, &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; are forwarded from CloudFront to the BFF.&lt;/p&gt;

&lt;p&gt;The BFF then writes to KVS.&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;registerLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&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;One important point:&lt;/p&gt;

&lt;p&gt;You should trust &lt;code&gt;cloudfront-viewer-country&lt;/code&gt; and &lt;code&gt;cloudfront-viewer-asn&lt;/code&gt; only when they are received directly from CloudFront.&lt;/p&gt;

&lt;p&gt;KVS writes should be done by the process that directly receives these headers from CloudFront.&lt;/p&gt;

&lt;p&gt;If you really need to relay those headers to another service, you should protect their integrity using mechanisms such as request signing, mTLS, private network controls, or strict origin access restrictions.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Authentication System Uses a Different Domain
&lt;/h2&gt;

&lt;p&gt;Suppose your architecture uses separate subdomains, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth.example.com
api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Lambda@Edge version, you needed to share cookies across subdomains and associate Lambda@Edge functions with multiple distributions.&lt;/p&gt;

&lt;p&gt;In the KVS version, the backend writes directly to KVS, so you do not need to associate Lambda functions across distributions in the same way.&lt;/p&gt;

&lt;p&gt;However, the login request must go through CloudFront.&lt;/p&gt;

&lt;p&gt;The country and ASN written to KVS come from CloudFront headers. If login does not pass through CloudFront, the application cannot obtain the same viewer location information used by the CloudFront Function.&lt;/p&gt;

&lt;p&gt;Using your own GeoIP lookup instead may cause false positives because your GeoIP database may not match CloudFront’s GeoIP database.&lt;/p&gt;

&lt;p&gt;A typical flow would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth.example.com via CloudFront
-&amp;gt; login API
-&amp;gt; PutKey to KVS

api.example.com CloudFront Function
-&amp;gt; read the same KVS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session cookie, such as &lt;code&gt;session_id&lt;/code&gt;, must be shared with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Domain=.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Blocking Direct Access to the Origin
&lt;/h2&gt;

&lt;p&gt;This mechanism assumes that the origin can only be accessed through CloudFront.&lt;/p&gt;

&lt;p&gt;If attackers can directly access the origin, they can bypass the CloudFront Function entirely by sending requests directly to the origin with the stolen session cookie.&lt;/p&gt;

&lt;p&gt;This is not just a header spoofing problem.&lt;/p&gt;

&lt;p&gt;If CloudFront is bypassed, the validation logic does not run at all.&lt;/p&gt;

&lt;p&gt;Examples of origin access protection include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For ALB: allow only the CloudFront managed prefix list, &lt;code&gt;com.amazonaws.global.cloudfront.origin-facing&lt;/code&gt;, in the security group&lt;/li&gt;
&lt;li&gt;For S3: use Origin Access Control (OAC)&lt;/li&gt;
&lt;li&gt;For custom origins: add a secret header from CloudFront to the origin and validate it at the origin or with AWS WAF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Origin protection is a prerequisite for this design.&lt;/p&gt;




&lt;h2&gt;
  
  
  KVS Limitations
&lt;/h2&gt;

&lt;p&gt;CloudFront KVS is suitable for small to medium-sized services.&lt;/p&gt;

&lt;p&gt;Before using it for session-level state, you need to understand its limitations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capacity
&lt;/h3&gt;

&lt;p&gt;CloudFront KVS has a 5 MB limit for the entire store.&lt;/p&gt;

&lt;p&gt;It is suitable for small values such as configuration, feature flags, or in this case, session hash to location mapping.&lt;/p&gt;

&lt;p&gt;Depending on the exact key and value size, the practical upper limit is likely on the order of tens of thousands of active sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  No TTL
&lt;/h3&gt;

&lt;p&gt;KVS entries do not have a TTL.&lt;/p&gt;

&lt;p&gt;If a user’s session expires without logout, the KVS key remains unless you delete it.&lt;/p&gt;

&lt;p&gt;Therefore, you need a cleanup mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write Rate and ETag Conflicts
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PutKey&lt;/code&gt; uses optimistic concurrency control with ETags.&lt;/p&gt;

&lt;p&gt;The typical flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DescribeKeyValueStore
-&amp;gt; get ETag
-&amp;gt; PutKey with IfMatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If many users log in at the same time, ETag conflicts may occur.&lt;/p&gt;

&lt;p&gt;Retries are required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eventual Consistency
&lt;/h3&gt;

&lt;p&gt;CloudFront KVS is eventually consistent.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;PutKey&lt;/code&gt;, the value may not be immediately readable from every edge location.&lt;/p&gt;

&lt;p&gt;In most cases, propagation is fast, but if your login flow immediately redirects users to a protected page, there is a small chance that legitimate users may be redirected back to login because the KVS entry has not propagated yet.&lt;/p&gt;

&lt;p&gt;Possible mitigations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;showing a short loading screen after login&lt;/li&gt;
&lt;li&gt;retrying the protected request&lt;/li&gt;
&lt;li&gt;showing a friendly message such as “Please try again”&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Not DynamoDB?
&lt;/h2&gt;

&lt;p&gt;You may wonder:&lt;/p&gt;

&lt;p&gt;Wouldn’t DynamoDB be better for session-level state?&lt;/p&gt;

&lt;p&gt;In many ways, yes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;CloudFront KVS&lt;/th&gt;
&lt;th&gt;DynamoDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Capacity&lt;/td&gt;
&lt;td&gt;5 MB limit&lt;/td&gt;
&lt;td&gt;Practically unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL&lt;/td&gt;
&lt;td&gt;No TTL&lt;/td&gt;
&lt;td&gt;TTL attribute supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writes&lt;/td&gt;
&lt;td&gt;ETag conflicts and retries&lt;/td&gt;
&lt;td&gt;Simple &lt;code&gt;PutItem&lt;/code&gt; overwrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read latency&lt;/td&gt;
&lt;td&gt;A few milliseconds at the edge&lt;/td&gt;
&lt;td&gt;Round trip to a region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;SigV4A, which can be tricky&lt;/td&gt;
&lt;td&gt;Standard SigV4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So why use KVS?&lt;/p&gt;

&lt;p&gt;The reason is latency and execution location.&lt;/p&gt;

&lt;p&gt;KVS is available from CloudFront Functions at the edge. Reads can complete in a few milliseconds.&lt;/p&gt;

&lt;p&gt;DynamoDB is regional. Reading from DynamoDB adds a network round trip from the edge to the AWS region.&lt;/p&gt;

&lt;p&gt;More importantly, CloudFront Functions cannot call DynamoDB directly.&lt;/p&gt;

&lt;p&gt;CloudFront Functions do not allow external network access.&lt;/p&gt;

&lt;p&gt;So if you want to use DynamoDB, you would need to move the check to Lambda@Edge, usually at Origin Request.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CachingDisabled&lt;/code&gt; is required again&lt;/li&gt;
&lt;li&gt;validation runs at Origin Request instead of Viewer Request&lt;/li&gt;
&lt;li&gt;latency increases&lt;/li&gt;
&lt;li&gt;Lambda@Edge pricing applies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the architecture becomes very similar to the Lambda@Edge version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Lambda@Edge + Cookie&lt;/th&gt;
&lt;th&gt;CloudFront Functions + KVS&lt;/th&gt;
&lt;th&gt;Lambda@Edge + DynamoDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application change&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;KVS write required&lt;/td&gt;
&lt;td&gt;DynamoDB write required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per 1M requests&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;td&gt;$0.10 + KVS $0.03&lt;/td&gt;
&lt;td&gt;$0.60 + DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution phase&lt;/td&gt;
&lt;td&gt;Origin Request&lt;/td&gt;
&lt;td&gt;Viewer Request&lt;/td&gt;
&lt;td&gt;Origin Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache compatibility&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CachingDisabled&lt;/code&gt; required&lt;/td&gt;
&lt;td&gt;Cacheable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CachingDisabled&lt;/code&gt; required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;No practical session-store limit&lt;/td&gt;
&lt;td&gt;5 MB / tens of thousands of sessions&lt;/td&gt;
&lt;td&gt;Scalable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL / cleanup&lt;/td&gt;
&lt;td&gt;Cookie expiration&lt;/td&gt;
&lt;td&gt;Manual cleanup&lt;/td&gt;
&lt;td&gt;DynamoDB TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immediate invalidation&lt;/td&gt;
&lt;td&gt;Difficult&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeleteKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeleteItem&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment constraint&lt;/td&gt;
&lt;td&gt;Lambda@Edge in &lt;code&gt;us-east-1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No Lambda@Edge regional constraint&lt;/td&gt;
&lt;td&gt;Lambda@Edge in &lt;code&gt;us-east-1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As of July 7, 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;CloudFront Functions can access &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; at Viewer Request, but the correct Origin Request Policy is required.&lt;/p&gt;

&lt;p&gt;Using KVS removes the need for &lt;code&gt;CachingDisabled&lt;/code&gt;, so cached content can still be protected.&lt;/p&gt;

&lt;p&gt;A default protection model, where only public paths are listed, helps avoid accidental exposure of new pages.&lt;/p&gt;

&lt;p&gt;ASN checking can help detect suspicious access even within the same country.&lt;/p&gt;

&lt;p&gt;KVS is suitable for small to medium-sized services, but it has important limitations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 MB capacity&lt;/li&gt;
&lt;li&gt;no TTL&lt;/li&gt;
&lt;li&gt;write rate limits&lt;/li&gt;
&lt;li&gt;ETag conflicts&lt;/li&gt;
&lt;li&gt;eventual consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DynamoDB solves many of these scale and lifecycle problems, but CloudFront Functions cannot call DynamoDB directly. Using DynamoDB would bring the architecture back to Lambda@Edge.&lt;/p&gt;

&lt;p&gt;BFF architectures can also use this approach, as long as the BFF writes to KVS using headers received directly from CloudFront.&lt;/p&gt;

&lt;p&gt;Blocking direct access to the origin is a prerequisite. If attackers can bypass CloudFront, the entire validation logic can be bypassed.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;cloudfront-viewer-*&lt;/code&gt; headers should be trusted only by the process that receives them directly from CloudFront. They should not be blindly relayed to downstream services.&lt;/p&gt;

&lt;p&gt;When I first thought of this idea, I expected it to be fairly simple.&lt;/p&gt;

&lt;p&gt;But once I started considering the security details, there were many more things to think about than I expected.&lt;/p&gt;

&lt;p&gt;Whether this is suitable for production depends on the service, traffic volume, authentication flow, and risk tolerance.&lt;/p&gt;

&lt;p&gt;Still, I think it is worth considering as one possible way to detect session hijacking at the edge.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
    </item>
    <item>
      <title>Implementing Impossible Location Detection Without Application Changes Using Amazon CloudFront + Lambda@Edge</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:19:58 +0000</pubDate>
      <link>https://dev.to/aws-builders/implementing-impossible-location-detection-without-application-changes-using-amazon-cloudfront--pp9</link>
      <guid>https://dev.to/aws-builders/implementing-impossible-location-detection-without-application-changes-using-amazon-cloudfront--pp9</guid>
      <description>&lt;p&gt;When a session is compromised, where does the attacker access your application from?&lt;/p&gt;

&lt;p&gt;For example, if a session cookie is stolen through XSS, an attacker may be able to impersonate the victim simply by setting that cookie in their own browser. In many cases, that access comes from a different country, network, or cloud provider than the legitimate user normally uses.&lt;/p&gt;

&lt;p&gt;This is where the idea of &lt;strong&gt;Impossible Location&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;If a user logs in from Tokyo and then accesses the application from New York five minutes later, that movement is physically impossible. In that case, session hijacking should be considered highly suspicious.&lt;/p&gt;

&lt;p&gt;However, I feel that this kind of detection is not always implemented in web applications, especially as a generic protection layer outside the application itself.&lt;/p&gt;

&lt;p&gt;In this article, I will introduce an approach for detecting and responding to Impossible Location access using Amazon CloudFront and Lambda@Edge, without modifying the application.&lt;/p&gt;

&lt;p&gt;This is only one possible design. I do not think this is the only correct answer, and there may be better architectures depending on your application and authentication flow. Please consider this article as one implementation idea.&lt;/p&gt;

&lt;p&gt;Also, this implementation detects a change in &lt;strong&gt;country&lt;/strong&gt; and optionally &lt;strong&gt;ASN&lt;/strong&gt; from the time of login. It does not calculate physical travel speed. For example, even if a legitimate user travels abroad 12 hours after logging in, they may be asked to reauthenticate.&lt;/p&gt;

&lt;p&gt;That said, this design redirects users to reauthentication instead of hard-blocking them, so the impact on legitimate users should be relatively small.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This mechanism is designed to help mitigate session hijacking.&lt;/p&gt;

&lt;p&gt;It does not prevent password compromise itself. For leaked passwords, MFA is still one of the most effective countermeasures. I recommend using this kind of location-based session protection together with MFA.&lt;/p&gt;

&lt;p&gt;This article covers the Lambda@Edge version.&lt;/p&gt;

&lt;p&gt;If you can add a small amount of logic to the application during login, another approach using &lt;strong&gt;Amazon CloudFront Functions + CloudFront KeyValueStore (KVS)&lt;/strong&gt; is also possible. That version has advantages in cost and latency, so I will cover it in a separate article.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The basic idea is simple.&lt;/p&gt;

&lt;p&gt;At login time, we store the viewer’s country information in a signed cookie.&lt;/p&gt;

&lt;p&gt;After login, we compare the current viewer location with the location stored at login.&lt;/p&gt;

&lt;p&gt;If the country, or optionally the ASN, changes unexpectedly, we redirect the user to the login page and ask them to reauthenticate.&lt;/p&gt;




&lt;h2&gt;
  
  
  At Login
&lt;/h2&gt;

&lt;p&gt;When the login API returns a successful response, an &lt;strong&gt;Origin Response Lambda@Edge&lt;/strong&gt; function intercepts the response.&lt;/p&gt;

&lt;p&gt;The Lambda function reads the &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; header, signs the value with HMAC-SHA256, and sets it as a cookie.&lt;/p&gt;

&lt;p&gt;Because this is done at the CloudFront layer, the application itself does not need to be modified.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpihcurqwwg9awyubx6l6.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%2Fpihcurqwwg9awyubx6l6.png" alt="Login flow: setting a signed location cookie with Origin Response Lambda@Edge" width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When Accessing Protected Pages
&lt;/h2&gt;

&lt;p&gt;For authenticated pages, an &lt;strong&gt;Origin Request Lambda@Edge&lt;/strong&gt; function verifies the signed location cookie.&lt;/p&gt;

&lt;p&gt;No external GeoIP database is required.&lt;/p&gt;

&lt;p&gt;No external API call is required.&lt;/p&gt;

&lt;p&gt;Amazon CloudFront automatically provides the viewer country through the &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; header. The Lambda@Edge function only needs to compare that value with the signed cookie.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjzpzcmbxhl10lpxyvry.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%2Fcjzpzcmbxhl10lpxyvry.png" alt="Protected page flow: verifying location with Origin Request Lambda@Edge" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The “Oh, That’s Why!” Moment
&lt;/h2&gt;

&lt;p&gt;At first, I thought the security check should be implemented in a &lt;strong&gt;Viewer Request Lambda@Edge&lt;/strong&gt; function.&lt;/p&gt;

&lt;p&gt;That sounds like the ideal place to block suspicious requests, right?&lt;/p&gt;

&lt;p&gt;Viewer Request is the earliest event in the CloudFront request lifecycle, so it feels like the best place to reject malicious requests before they go any further.&lt;/p&gt;

&lt;p&gt;However, when I actually tested it, the &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; header was not available in the Viewer Request Lambda.&lt;/p&gt;

&lt;p&gt;Even after adding it to the Cache Policy and Origin Request Policy, and even after creating a debug endpoint, the value was always &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After some investigation, I found the reason in the AWS documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CloudFront adds the CloudFront-Viewer-Country header after the viewer request event.&lt;/p&gt;
&lt;/blockquote&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%2Fht60hpsm8i52evfu0iw7.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%2Fht60hpsm8i52evfu0iw7.png" alt="CloudFront header timing and cache behavior" width="799" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In other words, CloudFront determines the viewer country after the Viewer Request event.&lt;/p&gt;

&lt;p&gt;So at the time the Viewer Request Lambda runs, the &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; header does not exist yet.&lt;/p&gt;

&lt;p&gt;Adding the header to a Cache Policy or Origin Request Policy means that the header can be made available from the Origin Request phase onward. It does not make the header available to Viewer Request.&lt;/p&gt;

&lt;p&gt;The same limitation applies even if you configure the policies correctly.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; cannot be added to a Cache Policy. It can only be included through an Origin Request Policy.&lt;/p&gt;

&lt;p&gt;Because of this limitation, I changed the architecture so that the location check runs in an Origin Request Lambda@Edge function.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why CachingDisabled Is Required
&lt;/h2&gt;

&lt;p&gt;For protected paths, &lt;code&gt;CachingDisabled&lt;/code&gt; is required.&lt;/p&gt;

&lt;p&gt;This is an important point.&lt;/p&gt;

&lt;p&gt;Origin Request Lambda@Edge is invoked only when CloudFront forwards the request to the origin. If CloudFront serves the response from cache, the Origin Request Lambda does not run.&lt;/p&gt;

&lt;p&gt;That means if a protected page is cacheable and CloudFront returns a cache hit, the security check can be bypassed.&lt;/p&gt;

&lt;p&gt;For authenticated pages, disabling cache is usually expected anyway, so this may not be a big issue in many applications.&lt;/p&gt;

&lt;p&gt;However, if you want to protect cacheable content, you need to be careful.&lt;/p&gt;

&lt;p&gt;This limitation is one of the reasons why the CloudFront Functions + KVS version can be attractive. CloudFront Functions run at Viewer Request, so they can run even when the response is served from cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Origin Request Policy
&lt;/h2&gt;

&lt;p&gt;To receive &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; in an Origin Request Lambda, the CloudFront behavior must use an Origin Request Policy that forwards those headers.&lt;/p&gt;

&lt;p&gt;One thing to be careful about is the &lt;code&gt;Host&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;The managed policy &lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt; includes CloudFront headers, but it also forwards the viewer’s original &lt;code&gt;Host&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;This can cause problems with origins such as API Gateway, Lambda Function URLs, or S3 with OAC.&lt;/p&gt;

&lt;p&gt;For example, API Gateway and Lambda Function URLs expect the origin domain name in the &lt;code&gt;Host&lt;/code&gt; header. If CloudFront forwards the viewer-facing host instead, the origin may return a 403 error.&lt;/p&gt;

&lt;p&gt;For S3 with OAC, forwarding the wrong &lt;code&gt;Host&lt;/code&gt; header may also cause a signature mismatch.&lt;/p&gt;

&lt;p&gt;On the other hand, the managed policy &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt; excludes the viewer’s &lt;code&gt;Host&lt;/code&gt; header. This makes it safer to use with API Gateway, Lambda Function URLs, and S3 origins.&lt;/p&gt;

&lt;p&gt;The AWS documentation says that this policy includes CloudFront viewer location headers such as &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt;, although the documentation is a little inconsistent because the detailed list does not clearly show every CloudFront header.&lt;/p&gt;

&lt;p&gt;In my testing, I confirmed that both &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; were available in the Origin Request Lambda when using &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also confirmed that the &lt;code&gt;Host&lt;/code&gt; header was replaced with the S3 origin domain name, so it did not break OAC signing.&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;"country"&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="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CloudFront-Viewer-Country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JP"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asn"&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="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CloudFront-Viewer-ASN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"17676"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"host"&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="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxx.s3.ap-northeast-1.amazonaws.com"&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;Because the documentation can be slightly confusing here, I recommend validating header delivery with a small debug Lambda before enabling this mechanism in production.&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="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;headers&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;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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;country:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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-country&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asn:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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-asn&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;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;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;
  
  
  Default Protection Mode
&lt;/h2&gt;

&lt;p&gt;In this sample design, I use a default protection model.&lt;/p&gt;

&lt;p&gt;Instead of listing every path that should be protected, we list only public paths.&lt;/p&gt;

&lt;p&gt;For 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;module&lt;/span&gt; &lt;span class="s2"&gt;"impossible_location_guard"&lt;/span&gt; &lt;span class="p"&gt;{&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;"your-org/impossible-location-guard/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;login_paths&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/api/auth/login"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;public_paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/login"&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;Everything except the listed public paths is protected.&lt;/p&gt;

&lt;p&gt;This is similar to the “default deny” approach in firewall design.&lt;/p&gt;

&lt;p&gt;The reason is simple: it is easy to forget to add newly created pages to a protected path list. If that happens, the new page may accidentally become unprotected.&lt;/p&gt;

&lt;p&gt;By listing only public paths, new pages are protected by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Implementation Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Include the Session ID in the HMAC Signature
&lt;/h3&gt;

&lt;p&gt;The signed cookie should not contain only the country and ASN.&lt;/p&gt;

&lt;p&gt;If the HMAC payload only includes country and ASN, a replacement attack may become possible.&lt;/p&gt;

&lt;p&gt;For example, an attacker could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in with their own account&lt;/li&gt;
&lt;li&gt;Obtain a valid &lt;code&gt;_ilg_loc&lt;/code&gt; cookie signed for the attacker’s country&lt;/li&gt;
&lt;li&gt;Steal a victim’s session cookie&lt;/li&gt;
&lt;li&gt;Combine the victim’s session cookie with the attacker’s valid location cookie&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To prevent this, the signed payload should include a value derived from the session ID, such as a hash of the session ID.&lt;/p&gt;

&lt;p&gt;For example:&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;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&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;"sha256(sessionId)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234654290&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;This binds the location cookie to the actual session.&lt;/p&gt;

&lt;p&gt;To verify this value, the Origin Request Lambda needs to know the name of the session cookie. In the sample module, this is configured through &lt;code&gt;session_cookie_name&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Set the Location Cookie Only on Successful Login
&lt;/h3&gt;

&lt;p&gt;Origin Response Lambda@Edge runs for all origin responses, including error responses such as 400 or 401.&lt;/p&gt;

&lt;p&gt;If the Lambda sets the location cookie without checking the response status code, even a failed login attempt could receive a valid location cookie.&lt;/p&gt;

&lt;p&gt;That is not what we want.&lt;/p&gt;

&lt;p&gt;In this sample implementation, the cookie is set only when the origin returns a successful status code, such as &lt;code&gt;200&lt;/code&gt; or &lt;code&gt;201&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, the module has a &lt;code&gt;success_statuses&lt;/code&gt; setting with a default value like:&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="s2"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"201"&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;h3&gt;
  
  
  Manage the HMAC Secret Carefully
&lt;/h3&gt;

&lt;p&gt;Lambda@Edge does not support environment variables in the same way as normal Lambda functions.&lt;/p&gt;

&lt;p&gt;In this sample, the HMAC secret is bundled into the Lambda package as &lt;code&gt;config.json&lt;/code&gt; at Terraform build time.&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;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config.json&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="o"&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;HMAC_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HMAC_SECRET is not configured&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the secret is missing, the function should fail closed.&lt;/p&gt;

&lt;p&gt;Failing open would mean allowing requests without a valid security check, which is dangerous.&lt;/p&gt;

&lt;p&gt;When sharing this mechanism across subdomains, Lambda functions generated from the same module can share the same HMAC secret.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended Cookie Attributes
&lt;/h2&gt;

&lt;p&gt;The location cookie should have secure attributes.&lt;/p&gt;

&lt;p&gt;I recommend at least the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Secure
HttpOnly
SameSite=Lax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Secure&lt;/code&gt; ensures the cookie is sent only over HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HttpOnly&lt;/code&gt; prevents JavaScript from reading or modifying the cookie, which helps reduce the impact of XSS-based tampering attempts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt; helps mitigate CSRF in many common cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling SPA and API Requests
&lt;/h2&gt;

&lt;p&gt;If your frontend is an SPA, it may call APIs using &lt;code&gt;fetch&lt;/code&gt; or XHR.&lt;/p&gt;

&lt;p&gt;In that case, returning a &lt;code&gt;302&lt;/code&gt; redirect for an API request can be inconvenient. The frontend may receive HTML from the login page and then fail when trying to parse it as JSON.&lt;/p&gt;

&lt;p&gt;A more practical approach is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return &lt;code&gt;302&lt;/code&gt; for normal browser page requests&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;401&lt;/code&gt; for API requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if the request path starts with &lt;code&gt;/api/&lt;/code&gt; or the &lt;code&gt;Accept&lt;/code&gt; header includes &lt;code&gt;application/json&lt;/code&gt;, returning &lt;code&gt;401 Unauthorized&lt;/code&gt; may be better than redirecting to &lt;code&gt;/login&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why ASN Can Help
&lt;/h2&gt;

&lt;p&gt;Country-based detection is useful, but it is not always enough.&lt;/p&gt;

&lt;p&gt;For example, in a large country such as the United States, an attacker may access the application from the same country as the victim.&lt;/p&gt;

&lt;p&gt;The same can happen in Japan.&lt;/p&gt;

&lt;p&gt;A legitimate user may log in from a home ISP or an office network, while an attacker may use a cloud provider in the same country. In both cases, the country code may be &lt;code&gt;JP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we only compare the country code, that attack may not be detected.&lt;/p&gt;

&lt;p&gt;To improve detection, we can also compare the ASN.&lt;/p&gt;

&lt;p&gt;CloudFront provides the &lt;code&gt;CloudFront-Viewer-ASN&lt;/code&gt; header, so the Origin Request Lambda can use it in the same way as the country header.&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;module&lt;/span&gt; &lt;span class="s2"&gt;"impossible_location_guard"&lt;/span&gt; &lt;span class="p"&gt;{&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;"your-org/impossible-location-guard/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;check_asn&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;In my sample design, &lt;code&gt;check_asn&lt;/code&gt; is disabled by default.&lt;/p&gt;

&lt;p&gt;This is because ASN changes can happen during legitimate usage. For example, a user may switch between mobile data and Wi-Fi, which can result in a different ASN.&lt;/p&gt;

&lt;p&gt;So ASN checking is a trade-off.&lt;/p&gt;

&lt;p&gt;For some applications, country-level detection may be enough. For other applications, especially those with higher security requirements, ASN checking may be worth enabling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reauthentication Instead of Hard Blocking
&lt;/h2&gt;

&lt;p&gt;My recommendation is to redirect users to reauthentication instead of hard-blocking them.&lt;/p&gt;

&lt;p&gt;If a legitimate user is mistakenly blocked, they may feel that the service is broken or unreliable.&lt;/p&gt;

&lt;p&gt;On the other hand, if they are asked to log in again, they may simply think that their session expired.&lt;/p&gt;

&lt;p&gt;This is often a more acceptable user experience.&lt;/p&gt;

&lt;p&gt;When a suspicious location change is detected, the behavior is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redirect the user to &lt;code&gt;redirect_path&lt;/code&gt;, such as &lt;code&gt;/login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The user logs in again&lt;/li&gt;
&lt;li&gt;The location cookie is overwritten with the current country and ASN&lt;/li&gt;
&lt;li&gt;Access is restored&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means that legitimate users who travel abroad or use a VPN only need to log in again.&lt;/p&gt;

&lt;p&gt;An attacker who only has the stolen session cookie cannot continue unless they also have valid authentication credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Terraform Configuration
&lt;/h2&gt;

&lt;p&gt;The application does not need to be modified.&lt;/p&gt;

&lt;p&gt;You only need to add the module and associate the Lambda@Edge functions with the appropriate CloudFront cache behaviors.&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="c1"&gt;# -----------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# impossible-location-guard module&lt;/span&gt;
&lt;span class="c1"&gt;# -----------------------------------------------&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"impossible_location_guard"&lt;/span&gt; &lt;span class="p"&gt;{&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;"your-org/impossible-location-guard/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;

  &lt;span class="nx"&gt;providers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;us_east_1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;us_east_1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;project_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myapp"&lt;/span&gt;
  &lt;span class="nx"&gt;login_paths&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/api/auth/login"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;redirect_path&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;public_paths&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;check_asn&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;cookie_domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;".example.com"&lt;/span&gt;

  &lt;span class="nx"&gt;create_cloudfront_distribution&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda@Edge must be deployed in &lt;code&gt;us-east-1&lt;/code&gt;, so the module requires a provider for that region.&lt;/p&gt;

&lt;p&gt;Here is an example of adding the functions to an existing CloudFront distribution.&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;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ... existing origin configuration ...&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;"s3"&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="s2"&gt;"4135ea2d-6df8-44a3-9df3-4b5a84be39ad"&lt;/span&gt; &lt;span class="c1"&gt;# CachingDisabled&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="s2"&gt;"b689b0a8-53d0-40ab-baf2-68738e2966ac"&lt;/span&gt; &lt;span class="c1"&gt;# AllViewerExceptHostHeader&lt;/span&gt;

    &lt;span class="nx"&gt;lambda_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;"origin-request"&lt;/span&gt;
      &lt;span class="nx"&gt;lambda_arn&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impossible_location_guard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin_request_lambda_arn&lt;/span&gt;
      &lt;span class="nx"&gt;include_body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&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;"/assets/*"&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;"s3"&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="s2"&gt;"658327ea-f89d-4fab-a63d-7e88639e58f6"&lt;/span&gt; &lt;span class="c1"&gt;# CachingOptimized&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&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;"/api/auth/login"&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;"api"&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;"DELETE"&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;"PATCH"&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;"PUT"&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="s2"&gt;"4135ea2d-6df8-44a3-9df3-4b5a84be39ad"&lt;/span&gt; &lt;span class="c1"&gt;# CachingDisabled&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="s2"&gt;"b689b0a8-53d0-40ab-baf2-68738e2966ac"&lt;/span&gt; &lt;span class="c1"&gt;# AllViewerExceptHostHeader&lt;/span&gt;

    &lt;span class="nx"&gt;lambda_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;"origin-response"&lt;/span&gt;
      &lt;span class="nx"&gt;lambda_arn&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impossible_location_guard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin_response_lambda_arn&lt;/span&gt;
      &lt;span class="nx"&gt;include_body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Note:&lt;/span&gt;
  &lt;span class="c1"&gt;# If you have other API paths such as /api/users,&lt;/span&gt;
  &lt;span class="c1"&gt;# add a separate ordered_cache_behavior for /api/*.&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# That behavior should use the API origin,&lt;/span&gt;
  &lt;span class="c1"&gt;# allow the required HTTP methods,&lt;/span&gt;
  &lt;span class="c1"&gt;# and use the origin-request Lambda for protection.&lt;/span&gt;

  &lt;span class="c1"&gt;# ... restrictions, viewer_certificate ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When the Authentication System Uses a Different Domain
&lt;/h2&gt;

&lt;p&gt;One case that requires additional consideration is when the authentication system and application use different domains.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth.example.com
api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this mechanism stores location information in a cookie, the cookie must be shared between subdomains.&lt;/p&gt;

&lt;p&gt;In that case, you can set:&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;cookie_domain&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;".example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the CloudFront distributions are also separate, the Lambda@Edge functions must be associated with the appropriate distributions.&lt;/p&gt;

&lt;p&gt;For 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="c1"&gt;# Distribution for auth.example.com&lt;/span&gt;
&lt;span class="c1"&gt;# Set the cookie in the login response&lt;/span&gt;
&lt;span class="nx"&gt;lambda_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;"origin-response"&lt;/span&gt;
  &lt;span class="nx"&gt;lambda_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ilg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin_response_lambda_arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Distribution for api.example.com&lt;/span&gt;
&lt;span class="c1"&gt;# Verify the location cookie on protected requests&lt;/span&gt;
&lt;span class="nx"&gt;lambda_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;"origin-request"&lt;/span&gt;
  &lt;span class="nx"&gt;lambda_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ilg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin_request_lambda_arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because both Lambda functions are generated from the same module, they can share the same HMAC secret.&lt;/p&gt;

&lt;p&gt;This allows the cookie set on the authentication side to be verified on the application side.&lt;/p&gt;

&lt;p&gt;However, in real environments, the authentication system and the application may belong to different AWS accounts or different teams. In that case, coordination is required.&lt;/p&gt;

&lt;p&gt;Also, each application that relies on this protection must be configured properly. Otherwise, some paths or applications may remain unprotected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I introduced one possible way to implement Impossible Location detection using Amazon CloudFront and Lambda@Edge without modifying the application.&lt;/p&gt;

&lt;p&gt;The key points are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront can provide viewer location information through headers such as &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; is not available in Viewer Request Lambda@Edge&lt;/li&gt;
&lt;li&gt;The location check must run in Origin Request Lambda@Edge&lt;/li&gt;
&lt;li&gt;Protected paths must use &lt;code&gt;CachingDisabled&lt;/code&gt;, otherwise cache hits can bypass the Origin Request Lambda&lt;/li&gt;
&lt;li&gt;Signing the location cookie with HMAC is important&lt;/li&gt;
&lt;li&gt;The signed payload should be bound to the session ID&lt;/li&gt;
&lt;li&gt;ASN checking can help detect suspicious access from the same country&lt;/li&gt;
&lt;li&gt;Reauthentication is usually more user-friendly than hard blocking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The term “Impossible Location” may be familiar, but concrete implementation examples are not always easy to find.&lt;/p&gt;

&lt;p&gt;That is why I built this as one possible approach.&lt;/p&gt;

&lt;p&gt;I hope this article helps someone design a better implementation or think more deeply about session hijacking detection at the edge.&lt;/p&gt;

&lt;p&gt;In the next article, I will cover another version using Amazon CloudFront Functions + CloudFront KeyValueStore.&lt;/p&gt;

&lt;p&gt;That approach requires a small application-side change during login, but it can run at Viewer Request, avoid &lt;code&gt;CachingDisabled&lt;/code&gt;, and provide better cost and latency characteristics.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
    </item>
    <item>
      <title>What did our VPC look like at 22:14 (Tue, 2026-05-20) — building clew, a CLI for navigating AWS Config snapshots</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Thu, 21 May 2026 21:50:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-did-our-vpc-look-like-at-2214-tue-2026-05-20-building-clew-a-cli-for-navigating-aws-bcc</link>
      <guid>https://dev.to/aws-builders/what-did-our-vpc-look-like-at-2214-tue-2026-05-20-building-clew-a-cli-for-navigating-aws-bcc</guid>
      <description>&lt;p&gt;It's 2am. Production is on fire. Someone in the war room asks, "what did the VPC actually look like at 22:14, when this started?", and the only honest answer is, "give me twenty minutes with jq."&lt;/p&gt;

&lt;p&gt;That moment, repeated enough times, is why I started writing the tool I want to talk about today: clew, a Go CLI that ingests AWS Config Configuration Snapshots into a local DuckDB and renders an interactive topology you can actually read.&lt;/p&gt;

&lt;p&gt;▎ &lt;a href="https://github.com/nishikawaakira/clew" rel="noopener noreferrer"&gt;https://github.com/nishikawaakira/clew&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is the story of why it exists and how to use it during an incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: "the moment-in-time configuration" is a real artifact you can't easily see
&lt;/h2&gt;

&lt;p&gt;A scenario you might recognize:&lt;/p&gt;

&lt;p&gt;It's 22:14. Targets behind an ALB start going UnHealthy, but only some of them. CloudWatch shows the symptom but not the cause. People in the war room start floating theories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Maybe someone tightened a security group rule."&lt;/li&gt;
&lt;li&gt;"Maybe a route table was modified and Private Subnet traffic is now going out the wrong gateway."&lt;/li&gt;
&lt;li&gt;"Maybe a subnet's route table association silently changed."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All plausible. But each of them needs the same piece of evidence: what the topology actually looked like at 22:14, ideally next to what it looked like at 18:00 when things were still fine. The AWS Console only shows you "right now," which is precisely the moment you don't care about anymore.&lt;/p&gt;

&lt;p&gt;The good news: AWS Config has been quietly dropping a full Configuration Snapshot of every tracked resource into S3 every six hours. The artifact exists. The bad news is that the artifact ships as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;account&amp;gt;_Config_&amp;lt;region&amp;gt;_ConfigSnapshot_&amp;lt;timestamp&amp;gt;_&amp;lt;snapshot-id&amp;gt;.json.gz&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Containing a flat &lt;code&gt;configurationItems&lt;/code&gt; array, mixed across resource types&lt;/li&gt;
&lt;li&gt;Where "this EC2 uses this security group" lives under &lt;code&gt;configuration.networkInterfaces[].groups[].groupId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;And "this VPC contains these subnets, which contain these instances" is purely implicit — you reconstruct it in your head from the &lt;code&gt;relationships[]&lt;/code&gt; field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;jq at 2am is a poor incident-response interface. And drawing the VPC topology on paper while pagers are going off is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing tooling, and where it fell short for me
&lt;/h2&gt;

&lt;p&gt;I'm not pretending nothing else exists. The usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Config Aggregator + Advanced Queries (SQL-ish querying from the console)&lt;/li&gt;
&lt;li&gt;CloudFormation StackSets with drift detection&lt;/li&gt;
&lt;li&gt;Commercial CMDB / IaC visualizers (Lucidchart's AWS importer, Hava, etc.)&lt;/li&gt;
&lt;li&gt;Rolling your own jq library and committing it to the team's snippet repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I couldn't find was something that hit all of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Works against an arbitrary historical snapshot (any S3 object, not just "now")&lt;/li&gt;
&lt;li&gt;Local-first, no SaaS round trip — incident artifacts shouldn't leave the responder's laptop&lt;/li&gt;
&lt;li&gt;Produces a single self-contained HTML file I can paste into a Slack thread or a postmortem doc&lt;/li&gt;
&lt;li&gt;Lets me stack multiple snapshots in the same store and compare across time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I started writing what I wished existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What clew does
&lt;/h2&gt;

&lt;p&gt;Three commands, one data model:&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%2Fibwg7gzaud1xvyhgdlps.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%2Fibwg7gzaud1xvyhgdlps.png" alt="clew data flow: AWS Config snapshot to DuckDB to render/query outputs" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The DuckDB file holds three tables:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;config_items&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores one row per imported &lt;code&gt;configurationItem&lt;/code&gt;, including the original configuration, relationships, tags, and &lt;code&gt;capture_time&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;graph_nodes&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores one row per resource, including placeholder rows for resources referenced by relationships but not yet imported.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;graph_edges&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores resource-to-resource edges extracted from both &lt;code&gt;relationships[]&lt;/code&gt; and the configuration body.&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%2Fniqt6qdz7nybx04enooq.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%2Fniqt6qdz7nybx04enooq.png" alt="clew VPC topology view showing VPC, subnet, EC2, and ENI nodes" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;render --format html&lt;/code&gt; output is the part most worth showing off. It produces an interactive topology with compound nodes — VPCs are drawn as outer boxes containing subnet boxes containing EC2 / ENI nodes — the same nesting you'd draw on a whiteboard during incident response, except generated from the snapshot you just imported. &lt;/p&gt;

&lt;p&gt;The generated HTML is interactive: you can pan and zoom, click nodes to inspect their type, resource ID, ARN, and placeholder status, toggle edge labels, switch layout direction, and choose between orthogonal and bezier edges.&lt;/p&gt;

&lt;p&gt;The file is fully self-contained — copy it anywhere, open it in any browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walkthrough: using clew in incident-response mode
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 0: Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requires Go 1.24+ and a C compiler — go-duckdb is CGO-bound.&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/nishikawaakira/clew@v0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This assumes AWS Config is already enabled and delivering configuration snapshots to S3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Pull the snapshot(s) closest to the incident from S3
&lt;/h3&gt;

&lt;p&gt;AWS Config writes keys with unpadded month/day (.../2026/5/20/...), so dancing around BSD-vs-GNU date is a waste of energy. Listing recursively and filtering by name is the safer path:&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;ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; Account &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;incident-region&amp;gt;
&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-config-bucket&amp;gt;

aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/AWSLogs/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Config/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;ConfigSnapshot | &lt;span class="nb"&gt;sort&lt;/span&gt;

&lt;span class="c"&gt;# Grab the two snapshots straddling the incident window and `aws s3 cp` them down.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need one right now and don't want to wait for the next scheduled delivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configservice deliver-config-snapshot &lt;span class="nt"&gt;--delivery-channel-name&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my experience, the S3 object usually shows up within 10–60 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Import everything into a single DuckDB
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clew import &lt;span class="nt"&gt;--input&lt;/span&gt; snapshot-2026-05-20T22-00.json.gz &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb
clew import &lt;span class="nt"&gt;--input&lt;/span&gt; snapshot-2026-05-20T18-00.json.gz &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can keep stacking snapshots into the same incident.duckdb. &lt;code&gt;config_items.item_id&lt;/code&gt; is a PRIMARY KEY of &lt;code&gt;account:region:type:resource_id:captureTime&lt;/code&gt;, combined with &lt;code&gt;ON CONFLICT (item_id) DO NOTHING&lt;/code&gt; on insert.&lt;/p&gt;

&lt;p&gt;The practical guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Re-importing the same snapshot is a true no-op (the inserts collide on item_id).&lt;/li&gt;
&lt;li&gt;Re-importing a snapshot taken at a different capture time keeps both rows for the same resource — history is preserved, not overwritten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;graph_nodes&lt;/code&gt; / &lt;code&gt;graph_edges&lt;/code&gt; always reflect the latest state, so the rendered topology is always "the most recent snapshot you imported," but the historical detail still lives in &lt;code&gt;config_items&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Render the topology
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clew render &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb &lt;span class="nt"&gt;--view&lt;/span&gt; vpc &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;--output&lt;/span&gt; vpc.html
open vpc.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want it for a Slack thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clew render &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb &lt;span class="nt"&gt;--view&lt;/span&gt; vpc &lt;span class="nt"&gt;--format&lt;/span&gt; png &lt;span class="nt"&gt;--output&lt;/span&gt; vpc.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raster output is fine for a quick share, but it doesn't scale well to large topologies — once you have more than a couple of dozen resources, the nodes get cramped and labels become hard to read. For anything larger, the interactive HTML is the better artifact to share with the team, because reviewers can open it locally and pan/zoom.&lt;/p&gt;

&lt;p&gt;If you want to embed it in a postmortem Markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clew render &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb &lt;span class="nt"&gt;--view&lt;/span&gt; vpc &lt;span class="nt"&gt;--format&lt;/span&gt; mermaid &lt;span class="nt"&gt;--output&lt;/span&gt; vpc.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Zoom in on a suspect
&lt;/h3&gt;

&lt;p&gt;When you have a specific theory — "the ALB stopped reaching this instance" — the global view is too much. &lt;code&gt;query&lt;/code&gt; runs a bidirectional BFS (breadth-first search) from one resource:&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="c"&gt;# Two hops out from a specific instance, as interactive HTML.&lt;/span&gt;
clew query &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb &lt;span class="nt"&gt;--resource-id&lt;/span&gt; i-0abc... &lt;span class="nt"&gt;--depth&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;--output&lt;/span&gt; i-0abc.html

&lt;span class="c"&gt;# Or a text summary in the terminal when you just want to confirm a hunch.&lt;/span&gt;
clew query &lt;span class="nt"&gt;--db&lt;/span&gt; incident.duckdb &lt;span class="nt"&gt;--resource-id&lt;/span&gt; sg-12345 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--format&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text format is intentionally boring and grep-friendly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nodes:
  - AWS::EC2::Instance/i-0abc...
  - AWS::EC2::SecurityGroup/sg-12345
  - AWS::EC2::Subnet/subnet-pub
  ...
Edges:
  - &amp;lt;node-id&amp;gt; ==[Uses security group]==&amp;gt; &amp;lt;node-id&amp;gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Diff across time, with plain SQL
&lt;/h3&gt;

&lt;p&gt;Because every snapshot lands as its own row in config_items, you can ask DuckDB directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb incident.duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;capture_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configuration_json&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;config_items&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;resource_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sg-12345'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;capture_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing the 22:00 vs. 22:14 configuration_json of a single security group is now a diff between two rows. The whole point of putting the data in a real database was to make the ad-hoc questions stop requiring custom code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What clew doesn't do yet
&lt;/h2&gt;

&lt;p&gt;Being honest about scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only the vpc view is implemented. The internal model is type-agnostic, so adding iam, org, kms, or network (peering/TGW) views is a matter of writing the type list and the configuration-edge extractors, not a redesign.&lt;/li&gt;
&lt;li&gt;The HTML output pulls Cytoscape.js / dagre / cytoscape-dagre from a CDN at view time. Useful in 95% of cases, useless on a fully air-gapped laptop. An --embed-js mode using go:embed is the obvious follow-up.&lt;/li&gt;
&lt;li&gt;No special tuning for very large snapshots. Inserts are straightforward prepared statements. For tens of thousands of resources you'd want the DuckDB Appender API or parallel ingest.&lt;/li&gt;
&lt;li&gt;Cross-account / cross-region edges only exist if AWS Config explicitly listed them. Reconstructing organization-wide topology needs more wiring (likely an Organizations + Config Aggregator integration).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why "clew"?
&lt;/h2&gt;

&lt;p&gt;clew is the Old English word for "ball of thread" — and the literal etymological root of the modern word clue. In Greek mythology it's the thread Ariadne gave Theseus so he could find his way back out of the Labyrinth.&lt;/p&gt;

&lt;p&gt;Production AWS environments tend to drift toward labyrinth shape, especially after years of "just one more security group / one more subnet / one more peering connection." Most of the time it's fine. Then 2am happens, and the same environment that felt familiar yesterday looks like a maze.&lt;/p&gt;

&lt;p&gt;clew is named for the thread you wish you had on the way in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/nishikawaakira/clew@v0.1.0
clew &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repository: &lt;a href="https://github.com/nishikawaakira/clew" rel="noopener noreferrer"&gt;https://github.com/nishikawaakira/clew&lt;/a&gt; (MIT)&lt;/p&gt;

&lt;p&gt;If you want to see the output without setting up real AWS Config, the repo ships a testdata/sample_snapshot.json that exercises every piece of the rendering pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clew import &lt;span class="nt"&gt;--input&lt;/span&gt; testdata/sample_snapshot.json &lt;span class="nt"&gt;--db&lt;/span&gt; /tmp/demo.duckdb
clew render &lt;span class="nt"&gt;--db&lt;/span&gt; /tmp/demo.duckdb &lt;span class="nt"&gt;--view&lt;/span&gt; vpc &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;--output&lt;/span&gt; /tmp/demo.html
open /tmp/demo.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still a PoC — there are sharp edges and obvious gaps. Issues, PRs and "here's the view I actually need" feature requests are all welcome.&lt;/p&gt;

&lt;p&gt;Thanks for reading. If this means one fewer engineer is writing jq queries at 2am next month, the project has paid for itself.&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
    <item>
      <title>Automating Phishing Site Reporting and Takedowns with AWS Lambda + Amazon Bedrock</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Fri, 03 Apr 2026 00:51:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/automating-phishing-site-reporting-and-takedowns-with-aws-lambda-bedrock-4ie8</link>
      <guid>https://dev.to/aws-builders/automating-phishing-site-reporting-and-takedowns-with-aws-lambda-bedrock-4ie8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;"Your account has been suspended." "Please update your payment information." — Phishing SMS and emails like these have become a daily occurrence in Japan.&lt;/p&gt;

&lt;p&gt;As a security engineer, I used to manually report every phishing site I came across, but the workload per report was simply too much to keep up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look up the domain's IP address&lt;/li&gt;
&lt;li&gt;Identify the hosting provider via WHOIS&lt;/li&gt;
&lt;li&gt;Find the abuse contact email address&lt;/li&gt;
&lt;li&gt;Write a report email in English&lt;/li&gt;
&lt;li&gt;Take a screenshot and attach it&lt;/li&gt;
&lt;li&gt;Send it, then check whether the site was taken down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;15 to 30 minutes per report.&lt;/strong&gt; Doing this for multiple sites every day just isn't realistic.&lt;/p&gt;

&lt;p&gt;So I built a system on AWS that automates all of the above with a single URL submission. In this article, I'll share the architecture and the lessons learned during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://xxx.execute-api.us-east-1.amazonaws.com/prod/report-auto &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://phishing-site.example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single curl command triggers the following automated workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capturing a screenshot of the site&lt;/li&gt;
&lt;li&gt;Using Bedrock (Claude) to analyze the page and infer the impersonated brand&lt;/li&gt;
&lt;li&gt;Looking up technical information such as hosting provider and abuse contact&lt;/li&gt;
&lt;li&gt;Drafting and sending a report email with supporting evidence&lt;/li&gt;
&lt;li&gt;Submitting reports to services such as Google Safe Browsing&lt;/li&gt;
&lt;li&gt;Monitoring DNS changes afterward to help confirm whether the site has been taken down&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To reduce false reports, the system stores reporting history, keeps a registry of known legitimate sites, and relies on multiple pieces of evidence before proceeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Diagram
&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%2Fk8oqsdmwcvevnmu0lmqi.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%2Fk8oqsdmwcvevnmu0lmqi.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Services Used
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; (HTTP API)&lt;/td&gt;
&lt;td&gt;REST endpoints (4 routes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; (Zip x3)&lt;/td&gt;
&lt;td&gt;Report processing, reply handling, takedown monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; (Container x1)&lt;/td&gt;
&lt;td&gt;Playwright screenshots + form auto-submission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DynamoDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Abuse contact cache, report history, legitimate site registry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screenshot storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container image for Screenshot Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Bedrock&lt;/strong&gt; (Claude)&lt;/td&gt;
&lt;td&gt;Email generation, screenshot analysis, brand detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EventBridge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Daily takedown monitoring schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Route 53&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email domain DNS (SPF/DKIM/DMARC)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;All infrastructure is managed with Terraform&lt;/strong&gt; — a single &lt;code&gt;terraform apply&lt;/code&gt; deploys everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting Technical Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Collecting Usable Screenshots from Phishing Sites
&lt;/h3&gt;

&lt;p&gt;Screenshots are important evidence in abuse reports. In practice, however, phishing sites often make automated access difficult, and a straightforward screenshot attempt may fail.&lt;/p&gt;

&lt;p&gt;To improve reliability, the system uses multiple approaches to collect screenshots, including third-party services such as urlscan.io when appropriate.&lt;/p&gt;

&lt;p&gt;In addition to helping with screenshot collection, third-party scan results can also serve as supporting evidence when reporting a site.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Three Uses of Bedrock (Claude)
&lt;/h3&gt;

&lt;p&gt;This system leverages Claude in three different contexts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a) Screenshot Quality Assessment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Determines whether the captured screenshot shows actual phishing content, rather than a bot detection page, 404 error, or blank page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;b) Automatic Brand Detection (&lt;code&gt;/report-auto&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The URL-only endpoint uses screenshots and HTML to determine which brand the phishing site is impersonating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;c) Report Email Generation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Based on technical information such as redirect chains, page HTML, and screenshots, Claude automatically generates professional emails for hosting provider abuse teams. It captures technical details that templates can't convey.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A Multi-Layered Reporting Strategy
&lt;/h3&gt;

&lt;p&gt;Reporting a phishing site to only one destination is often not enough.&lt;/p&gt;

&lt;p&gt;That is why the system can notify multiple relevant parties for a single phishing site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hosting provider abuse contacts&lt;/li&gt;
&lt;li&gt;Domain registrar abuse contacts&lt;/li&gt;
&lt;li&gt;Google Safe Browsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These channels serve different purposes.&lt;/p&gt;

&lt;p&gt;For example, hosting providers may take action against the server itself, while registrars may suspend the domain. Browser warning services can also help protect users by displaying alerts even if the site remains online for some time.&lt;/p&gt;

&lt;p&gt;This layered approach improves the chance of reducing harm, even when one reporting path does not immediately lead to action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Because the system is built on serverless services, the monthly cost has been relatively low in my current usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: Pay-per-use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt;: On-demand capacity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt;: Screenshot storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock (Claude)&lt;/strong&gt;: the main variable cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt;: Email sending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;urlscan.io&lt;/strong&gt;: public scans within the free tier &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my current setup and usage volume, the total cost has been roughly $5–10 per month, though this can vary depending on report volume and model usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Plans
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A reporting analytics dashboard to visualize response times by hosting provider and impersonated brand trends&lt;/li&gt;
&lt;li&gt;Expanding support for additional browser warning services such as Microsoft SmartScreen&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Reporting phishing sites is repetitive and time-consuming work, especially when handled one case at a time.&lt;/p&gt;

&lt;p&gt;By combining AWS serverless services with Bedrock (Claude), I built a system that streamlines much of that process — from screenshot collection to reporting and follow-up monitoring — from a single URL submission.&lt;/p&gt;

&lt;p&gt;There is still plenty of room to improve how we respond to phishing at scale, but even small automation can make this work more sustainable. I hope this article provides a useful reference for others working on similar problems.&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%2Fe0pl469x7ujsh31rpim4.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%2Fe0pl469x7ujsh31rpim4.png" alt=" " width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>aws</category>
    </item>
    <item>
      <title>AWS Security Hub notifications made actionable using Amazon Bedrock</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Sat, 28 Dec 2024 05:23:43 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-security-hub-notifications-made-actionable-using-amazon-bedrock-1f3i</link>
      <guid>https://dev.to/aws-builders/aws-security-hub-notifications-made-actionable-using-amazon-bedrock-1f3i</guid>
      <description>&lt;p&gt;There were many Generative AI sessions at AWS re:Invent. Although I didn’t attend most of them, participating in a Generative AI GameDay made me realize that Generative AI isn’t as difficult as I thought. This inspired me to work on making Security Hub notifications more actionable, so that any engineer can handle them. This is the background behind writing this blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Read This?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Engineers who don’t have dedicated security engineers and feel unsure about what to do with Security Hub notifications.&lt;/li&gt;
&lt;li&gt;Security engineers who send out Security Hub notifications but struggle to get others to take action.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The architecture is very simple: &lt;br&gt;
Security Hub detection -&amp;gt; EventBridge -&amp;gt; Lambda (invoking Amazon Bedrock). &lt;br&gt;
While the setup is straightforward, there are some potential pitfalls, and I’d like to share those considerations with you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step-by-Step Guide (Amazon Bedrock)
&lt;/h2&gt;

&lt;p&gt;First and foremost, you need to prepare to use Bedrock. Let’s start by submitting a request.&lt;/p&gt;

&lt;p&gt;You can apply for access by navigating to the Bedrock page and going to the Model access section.&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%2F6rdcx2odi4omo5lfhw9q.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%2F6rdcx2odi4omo5lfhw9q.png" alt=" " width="590" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used Claude 3.5 Sonnet. In my environment, access was already granted, but if you need to request access, click on "Available to request." &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%2Fopr6vs3joei8bhdwm97a.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%2Fopr6vs3joei8bhdwm97a.png" alt=" " width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking, a popup will appear. Select "Request model access" and proceed. You’ll then see a screen where you can select the models you want to use. Choose the desired model and click Next.&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%2Fna4d8aihr2uvtj72isyf.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%2Fna4d8aihr2uvtj72isyf.png" alt=" " width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this is your first request, you’ll need to provide information such as your company name and the intended use case during the application process. Fill in the required details and submit your request. In my case, access was granted in about 10 minutes. With that, the Bedrock setup is complete.&lt;/p&gt;

&lt;p&gt;Even while waiting for approval, you can proceed with the following setup steps, so let’s keep going.&lt;/p&gt;

&lt;p&gt;Note: In this blog, I will also introduce the Lambda code, assuming the use of Claude 3.5 Sonnet. Please be aware that the interface might vary slightly depending on the model. If you’re using a different model, you may need to make adjustments accordingly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step-by-Step Guide (AWS Lambda)
&lt;/h2&gt;

&lt;p&gt;Let’s start by writing the code. The example is in Python. Make sure to adjust settings such as the region and the Slack Webhook URL as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json
import boto3
import requests

bedrock_client = boto3.client(service_name='bedrock-runtime', region_name="ap-northeast-1")

SLACK_WEBHOOK_URL = ""

def lambda_handler(event, context):
    try:

        # Retrieve Security Hub findings
        findings = event.get('detail', {}).get('findings', [])
        if not findings:
            print("No findings received.")
            return

        # Summarize Findings (Bedrock API call)
        summary = summarize_findings_with_bedrock(findings)

        send_to_slack(summary)

        return {
            'statusCode': 200,
            'body': json.dumps('Notification sent successfully!')
        }

    except Exception as e:
        print(f"Error processing findings: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps(f"Error: {str(e)}")
        }

def summarize_findings_with_bedrock(findings):
    # Findings are summarized and converted to text
    findings_text = "\n".join([
        f"Id: {finding.get('Id')}\nDescription: {finding.get('Description')}"
        for finding in findings
    ])

    prompt = f"""
    {findings_text}

    Please provide clear instructions and example terraform code on why and how to respond so that everyone can respond when they receive the above Security Hub findings notification.
    """ 

    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 2048,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.7
    })

    response = bedrock_client.invoke_model(
        body=body,
        modelId='anthropic.claude-3-5-sonnet-20240620-v1:0',
        contentType="application/json",
        accept="application/json"
    )

    response_body = json.loads(response['body'].read())
    return findings_text + response_body['content'][0]['text']    

def send_to_slack(message):
    slack_message = {
        "text": f"Security Hub Findings Summary:\n{message}"
    }

    response = requests.post(
        SLACK_WEBHOOK_URL,
        headers={"Content-Type": "application/json"},
        data=json.dumps(slack_message)
    )

    if response.status_code != 200:
        raise ValueError(f"Failed to send message to Slack: {response.status_code}, {response.text}")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we provide Terraform code as a sample. However, feel free to modify it to suit your organization's needs. If it hasn't been integrated into your IaC process, remove any unnecessary prompts accordingly.&lt;/p&gt;

&lt;p&gt;The anthropic_version is included in the body of the request, but this is only required when using Anthropic models. It might not be necessary for some Anthropic models, though this hasn’t been thoroughly investigated. Additionally, the structure of messages and the content passed as a prompt can vary depending on the model, which can be a potential stumbling block. If you encounter issues when using a different model, it’s a good idea to check these aspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings and adding the layer
&lt;/h2&gt;

&lt;p&gt;I set the timeout to around 1 minute. If it's set to something like 10 seconds, there’s a chance the response won’t return. At the very least, the default 3 seconds for Lambda is definitely not sufficient, so it needs to be increased.&lt;/p&gt;

&lt;p&gt;Add an inline policy to allow Lambda to invoke Bedrock. Click on the role name displayed below to configure the role.&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%2F4ip7ww99s5ce5k2k9bhh.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%2F4ip7ww99s5ce5k2k9bhh.png" alt=" " width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the Add permissions section, click Create inline policy.&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%2Fpy98j91fqzp8crlsz9jz.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%2Fpy98j91fqzp8crlsz9jz.png" alt=" " width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select JSON and enter the content below. For Bedrock actions, only InvokeModel is needed.&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%2Faz2hjaz9l3ye5w7wf8bd.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%2Faz2hjaz9l3ye5w7wf8bd.png" alt=" " width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For reference, I’ll also provide the text version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "bedrock:InvokeModel",
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add a Layer to the Lambda function.&lt;/p&gt;

&lt;p&gt;Since we’re using requests and boto3, we’ll create it quickly. Follow the steps below to create a ZIP file containing the required Python libraries. Note that the latest version of boto3 is required, so make sure to follow this procedure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir libraries
pip3 install requests -t libraries/
pip3 install boto3 -t libraries/
pip3 install botocore -t libraries/
zip -r layer.zip libraries/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the above steps are complete, go to Layers and click Create layer.&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%2F8n1lvz8xququ58v2kb24.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%2F8n1lvz8xququ58v2kb24.png" alt=" " width="799" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter an appropriate name and simply upload the ZIP file you created earlier.&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%2Ffunuvu9zq6jkqpgf20x5.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%2Ffunuvu9zq6jkqpgf20x5.png" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the layer is created, go to its details page and copy the ARN.&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%2Fywoid3t4zl4m3yz0nnmk.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%2Fywoid3t4zl4m3yz0nnmk.png" alt=" " width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Return to the Lambda function you created earlier and click Add a layer.&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%2Fdontqpynk6jog50z5oi5.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%2Fdontqpynk6jog50z5oi5.png" alt=" " width="800" height="48"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select Specify an ARN, paste the ARN you copied earlier, and click the Add button to complete the process.&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%2Fvgo065szl89g5okvugfj.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%2Fvgo065szl89g5okvugfj.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Guide (Amazon EventBridge)
&lt;/h2&gt;

&lt;p&gt;Finally, navigate to the EventBridge page, go to Buses, click on Rules, and create a rule by selecting Create rule.&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%2Fx8yr0zifo4kmspdf7s8s.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%2Fx8yr0zifo4kmspdf7s8s.png" alt=" " width="546" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter an appropriate name for the rule and select any desired Event bus. Then, choose Rule with an event pattern and click Next.&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%2F2ouuodp17jl7x5vt7pzv.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%2F2ouuodp17jl7x5vt7pzv.png" alt=" " width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep most of the default settings, focusing only on selecting the Event pattern. For AWS service, select Security Hub, and for Event type, choose Security Hub Findings - Imported.&lt;/p&gt;

&lt;p&gt;Next, configure the notification targets. To avoid excessive notifications, limit the findings to HIGH and CRITICAL severity levels. Additionally, to target only newly created findings, select NEW for the workflow status.&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%2Fwquj6tdiunf7nosnukk3.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%2Fwquj6tdiunf7nosnukk3.png" alt=" " width="799" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Proceed to the next page and specify the Lambda function you created earlier.&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%2Fpjt74zu50b5wujx32e1l.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%2Fpjt74zu50b5wujx32e1l.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If no additional tags or settings are needed, go ahead and click Create rule. That’s it — the setup is complete!&lt;/p&gt;

&lt;h2&gt;
  
  
  Let Security Hub detect it
&lt;/h2&gt;

&lt;p&gt;The easiest way to test is by creating an open Security Group. It's extremely useful for validation!&lt;/p&gt;

&lt;p&gt;Enter a suitable name and description, select a VPC, and add the following settings to the Inbound rules. Click the Create security group button, and then just wait for it to be detected.&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%2Fmx77b5dr2r2m2ca5zlie.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%2Fmx77b5dr2r2m2ca5zlie.png" alt=" " width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One final note: if the detection frequency in your AWS Config settings is set to Daily, detection may not happen immediately. In that case, you’ll just need to wait. There likely isn’t any workaround other than temporarily changing the frequency. If the setting is configured for continuous detection, it was detected within around 5 minutes in my experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual content of the notification
&lt;/h2&gt;

&lt;p&gt;Security Hub Findings Summary:&lt;/p&gt;

&lt;p&gt;Id: arn:aws:securityhub:ap-northeast-1:637423304033:security-control/EC2.18/finding/9e0e7000-0e27-435f-9f59-e5cc716e561b&lt;/p&gt;

&lt;p&gt;Description: This control checks whether an Amazon EC2 security group permits unrestricted incoming traffic from unauthorized ports. The control status is determined as follows: If you use the default value for 'authorizedTcpPorts', the control fails if the security group permits unrestricted incoming traffic from any port other than ports 80 and 443; If you provide custom values for 'authorizedTcpPorts' or 'authorizedUdpPorts', the control fails if the security group permits unrestricted incoming traffic from any unlisted port; If no parameter is used, the control fails for any security group that has an unrestricted inbound traffic rule.Certainly! I'll provide an explanation of the finding and how to address it using Terraform code.&lt;/p&gt;

&lt;p&gt;Explanation:&lt;br&gt;
This Security Hub finding (EC2.18) is alerting you about potentially insecure configurations in your EC2 security groups. The control checks for unrestricted incoming traffic on unauthorized ports, which could expose your instances to unnecessary risks.&lt;/p&gt;

&lt;p&gt;Why it's important:&lt;br&gt;
Limiting inbound traffic to only necessary ports reduces the attack surface of your EC2 instances, enhancing overall security.&lt;/p&gt;

&lt;p&gt;How to respond:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Review your security group rules.&lt;/li&gt;
&lt;li&gt;Identify which ports are necessary for your application.&lt;/li&gt;
&lt;li&gt;Update your security group rules to allow traffic only on required ports.&lt;/li&gt;
&lt;li&gt;Use CIDR blocks to restrict incoming traffic to known IP ranges when possible.
Here's an example Terraform code to address this issue:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hcl
resource "aws_security_group" "example" {
  name        = "example-sg"
  description = "Example security group with restricted inbound rules"
  vpc_id      = aws_vpc.main.id

  # Allow incoming HTTPS traffic
  ingress {
    description = "HTTPS from anywhere"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow incoming HTTP traffic
  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow SSH access from a specific IP range
  ingress {
    description = "SSH from corporate network"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]  # Replace with your corporate network CIDR
  }

  # Allow outbound traffic to anywhere
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "example-sg"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Allows inbound HTTPS (443) and HTTP (80) traffic from anywhere.&lt;/li&gt;
&lt;li&gt;Restricts SSH (22) access to a specific IP range (replace with your actual range).&lt;/li&gt;
&lt;li&gt;Allows all outbound traffic.
To use this:&lt;/li&gt;
&lt;li&gt;Replace aws_vpc.main.id with your actual VPC ID or reference.&lt;/li&gt;
&lt;li&gt;Adjust the ingress rules based on your specific needs.&lt;/li&gt;
&lt;li&gt;Update the CIDR blocks to match your required IP ranges.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always use the principle of least privilege.&lt;/li&gt;
&lt;li&gt;Regularly review and update your security group rules.&lt;/li&gt;
&lt;li&gt;Consider using Security Group references instead of CIDR blocks for internal resources.
By implementing these changes, you should resolve the Security Hub finding and improve your EC2 instance security.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What did you think? By reviewing the notifications this way, it’s now much clearer why detections occur and what actions need to be taken. From the next time on, it seems likely that more attention will be paid to the configurations. While the notification content of Security Hub findings can be quite hard to understand as is, passing them through Generative AI makes them significantly easier to interpret.&lt;/p&gt;

&lt;p&gt;Additionally, if your infrastructure is managed as code, you could even pass the source code itself to Generative AI and ask directly what needs to be changed. That could be a great approach as well.&lt;/p&gt;

&lt;p&gt;The possibilities for improvements seem endless, don’t they? Thank you for reading to the end!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
    </item>
    <item>
      <title>Unlocking the Potential of Amazon CloudFront with VPC Origins: Overcoming WAF Bypass Challenges</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Sat, 28 Dec 2024 03:44:32 +0000</pubDate>
      <link>https://dev.to/aws-builders/unlocking-the-potential-of-amazon-cloudfront-with-vpc-origins-overcoming-waf-bypass-challenges-492f</link>
      <guid>https://dev.to/aws-builders/unlocking-the-potential-of-amazon-cloudfront-with-vpc-origins-overcoming-waf-bypass-challenges-492f</guid>
      <description>&lt;p&gt;Amazon CloudFront VPC Origins was announced on November 20, 2024. Have you had a chance to try it yet? Today, I’d like to explore why this feature is exciting from a security perspective and discuss the challenges it addresses when this feature is not in use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is Amazon CloudFront with VPC Origins exciting?
&lt;/h2&gt;

&lt;p&gt;One of the significant advantages of this feature is the elimination of direct access to ALB or EC2 instances. These resources can now reside in private subnets, making them inaccessible directly from external sources. Access is strictly limited to routes through CloudFront within the same AWS account. Moreover, when WAF is configured on CloudFront, it ensures that origin access cannot bypass the WAF, adding an extra layer of security.&lt;/p&gt;

&lt;p&gt;The structure is as shown in the following diagram.&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%2Fovzwgq3j0yxeeq6u5yff.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%2Fovzwgq3j0yxeeq6u5yff.png" alt=" " width="799" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it differ from previous setups?
&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%2Fb5nufgci4ipyp93mn4nn.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%2Fb5nufgci4ipyp93mn4nn.png" alt=" " width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the architecture shown above, direct access to the ALB was possible if no additional configurations were applied. Similarly, if EC2 instances were deployed in public subnets, they could also be accessed directly. This meant that even with WAF configured on CloudFront, it couldn’t function effectively in this scenario.&lt;/p&gt;

&lt;p&gt;To mitigate these risks and block direct access to origins, it was necessary to implement additional measures. These included configuring CloudFront's managed prefix list on the ALB or adding custom headers in CloudFront and validating them at the ALB, as demonstrated below:&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%2F60tdj2djusuv2b7rfih2.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%2F60tdj2djusuv2b7rfih2.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
*Example: Configuring the managed prefix list.&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%2Fc6ijz2fgvwr90purzjt0.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%2Fc6ijz2fgvwr90purzjt0.png" alt=" " width="800" height="176"&gt;&lt;/a&gt;&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%2Fd5sax0yaj7c7gtkljflv.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%2Fd5sax0yaj7c7gtkljflv.png" alt=" " width="799" height="529"&gt;&lt;/a&gt;&lt;br&gt;
*Setting and verifying custom headers&lt;/p&gt;

&lt;p&gt;While effective, these configurations could be surprisingly tedious. Ideally, CloudFront should have been the sole entry point from the start. The new VPC Origins feature eliminates the need to worry about inadvertent external access caused by misconfigurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations of ALB-Based Validation
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, preventing origin access previously relied on measures like CloudFront's managed prefix list or custom header validation on the ALB. However, these methods could still be bypassed.&lt;/p&gt;

&lt;p&gt;Relying solely on CloudFront's managed prefix list is particularly insufficient. If an attacker knows the domain of the ALB, they can reference it from a different AWS account using their own CloudFront distribution. In other words, cross-account access is still possible.&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%2Fbtg8rw4ny79yu80shmk6.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%2Fbtg8rw4ny79yu80shmk6.png" alt=" " width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In such cases, requests can still reach the ALB from CloudFront's IP addresses. The problem here is that while the legitimate CloudFront may have WAF configured, the attacker’s CloudFront would not. As a result, even the most robust WAF rules can be bypassed entirely.&lt;/p&gt;

&lt;p&gt;For this reason, it’s generally recommended to validate custom headers alongside using the managed prefix list. However, this issue is completely resolved with CloudFront VPC Origins—a game-changing improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can the ALB domain be exposed?
&lt;/h2&gt;

&lt;p&gt;There are scenarios where the ALB domain becomes discoverable. For instance, when enabling HTTPS access from CloudFront to the ALB, it’s common to assign a subdomain to the ALB. If the domain assigned to CloudFront is example.com, the ALB might use a subdomain like alb.example.com.&lt;/p&gt;

&lt;p&gt;In such cases, bypassing the legitimate CloudFront becomes relatively straightforward. Additionally, this information could potentially leak through former employees. Rotating credentials or domains to mitigate such risks can be both time-consuming and cumbersome. This is why VPC Origins, which can guard against such threats, is an incredibly valuable feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What do you think? Have you been relying solely on a managed prefix list? As this post highlights, there are potential pitfalls to that approach. Let’s take advantage of CloudFront VPC Origins to build a more secure and robust environment!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>waf</category>
    </item>
    <item>
      <title>How to Transition AWS WAF from COUNT Mode to BLOCK Mode</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Wed, 02 Oct 2024 19:37:26 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-transition-aws-waf-from-count-mode-to-block-mode-g0l</link>
      <guid>https://dev.to/aws-builders/how-to-transition-aws-waf-from-count-mode-to-block-mode-g0l</guid>
      <description>&lt;p&gt;In this article, I will share my experience after a colleague asked me, “I've been running WAF in COUNT mode for a while, but how do I determine which rules should be switched to BLOCK mode?”&lt;/p&gt;

&lt;p&gt;Although I initially thought it would be straightforward, I couldn't easily explain it, so I created a procedure. This article outlines how to search logs in Amazon CloudWatch Logs, analyze them, and determine when to switch to BLOCK mode. Even if you're using a different WAF, the same logic should apply when transitioning to BLOCK mode.&lt;/p&gt;

&lt;p&gt;Since this article assumes prior knowledge, I won’t explain basic concepts like “What is WAF?” or the specifications of AWS WAF’s managed rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  COUNT Mode vs BLOCK Mode
&lt;/h2&gt;

&lt;p&gt;WAF typically has two modes: COUNT and BLOCK. Although there is also an ALLOW mode, it’s less commonly used as a default setting. &lt;/p&gt;

&lt;p&gt;When first providing a service, you use COUNT mode because starting with BLOCK mode might accidentally block legitimate requests. After running in COUNT mode, you assess the logs to decide whether to switch to BLOCK mode.&lt;/p&gt;

&lt;p&gt;This article aims to share the know-how of analyzing logs. The general idea is that the rule can be switched to BLOCK if legitimate requests aren't recorded under COUNT mode.&lt;/p&gt;

&lt;p&gt;The truth is, the decision of whether or not to block also depends on the service we are providing. &lt;br&gt;
For example, it fluctuates depending on the following factors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the service require authentication?&lt;/li&gt;
&lt;li&gt;Is it offered internationally?&lt;/li&gt;
&lt;li&gt;Does it provide APIs that third parties can call?&lt;/li&gt;
&lt;li&gt;Is it B2C or B2B?&lt;/li&gt;
&lt;li&gt;This article focuses on general SaaS services and how to proceed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article summarizes the content of what you should look for in general SaaS.&lt;/p&gt;

&lt;p&gt;Also, you need to pay attention to the logs output by AWS WAF, so let's examine those first.&lt;/p&gt;
&lt;h2&gt;
  
  
  Viewing AWS WAF Logs
&lt;/h2&gt;

&lt;p&gt;Here, we will look at the logs using CloudWatch Logs Insights.&lt;br&gt;
However, you will only use CloudWatch Logs Insights the first time to download the COUNT logs.&lt;br&gt;
Because the AWS WAF logs are complex and the detection details are written in arrays, it is extremely difficult for me to analyze them using a query as they are, and depending on the number of logs, trial and error can end up costing a lot of money. Furthermore, when you want to search for User-Agent and user-agent at the same time, you can't even write a regular expression, so I chose jq. However, of course, there are cases where it is not possible to download the logs for governance reasons. In that case, I personally think it is better to output the logs to Amazon S3 and use Amazon Athena.&lt;/p&gt;

&lt;p&gt;First of all, we need to understand the structure of the log, so let's take a look. It looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{

    "timestamp":1533689070589,                            
    "formatVersion":1,                                   
    "webaclId":"385cb038-3a6f-4f2f-ac64-09ab912af590",  
    "terminatingRuleId":"Default_Action",                
    "terminatingRuleType":"REGULAR",                     
    "action":"ALLOW",                                    
    "httpSourceName":"CF",                               
    "httpSourceId":"i-123",                             
    "ruleGroupList":[                                    
     {  
        "ruleGroupId":"41f4eb08-4e1b-2985-92b5-e8abf434fad3",
        "terminatingRule":null,    
        "nonTerminatingMatchingRules":[                  
            {"action" : "COUNT", "ruleId" : "4659b169-2083-4a91-bbd4-08851a9aaf74"}       
        ],
        "excludedRules":[
            {"exclusionType" : "EXCLUDED_AS_COUNT", "ruleId" : "5432a230-0113-5b83-bbb2-89375c5bfa98"}
        ]                          
     }
    ],
    "rateBasedRuleList":[                                 
     {  
        "rateBasedRuleId":"7c968ef6-32ec-4fee-96cc-51198e412e7f",   
        "limitKey":"IP",
        "maxRateAllowed":100                                                                                           
     },
     {  
        "rateBasedRuleId":"462b169-2083-4a93-bbd4-08851a9aaf30",
        "limitKey":"IP",
        "maxRateAllowed":100
     }
    ],      
    "nonTerminatingMatchingRules":[                                
        {"action" : "COUNT",  "ruleId" : "4659b181-2011-4a91-bbd4-08851a9aaf52"}    
    ],                                  
    "httpRequest":{                                                             
        "clientIp":"192.10.23.23",                                           
        "country":"US",                                                         
        "headers":[                                                                 
            {  
                "name":"Host",
                "value":"127.0.0.1:1989"
             },
             {  
                "name":"User-Agent",
                "value":"curl/7.51.2"
             },
             {  
                 "name":"Accept",
                 "value":"*/*"
             }
        ],
        "uri":"REDACTED",                                                
        "args":"usernam=abc",                                         
        "httpVersion":"HTTP/1.1",
        "httpMethod":"GET",
        "requestId":"cloud front Request id"                    
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference: &lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/classic-logging.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/waf/latest/developerguide/classic-logging.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the case of AWS WAF, the field to look at in COUNT mode is called nonTerminatingMatchingRules.&lt;br&gt;
If the rule is set to ALLOW or BLOCK mode, you need to look at the &lt;code&gt;TerminatingMatchingRules&lt;/code&gt; field, which ends processing when a match is found, but COUNT mode is a non-terminating process, and it evaluates whether or not other rules match until the end. Therefore, you need to look at &lt;code&gt;notTerminatingMatchingRules&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Analyzing Logs
&lt;/h2&gt;

&lt;p&gt;You can extract the COUNT log by executing the following query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fields @timestamp, @message 
| filter @message like /"action":"COUNT"/ 
| sort @timestamp desc 
| display @timestamp, @message, httpRequest.clientIp, httpRequest.uri, httpRequest.country
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above results can be downloaded in JSON format. We recommend a minimum period of one month, but you should be aware that CloudWatch Logs Insights can only retrieve up to 10,000 items at a time.&lt;/p&gt;

&lt;p&gt;Once you've downloaded it, you can use jq to analyze it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determining if the Request is Legitimate
&lt;/h2&gt;

&lt;p&gt;Now that we've come this far, we'll finally start to judge whether or not the request is legitimate, but we'll continue on the assumption that the SaaS has authentication (login).&lt;/p&gt;

&lt;p&gt;First, run the following query. As previously mentioned, check the &lt;code&gt;ruleId&lt;/code&gt; within the &lt;code&gt;nonTerminatingMatchingRules&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jq -r '.[] | .["@message"].nonTerminatingMatchingRules[] | .ruleId' logs-insights-results.json | sort | uniq -c | sort -nr 

10 AWSManagedRulesAnonymousIpList 
5   AWSManagedRulesCommonRuleSet 
5   AWSManagedRulesBotControlRuleSet 
1   AWSManagedRulesLinuxRuleSet 
1   AWSManagedRulesKnownBadInputsRuleSet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can confirm these results. It is okay to set the rules that are not displayed here to BLOCK. This is because the fact that they are not recorded in normal use means that they are only recorded when an attack is received.&lt;/p&gt;

&lt;p&gt;Next, we will look at the combination of rules and URIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jq -r '.[] | .["@message"] as $msg | $msg.nonTerminatingMatchingRules[] | "\(.ruleId) \($msg.httpRequest.uri)"' logs-insights-results.json | sort AWSManagedRulesAnonymousIpList / 

AWSManagedRulesAnonymousIpList / 
AWSManagedRulesAnonymousIpList /.env 
AWSManagedRulesAnonymousIpList /auth/favicon.ico AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /wp-login.php 
AWSManagedRulesAnonymousIpList /wp-login.php 
AWSManagedRulesBotControlRuleSet / 
AWSManagedRulesBotControlRuleSet / 
AWSManagedRulesBotControlRuleSet /auth/favicon.ico AWSManagedRulesBotControlRuleSet /auth/login 
AWSManagedRulesBotControlRuleSet /auth/login 
AWSManagedRulesCommonRuleSet / 
AWSManagedRulesCommonRuleSet /news/12345
AWSManagedRulesCommonRuleSet /news/files 
AWSManagedRulesCommonRuleSet /auth/login 
AWSManagedRulesCommonRuleSet /news/sources/03a0 AWSManagedRulesKnownBadInputsRuleSet /.env 
AWSManagedRulesLinuxRuleSet /.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the following cases, it is possible to determine that it is BLOCK.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detection is only performed for URIs that are not used in the service

&lt;ul&gt;
&lt;li&gt;In the above example, the following are targeted

&lt;ul&gt;
&lt;li&gt;AWSManagedRulesKnownBadInputsRuleSet&lt;/li&gt;
&lt;li&gt;AWSManagedRulesLinuxRuleSet&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IP addresses/bot-related rules that are not detected in access after authentication

&lt;ul&gt;
&lt;li&gt;If there is no detection even after authentication, it can be seen that it is not legitimate use

&lt;ul&gt;
&lt;li&gt;It means that an unauthorized user logged in and was unable to use it&lt;/li&gt;
&lt;li&gt;If you don't want to allow bot access in the first place, just BLOCK them&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the example above, the following are targeted

&lt;ul&gt;
&lt;li&gt;AWSManagedRulesAnonymousIpList&lt;/li&gt;
&lt;li&gt;AWSManagedRulesBotControlRuleSe&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are not confident about your decision here, outputting the User-Agent may help you make a decision. You can output the User-Agent as follows (I will omit the results)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jq -r '.[] | .["@message"] as $msg | $msg.nonTerminatingMatchingRules[] | "\(.ruleId) \($msg.httpRequest.uri) \($msg.httpRequest.headers[] | select(.name | test("(?i)user-agent")) | .value)"' logs-insights-results.json | sort
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you look at this, you can see &lt;code&gt;User-Agent&lt;/code&gt; that are clearly not your own users, so you can use this as an element to determine that it is not a legitimate request Basically, the rules for BLOCK can be determined from this investigation. For the rest, we recommend continuing to operate in COUNT mode in case anything happens. The reason for BLOCKing up to this point is that there are not many cases where it is okay to change to BLOCK after logging in, and if there is a case where BLOCK should be used, it means that the site has already been attacked, so there is a possibility that incident response will begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes on detecting bots
&lt;/h2&gt;

&lt;p&gt;AWS WAF has a managed rule called &lt;code&gt;“AWSManagedRulesBotControlRuleSet”&lt;/code&gt;. As the name suggests, this is for detecting bot access.&lt;br&gt;
If you BLOCK access because “our service does not allow bot access”,&lt;br&gt;
there may be unexpected pitfalls. When you set this to BLOCK, please keep in mind the E2E test. It is possible that the E2E test will be detected as a Bot access, the test will fail, and the deployment will fail. Of course, you will be able to notice this before it goes live, so it won't be a big problem, but it is possible that the deployment will suddenly fail, and you won't know what the cause is, so I think it would be good to be aware of this. Also, if you are using a Bot for a service that anyone can access, such as a toC service, you may receive many attack scans if you are outputting logs. This may result in a large amount of output and cost you money. Please consider whether or not to enable BotControl, including these points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This article summarized how to determine which rules can be safely switched to BLOCK mode based on AWS WAF logs. While it may seem straightforward, figuring out how to search logs required some trial and error. Understanding the structure of logs is key.&lt;/p&gt;

&lt;p&gt;I hope this article helps, and I’d appreciate any feedback or suggestions. Thank you for reading.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Delete automatic assignment of Public IPv4 addresses to Amazon EC2 instances using the AWS Systems Manager Automation runbook.</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Thu, 11 Jul 2024 07:49:47 +0000</pubDate>
      <link>https://dev.to/nishikawaakira/delete-automatic-assignment-of-public-ipv4-addresses-to-amazon-ec2-instances-using-the-aws-systems-manager-automation-runbook-15h8</link>
      <guid>https://dev.to/nishikawaakira/delete-automatic-assignment-of-public-ipv4-addresses-to-amazon-ec2-instances-using-the-aws-systems-manager-automation-runbook-15h8</guid>
      <description>&lt;p&gt;I recently did something similar using AWS Config -&amp;gt; Amazon EventBridge -&amp;gt; AWS Lambda in another article. &lt;a href="https://dev.to/nishikawaakira/automatically-remove-automatic-ipv4-address-assignment-to-amazon-ec2-instances-12fl"&gt;another article&lt;/a&gt;. In this case, I would like to use an Automation runbook in AWS Systems Manager to automatically delete the public IPv4 addresses of Amazon EC2 instances.&lt;/p&gt;

&lt;p&gt;I would like to tell you that you don't need many programming skills to use an Automation runbook to gain control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an AWS Systems Manager Automation runbook
&lt;/h2&gt;

&lt;p&gt;Now, let's go ahead and create an AWS Systems Manager Automation runbook.&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%2Fhv4dao7p0u23klm29ac4.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%2Fhv4dao7p0u23klm29ac4.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&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%2Figqn4z2gyazad1jx33yq.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%2Figqn4z2gyazad1jx33yq.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, the following screen is opened, with the design tab now selected.&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%2F79dkawy15gq1tmtm3hxd.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%2F79dkawy15gq1tmtm3hxd.png" alt=" " width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can create a runbook using the graphical interface in this way, or you can select the {} Code tab and edit YAML or JSON directly.&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%2F023d7wmhs2lquytl96jn.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%2F023d7wmhs2lquytl96jn.png" alt=" " width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Writing an Automation runbook
&lt;/h2&gt;

&lt;p&gt;When you visually add parts to the Automation runbook, you will see YAML being described. You can also write Python within this YAML. Take, for example, &lt;a href="https://ap-northeast-1.console.aws.amazon.com/systems-manager/documents/AWS-BulkDeleteAssociation/content?region=ap-northeast-1" rel="noopener noreferrer"&gt;AWS-BulkDeleteAssociation&lt;/a&gt; in the managed runbook.&lt;/p&gt;

&lt;p&gt;The following is written in Python, but this is quite difficult without some programming skills. This is not smart, and for the purpose of this article, I want to show that it can be implemented by people with no programming knowledge, so I want to prove that we can do it without using services like AWS Lambda!&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%2Fif3trflsmtzzy8tja9st.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%2Fif3trflsmtzzy8tja9st.png" alt=" " width="800" height="860"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First of all, I will create a runbook with only the minimum required elements. Select the "AWS APIs" tab under the search box and enter "modifynetwork" in the search box to find the ModifyNetworkInterfaceAttribute of AWS API to be used this time. Drag and drop it to the middle of the "ModifyNetworkInterfaceAttribute" between Start and End.&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%2Fmifojlreaiu5m1tbi0xu.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%2Fmifojlreaiu5m1tbi0xu.png" alt=" " width="799" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{} Open the Code tab. You will see that "ModifyNetworkInterfaceAttribute" is being hit as an API, as shown below.&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%2Feog08tu6jpqry91n9j7x.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%2Feog08tu6jpqry91n9j7x.png" alt=" " width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We want to put arguments here, as you can see from the AWS Lambda code in the previous article. It seems to be sufficient to enter NetworkInterfaceId.　&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyNetworkInterfaceAttribute.html" rel="noopener noreferrer"&gt;API Reference is here&lt;/a&gt; You can also see that you need to set AssociatePublicIpAddress to false to prevent the automatic assignment of Public IPv4 addresses.&lt;/p&gt;

&lt;p&gt;Let's describe it as follows. The regular expression is written assuming that NetworkInterfaceId starts with "eni-" and that a combination of alphabets and numbers is entered. Also, entering (Required) in the parameter description will make it a required parameter. It is not mandatory to pass assumeRole as a parameter, but it is always recommended to do so. This is because you may want to use this runbook by assuming it from another AWS account. For example, there are use cases where a runbook is distributed to each account in CloudFormation StackSets, and the runbook can be assumed and executed from an administrative account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schemaVersion: '0.3'
description: Disable Public IPv4 on EC2.
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  AutomationAssumeRole:
    type: String
    description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf.
    default: ''
  NetworkInterfaceId:
    type: String
    description: (Required) Network Interface Id
    allowedPattern: ^eni-[a-z0-9]+$
mainSteps:
  - name: ModifyNetworkInterfaceAttribute
    action: aws:executeAwsApi
    isEnd: true
    inputs:
      Service: ec2
      Api: ModifyNetworkInterfaceAttribute
      NetworkInterfaceId: '{{ NetworkInterfaceId }}'
      AssociatePublicIpAddress: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, enter a suitable name in NewRunbook and press "Create runbook".&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%2Fcovitff45kpch1p5qls1.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%2Fcovitff45kpch1p5qls1.png" alt=" " width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Run Automation runbook
&lt;/h2&gt;

&lt;p&gt;Open the runbook you have created. You can find the runbook you created in the "Owned by me" tab.&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%2Ft2guz0eusmxahyanft4s.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%2Ft2guz0eusmxahyanft4s.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open it and click on Execute automation.&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%2F0i3gesva1jivupytioe0.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%2F0i3gesva1jivupytioe0.png" alt=" " width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before you do so, create an EC2 instance. Check the network interface ID of the Amazon EC2 instance.&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%2Fl6ytvwlqn2twj1h4e5hs.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%2Fl6ytvwlqn2twj1h4e5hs.png" alt=" " width="800" height="762"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try entering a Network Interface ID starting with "eni-" in the "NetworkInterfaceId" field of the Input parameters and click Execute.&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%2F6em9to6wy24dlt0da7mb.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%2F6em9to6wy24dlt0da7mb.png" alt=" " width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check that the public IP address in the network interface ID on the EC2 instance has been removed. You will see that it has been deleted. We will now see if this is really the end of the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Config Settings
&lt;/h2&gt;

&lt;p&gt;As before, I will use the "ec2-instance-no-public-ip" managed rule. Open this Config rule and click on "Manage remediation".&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%2Fz0gsh24r6v6k31jkrtfk.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%2Fz0gsh24r6v6k31jkrtfk.png" alt=" " width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose your own AWS Systems Manager Automation runbook.&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%2F10zxtv3h4oyloon4pefl.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%2F10zxtv3h4oyloon4pefl.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that only InstanceId can be passed as a Resource ID parameter.&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%2Fjdfltv551yq40gjn2ei7.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%2Fjdfltv551yq40gjn2ei7.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oops, it seems that the network interface ID cannot be passed here, even though I really want to pass it here. In other words, there seems to be no way to pass the network interface ID dynamically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a List of Network Interface IDs from EC2 Instance ID
&lt;/h2&gt;

&lt;p&gt;We have no choice but to change course. You will find that you need to get the network interface ID from the EC2 instance to pass it on. There are two main methods. The first is to use DescribeInstances to get all EC2 information and extract only the necessary information. The second is to use &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkInterfaces.html" rel="noopener noreferrer"&gt;DescribeNetworkInterfaces&lt;/a&gt; to get just the network interface information.&lt;/p&gt;

&lt;p&gt;This time, the information on the EC2 instance will hardly be used, so we will hit the DescribeNetworkInterfaces API to get the information on network interfaces only.&lt;/p&gt;

&lt;p&gt;So, as shown below, the DescribeNetworkInterfaces API is used to create the part that obtains information on network interfaces.&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%2F9epmrzxb1fx814oa38c9.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%2F9epmrzxb1fx814oa38c9.png" alt=" " width="688" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schemaVersion: '0.3'
description: Disable Public IPv4 on EC2.
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  InstanceId:
    type: String
    description: The ID of EC2 instance
  AutomationAssumeRole:
    type: String
    description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf.
    default: ''
mainSteps:
  - name: DescribeNetworkInterfaces
    action: aws:executeAwsApi
    isEnd: true
    inputs:
      Service: ec2
      Api: DescribeNetworkInterfaces
      Filters:
        - Name: attachment.instance-id
          Values:
            - '{{InstanceId}}'
    outputs:
      - Name: NetworkInterfaceIds
        Selector: $.NetworkInterfaces..NetworkInterfaceId
        Type: StringList
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Network interface IDs can now be retrieved as a StringList. There is a caveat here: if you look at the API specification for DescribeNetworkInterfaces, the Request Parameter is written as Filter.N, but at first, it was not clear how to write it in YAML. The answer I arrived at is below, which is closer to the way &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-network-interfaces.html" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;　is written.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filters:
- Name: attachment.instance-id
Values:
- '{{InstanceId}}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another is the Selector in outputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Selector: $.NetworkInterfaces..NetworkInterfaceId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Selector allows you to specify which values are output and used in the next step. As an EC2 instance may have multiple network interfaces, it is necessary to retrieve them all, but when using Selector: $.NetworkInterfaces, only a MapList (like an associative array) can be specified for Type. In this case, the loop in the runbook cannot be used. Therefore, you need to get the array as a StringList, which can be done by writing "..". By writing this, the list of specified items can be retrieved as a StringList.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete the Public IPv4 Address of the Relevant Network Interface ID
&lt;/h2&gt;

&lt;p&gt;Now that you have reached this point, the rest is easy. Create a runbook like the following.&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%2F692nutvvc6kyzioci1ae.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%2F692nutvvc6kyzioci1ae.png" alt=" " width="800" height="890"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final code is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schemaVersion: '0.3'
description: Disable Public IPv4 on EC2.
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  InstanceId:
    type: String
    description: The ID of EC2 instance
  AutomationAssumeRole:
    type: String
    description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf.
    default: ''
mainSteps:
  - name: DescribeNetworkInterfaces
    action: aws:executeAwsApi
    nextStep: Loop
    isEnd: false
    inputs:
      Service: ec2
      Api: DescribeNetworkInterfaces
      Filters:
        - Name: attachment.instance-id
          Values:
            - '{{InstanceId}}'
    outputs:
      - Name: NetworkInterfaceIds
        Selector: $.NetworkInterfaces..NetworkInterfaceId
        Type: StringList
  - name: Loop
    action: aws:loop
    isEnd: true
    inputs:
      Iterators: '{{ DescribeNetworkInterfaces.NetworkInterfaceIds }}'
      Steps:
        - name: ModifyNetworkInterfaceAttribute
          action: aws:executeAwsApi
          isEnd: true
          inputs:
            Service: ec2
            Api: ModifyNetworkInterfaceAttribute
            NetworkInterfaceId: '{{ Loop.CurrentIteratorValue }}'
            AssociatePublicIpAddress: false

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Create new version", then select "Create new default version" to create a new document.&lt;/p&gt;

&lt;p&gt;Before you start testing, launch an EC2 instance and attach several Network Interfaces.&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%2Fyfmh3j2d41zkq86orw1y.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%2Fyfmh3j2d41zkq86orw1y.png" alt=" " width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then open the runbook, click on "Execute automation", enter the InstanceId and press Execute.&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%2Faly8h53ps2gg4xo9pwfy.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%2Faly8h53ps2gg4xo9pwfy.png" alt=" " width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check the previous screen to see if it has been disabled properly. If it is disabled as shown in the image below, you have succeeded.&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%2Fygzk1df8gb1126uccmgu.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%2Fygzk1df8gb1126uccmgu.png" alt=" " width="800" height="775"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this way, control can be exercised without programming knowledge. However, when you write it like this, you might think it's easy to do! And while that is the aim, there are some real quirks in the way YAML is written, and I had a lot of trouble with it. At first, I thought that to get the network interface IDs as a list, I would need to write some Python code. And indeed, the first version of this blog did. While researching, I realized that it could be retrieved using "..".&lt;/p&gt;

&lt;p&gt;I'm going to accumulate this kind of runbook writing style and work towards a situation where anyone can control the AWS environment!&lt;/p&gt;

&lt;p&gt;Thank you for reading to the end.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>automation</category>
    </item>
    <item>
      <title>Automatically remove automatic IPv4 address assignment to Amazon EC2 instances</title>
      <dc:creator>nishikawaakira</dc:creator>
      <pubDate>Wed, 05 Jun 2024 18:49:42 +0000</pubDate>
      <link>https://dev.to/nishikawaakira/automatically-remove-automatic-ipv4-address-assignment-to-amazon-ec2-instances-12fl</link>
      <guid>https://dev.to/nishikawaakira/automatically-remove-automatic-ipv4-address-assignment-to-amazon-ec2-instances-12fl</guid>
      <description>&lt;p&gt;It is now possible to dynamically remove and add public IPv4 addresses that were recently automatically assigned to an EC2 instance.&lt;br&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/04/removing-adding-auto-assigned-public-ipv4-address/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Accordingly, this article will try to automatically delete the public IPv4 address that was assigned to EC2 by default.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;This time, I used the configuration AWS Config -&amp;gt; Amazon EventBridge -&amp;gt; AWS Lambda. Actually, AWS Config -&amp;gt; AWS Systems Manager can also be configured using Automation runbook, which is smarter, but this time I would like to introduce the method using AWS Lambda.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configure AWS Config
&lt;/h2&gt;

&lt;p&gt;First, configure AWS Config." &lt;a href="https://docs.aws.amazon.com/config/latest/developerguide/ec2-instance-no-public-ip.html" rel="noopener noreferrer"&gt;ec2-instance-no-public-ip&lt;/a&gt;". This managed rule, however, does not enable evaluation of provisioned resources. However, the evaluation mode of this managed rule enables evaluation of provisioned resources, and evaluation is performed only after the instance is started. Therefore, if you want to modify it at startup, you need to prepare your own rules. On the other hand, if you want to target EC2 after startup, this rule may be suitable.&lt;/p&gt;

&lt;p&gt;That's all there is to AWS Config. It is very easy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Create AWS Lambda
&lt;/h2&gt;

&lt;p&gt;In fact, here is where I run into a bit of a snag. In this case, I have chosen Python, but in general, if you want to use Python in Lambda to execute APIs, you will need to use a library called Boto3. Lambda comes standard with this library, but if the API specification has just been changed, as in this case, Boto3 is not supported, or the version of Boto3 in Lambda itself is a bit old, and an error will occur.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyNetworkInterfaceAttribute.html" rel="noopener noreferrer"&gt;API Specification&lt;/a&gt; and send a request as follows,&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%2Fpt0r9o4m6uitcc2uz6oo.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%2Fpt0r9o4m6uitcc2uz6oo.png" alt="Actual error" width="795" height="71"&gt;&lt;/a&gt;&lt;br&gt;
Unknown parameter in input: "AssociatePublicIpAddress"&lt;br&gt;
You may see that you have been told, "Unknown parameter in input: "AssociatePublicIpAddress".&lt;/p&gt;

&lt;p&gt;So, let's first find out how many versions of Boto3 Lambda has.&lt;br&gt;
Let's run the following in Lambda.&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%2Frzzag7dtzt3lcx4wahza.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%2Frzzag7dtzt3lcx4wahza.png" alt="print Boto3 version" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I ran this, it showed 1.34.42 (as of 5/14/2024).&lt;br&gt;
However, the latest version of Boto3 at this time is 1.34.103. And I see that the ability to dynamically remove and add auto-assigned public IPv4 addresses for this EC2 instance was added in 1.34.91.&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%2F60scwkzo74zmc4sucecq.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%2F60scwkzo74zmc4sucecq.png" alt="Boto3 version" width="799" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I found that it is not possible to run the application as it is, so we need to create a Lambda Layer.&lt;/p&gt;

&lt;p&gt;Incidentally, as a side note, the same results were obtained when making API calls with YAML in Systems Manager, so it seems that Systems Manager also takes some time to catch up with the latest API.&lt;br&gt;
So, keep in mind that you will need to wait a few days after a new specification is implemented.&lt;/p&gt;

&lt;p&gt;I will not discuss how to create the Lambda Layer, but once it has been created, implement it as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json
import boto3

def modify_network_interface_attribute(network_interface_id):
    ec2 = boto3.client('ec2')

    try:
        response = ec2.modify_network_interface_attribute(
            NetworkInterfaceId=network_interface_id, # Specify the target Network Interface ID.
            AssociatePublicIpAddress=False # False deletes the public IPv4 address
        )
        print("Network Interface Modified Successfully:")
        print(response)
    except Exception as e:
        print("An error occurred:", e)



def lambda_handler(event, context):

    detail = event['detail']
    # network_interface_id = detail['configurationItem']['configuration']['networkInterfaceId']

    network_interface_ids = []

    # Get a list of resources that have changed
    result = detail.get('newEvaluationResult')
    identifier = result.get('evaluationResultIdentifier')
    configuration_item = identifier.get('evaluationResultQualifier')

    # Check if the resource type is an EC2 instance
    if configuration_item and configuration_item['resourceType'] == 'AWS::EC2::Instance':
        instance_id = configuration_item['resourceId']

        ec2_client = boto3.client('ec2')

        # Get instance details
        response = ec2_client.describe_instances(InstanceIds=[instance_id])
        reservations = response['Reservations']

        # Stop auto-assign of all network interface IDs
        for reservation in reservations:
            for instance in reservation['Instances']:
                for interface in instance['NetworkInterfaces']:
                    network_interface_ids.append(interface['NetworkInterfaceId'])
                    modify_network_interface_attribute(interface['NetworkInterfaceId'])

    print("Network Interface IDs: ", network_interface_ids)

    return {
        'statusCode': 200,
        'body': json.dumps('Success!!')
    }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is written in a rather crude manner and returns, "Success! but if you just want to remove the public IP address automatically, you can do it with this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing AWS Lambda
&lt;/h2&gt;

&lt;p&gt;The following can be used for testing. Please enter the instance ID you have set up appropriately in resourceId, and then run the test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "version": "0",
  "id": "ID",
  "detail-type": "Config Rules Compliance Change",
  "source": "aws.config",
  "account": "[AWS Account ID]",
  "time": "2024-05-19T12:19:54Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {
    "resourceId": "[Instance ID]",
    "awsRegion": "ap-northeast-1",
    "awsAccountId": "[AWS Account ID]",
    "configRuleName": "ec2-instance-no-public-ip",
    "recordVersion": "1.0",
    "configRuleARN": "arn:aws:config:ap-northeast-1:[AWS Account ID]:config-rule/config-rule-qx4cpe",
    "messageType": "ComplianceChangeNotification",
    "newEvaluationResult": {
      "evaluationResultIdentifier": {
        "evaluationResultQualifier": {
          "configRuleName": "ec2-instance-no-public-ip",
          "resourceType": "AWS::EC2::Instance",
          "resourceId": "[Instance ID]",
          "evaluationMode": "DETECTIVE"
        },
        "orderingTimestamp": "2024-05-19T00:03:43.749Z"
      },
      "complianceType": "NON_COMPLIANT",
      "resultRecordedTime": "2024-05-19T12:19:53.922Z",
      "configRuleInvokedTime": "2024-05-19T12:19:53.696Z",
      "annotation": "This Amazon EC2 Instance uses a public IP."
    },
    "notificationCreationTime": "2024-05-19T12:19:54.417Z",
    "resourceType": "AWS::EC2::Instance"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it works correctly, the configuration of the instance in question should change as follows.&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%2Fx4u3w06tl66sobq8pw45.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%2Fx4u3w06tl66sobq8pw45.png" alt="Auto-assign public IP disabled" width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Configure Amazon Event Bridge
&lt;/h2&gt;

&lt;p&gt;Finally, let's create the rules for EventBridge.&lt;/p&gt;

&lt;p&gt;The event pattern is as follows AWS Config is the source and calls Lambda based on Compliance Change for ec2-instance-no-public-ip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "source": ["aws.config"],
  "detail-type": ["Config Rules Compliance Change"],
  "detail": {
    "configRuleName": ["ec2-instance-no-public-ip"]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select AWS Lambda function in Target and select AWS Lambda function you just created.&lt;/p&gt;

&lt;p&gt;All that remains is to create another instance and confirm that the modifications have been made automatically.&lt;br&gt;
You may want to monitor it to make sure it is working properly.&lt;br&gt;
If it is not working properly, try logging in AWS Lambda function and debugging to see if the Lambda itself is being called properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I was able to remove the public IP from EC2 more easily than I expected, but it is not as simple as using AWS Systems Manager Automation runbook (my own subjective opinion), and requires a fair amount of coding knowledge. I'm writing roughly again this time, but we should also pick up error cases more thoroughly, and we need to pay attention to such things.&lt;br&gt;
Next time, I would like to do the same assignment to see how it can be written using AWS Systems Manager Automation runbook.&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
  </channel>
</rss>
