<?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: BlackNeuron</title>
    <description>The latest articles on DEV Community by BlackNeuron (@blackneuron).</description>
    <link>https://dev.to/blackneuron</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%2F4006713%2Ffdd4a936-d6b9-4e89-a8e7-439de7b232c8.png</url>
      <title>DEV Community: BlackNeuron</title>
      <link>https://dev.to/blackneuron</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blackneuron"/>
    <language>en</language>
    <item>
      <title>Your CDN says it is protecting you. I checked, and 0% of the traffic was cached.</title>
      <dc:creator>BlackNeuron</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:27:52 +0000</pubDate>
      <link>https://dev.to/blackneuron/your-cdn-says-it-is-protecting-you-i-checked-and-0-of-the-traffic-was-cached-ai</link>
      <guid>https://dev.to/blackneuron/your-cdn-says-it-is-protecting-you-i-checked-and-0-of-the-traffic-was-cached-ai</guid>
      <description>&lt;p&gt;You put your site behind a CDN, watched the dashboard turn green, and moved on. The edge will soak up the floods now. Right?&lt;/p&gt;

&lt;p&gt;Mostly. Its network still eats volumetric floods and its edge can filter and rate-limit. But one layer quietly depends on you, and the availability dashboard stays green while it fails: &lt;strong&gt;your responses are not actually being cached.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one sentence
&lt;/h2&gt;

&lt;p&gt;Cache absorption is the layer that depends on your own config: a CDN serves what it can &lt;strong&gt;from its own cache,&lt;/strong&gt; and those requests never reach your origin. Every response it cannot cache is one your origin has to generate itself. So under a flood of uncacheable requests, the edge forwards them through, and unless another edge control catches them, your origin answers every one.&lt;/p&gt;

&lt;p&gt;The math is blunt:&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%2Fgr58ah4gh9tucoxoov1b.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%2Fgr58ah4gh9tucoxoov1b.png" alt="How much of a 50,000 rps flood reaches your origin, by edge cache hit-ratio"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a 0% hit-ratio your origin sees the entire flood. At 95% it sees a twentieth of it. A CDN does not move you along that curve by being installed. It moves you by actually caching, and that is a property of &lt;em&gt;your&lt;/em&gt; response headers, not the vendor's marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the edge quietly caches less than you think
&lt;/h2&gt;

&lt;p&gt;None of these require a misconfigured CDN. They are ordinary application defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Cache-Control: private&lt;/code&gt; or &lt;code&gt;no-store&lt;/code&gt;.&lt;/strong&gt; Frameworks slap these on by default for anything that looks dynamic. Perfectly correct for a logged-in page. Also means the edge forwards every hit to origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;Set-Cookie&lt;/code&gt; on the response.&lt;/strong&gt; By default, a session cookie on your HTML (or worse, on your static assets) tells shared caches not to store it. One analytics or CSRF cookie can make your whole site uncacheable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max-age=0&lt;/code&gt; / &lt;code&gt;no-cache&lt;/code&gt;.&lt;/strong&gt; The object revalidates with origin on every request, so the origin is in the loop every time anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt;.&lt;/strong&gt; Cloudflare caches common static extensions by default, but HTML and anything off that list comes back &lt;code&gt;DYNAMIC&lt;/code&gt; and is not cached until you add a Cache Rule. Most sites never add it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any one of these, sitting on the paths a flood targets, and your expensive edge is a very fast reverse proxy to the thing you were trying to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I wrote a small tool to measure it
&lt;/h2&gt;

&lt;p&gt;I wanted a one-command answer to "what fraction of my own traffic does the edge actually absorb, right now?" So I built &lt;strong&gt;&lt;code&gt;edge-cache-check&lt;/code&gt;&lt;/strong&gt; in Rust. It is deliberately boring on the network: it fetches your own pages and reads the cache headers. No flooding, no attacks, single binary, no config.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the &lt;strong&gt;landing page&lt;/strong&gt; and fingerprint the edge from its headers (&lt;code&gt;cf-cache-status&lt;/code&gt;, &lt;code&gt;x-cache&lt;/code&gt;, &lt;code&gt;x-vercel-cache&lt;/code&gt;, &lt;code&gt;via&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Read the HTML and build a &lt;strong&gt;representative sample&lt;/strong&gt; of your own URLs: the static assets it references plus a few internal pages.&lt;/li&gt;
&lt;li&gt;Request each URL &lt;strong&gt;twice.&lt;/strong&gt; The first request warms the edge; the second is the one scored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; each warmed response as &lt;code&gt;EDGE&lt;/code&gt; (served from cache), &lt;code&gt;CACHE&lt;/code&gt; (cacheable but missed), or &lt;code&gt;ORIGIN&lt;/code&gt; (cannot be cached), with the reason.&lt;/li&gt;
&lt;li&gt;Report an absorption score for the static assets, whether the landing page is cached, and the top reasons anything falls through.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The interesting part: deciding "did the origin see this?"
&lt;/h3&gt;

&lt;p&gt;Every CDN spells "cache hit" differently, so the load-bearing logic is normalising them into one honest answer. A hit is a hit whether it is Cloudflare's &lt;code&gt;cf-cache-status: HIT&lt;/code&gt;, CloudFront's &lt;code&gt;x-cache: Hit from cloudfront&lt;/code&gt;, Vercel's &lt;code&gt;x-vercel-cache: HIT&lt;/code&gt;, or simply an &lt;code&gt;Age&lt;/code&gt; header greater than zero (if the object has been sitting in cache for 12 seconds, it came from cache):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.cf_cache&lt;/span&gt;&lt;span class="nf"&gt;.as_deref&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;matches!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.to_uppercase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
             &lt;span class="s"&gt;"HIT"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"UPDATING"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"STALE"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.x_cache&lt;/span&gt;&lt;span class="nf"&gt;.as_deref&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.to_lowercase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hit"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.x_vercel&lt;/span&gt;&lt;span class="nf"&gt;.as_deref&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&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;let&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.to_uppercase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"HIT"&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"STALE"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.age&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a&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="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&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="nn"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Absorbed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"served from edge cache"&lt;/span&gt;&lt;span class="nf"&gt;.into&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 it was not a hit, the useful output is not "MISS", it is &lt;em&gt;why&lt;/em&gt;. That is what turns the result into a fix instead of a shrug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;has_vendor&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="nn"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"no CDN edge detected - request reaches origin"&lt;/span&gt;&lt;span class="nf"&gt;.into&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="n"&gt;cc&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no-store"&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="nn"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Cache-Control: no-store"&lt;/span&gt;&lt;span class="nf"&gt;.into&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="n"&gt;cc&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"private"&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="nn"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Cache-Control: private"&lt;/span&gt;&lt;span class="nf"&gt;.into&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="n"&gt;cc&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no-cache"&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="nn"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Cache-Control: no-cache (revalidate every time)"&lt;/span&gt;&lt;span class="nf"&gt;.into&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="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.set_cookie&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"public"&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="nn"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Set-Cookie on the response defeats shared caching"&lt;/span&gt;&lt;span class="nf"&gt;.into&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 two-fetch design matters here too. A single request to a cold object on a large multi-PoP edge is a MISS even when the object is perfectly cacheable, so a naive checker would cry wolf. Warming first, then scoring the second response, separates "cannot be cached" from "just was not cached yet." The tool reports those as two different buckets on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; example.com
&lt;span class="c"&gt;# or build once, then:&lt;/span&gt;
./target/release/edge-cache-check example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy run is calm: your assets come back &lt;code&gt;[EDGE ]&lt;/code&gt;, the score is high, and the verdict is &lt;code&gt;STRONG&lt;/code&gt;. An unhealthy one is specific about the damage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    static assets:   2/14 served from edge (14%), 21% cacheable in total
    landing page:    NOT cached - every hit reaches origin
    why not cached:
      11x  Set-Cookie on the response defeats shared caching
       3x  Cache-Control: private

[!] WEAK: an edge is present but is absorbing very little.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a two-line fix (drop the cookie from static asset responses, add a cache rule), and a real change in how much of a flood your origin ever sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The remediation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make static assets genuinely cacheable: a long &lt;code&gt;max-age&lt;/code&gt;, &lt;code&gt;immutable&lt;/code&gt;, and &lt;strong&gt;no &lt;code&gt;Set-Cookie&lt;/code&gt;&lt;/strong&gt; on asset responses.&lt;/li&gt;
&lt;li&gt;Add an explicit cache/edge rule on the paths that can tolerate it, rather than assuming the CDN's default covers everything (Cloudflare caches static extensions by default, but not HTML).&lt;/li&gt;
&lt;li&gt;For the dynamic paths that legitimately cannot be cached, stop relying on the origin as the only line of defence. That is what edge rate-limiting and a WAF are for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Absorption is not the whole of DDoS resilience, but it is the pillar people assume they have for free the moment they install a CDN, and most do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule
&lt;/h2&gt;

&lt;p&gt;Run it only against domains and infrastructure &lt;strong&gt;you own or are explicitly authorized to test.&lt;/strong&gt; It only reads your own public responses, but point it at your own stuff.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Tool (MIT, Rust): &lt;strong&gt;&lt;a href="https://github.com/blackneuron-ai/edge-cache-check" rel="noopener noreferrer"&gt;https://github.com/blackneuron-ai/edge-cache-check&lt;/a&gt;&lt;/strong&gt; (a star helps if it is useful to you ⭐)&lt;/li&gt;
&lt;li&gt;More on edge absorption and DDoS resilience: &lt;strong&gt;&lt;a href="https://blackneuron.ai/blog" rel="noopener noreferrer"&gt;https://blackneuron.ai/blog&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run it on your own site and the number surprises you, the reason column is usually the interesting part. I would like to hear what caught you out.&lt;/p&gt;

&lt;p&gt;*Written while building defensive tooling at &lt;a href="https://blackneuron.ai" rel="noopener noreferrer"&gt;BlackNeuron&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>You hardened your origin and your CDN. Your DNS is still one provider away from going dark.</title>
      <dc:creator>BlackNeuron</dc:creator>
      <pubDate>Sun, 05 Jul 2026 17:12:13 +0000</pubDate>
      <link>https://dev.to/blackneuron/you-hardened-your-origin-and-your-cdn-your-dns-is-still-one-provider-away-from-going-dark-3abk</link>
      <guid>https://dev.to/blackneuron/you-hardened-your-origin-and-your-cdn-your-dns-is-still-one-provider-away-from-going-dark-3abk</guid>
      <description>&lt;p&gt;You put the site behind a CDN, locked the origin firewall to the edge ranges, tuned rate limits, and slept better. Then someone floods your DNS instead, and none of that matters, because nobody can resolve your name to reach any of it.&lt;/p&gt;

&lt;p&gt;DNS is the piece almost nobody load-checks for resilience. And the most common failure is boring: every authoritative nameserver sitting with one provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one provider is the whole risk
&lt;/h2&gt;

&lt;p&gt;If all your &lt;code&gt;NS&lt;/code&gt; records point at a single DNS host, that host is a single point of failure for your entire domain. A DDoS against it, or just a bad day in their control plane, and every name under you goes dark at once. The blast radius is total, and it happens upstream of everything you spent time hardening.&lt;/p&gt;

&lt;p&gt;The fix is old and well known: run authoritative DNS across two providers on different networks. The problem is that almost nobody checks whether they actually did, because it is invisible until the day it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I wrote a small tool to check my own
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dns-resilience-check&lt;/code&gt; points at a domain you own and answers one question: is your DNS a single point of failure? It looks at what a resilience review would look at.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How many authoritative nameservers you run (one is a SPOF).&lt;/li&gt;
&lt;li&gt;Whether those nameservers actually sit in different networks, not just different hostnames on the same provider.&lt;/li&gt;
&lt;li&gt;Whether DNSSEC is on.&lt;/li&gt;
&lt;li&gt;Whether the NS TTLs are sane.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It reads only public DNS data over DNS-over-HTTPS. No zone transfers, no traffic at anyone, nothing to install beyond Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I liked building
&lt;/h2&gt;

&lt;p&gt;"Different provider" is the finding that matters, and the honest way to check it is by network, not by name. &lt;code&gt;ns1&lt;/code&gt; and &lt;code&gt;ns2&lt;/code&gt; on the same host tell you nothing. So the tool resolves each nameserver to its IPs and maps each IP to the network (ASN) that owns it. If every nameserver lands in one ASN, that is your single point of failure, no matter how many &lt;code&gt;NS&lt;/code&gt; records you have.&lt;/p&gt;

&lt;p&gt;The neat trick is that you can get IP-to-ASN over plain DNS, via Team Cymru's public zone. Reverse the octets, ask for a TXT record, and you get the ASN back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1.2.3.4 -&amp;gt; "4.3.2.1.origin.asn.cymru.com" TXT -&amp;gt; "AS64500 | 1.2.0.0/16 | ..."&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;asn_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}.{}.{}.{}.origin.asn.cymru.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;o&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="n"&gt;o&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="n"&gt;o&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="c1"&gt;// doh() is one DNS-over-HTTPS query; parse the TXT and take the ASN field&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;doh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"TXT"&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="c1"&gt;// "AS64500 | prefix | CC | ..."&lt;/span&gt;
    &lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'|'&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="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.split_whitespace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AS{s}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.collect&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;Collect the ASNs across all the nameservers, and if the set has one element, the tool flags it and tells you to add a second provider on a different network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CONTAINED&lt;/code&gt; means nothing obvious is wrong. Each &lt;code&gt;[FAIL]&lt;/code&gt; is a gap with the fix printed next to it. On a domain sitting entirely with one DNS host it will say so plainly, which is usually the moment people go add a secondary.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule
&lt;/h2&gt;

&lt;p&gt;Run it against domains you own or are allowed to test. It only reads public data, but point it at your own stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code + the longer write-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tool (MIT, Rust): &lt;strong&gt;&lt;a href="https://github.com/BlackNeuron-ai/dns-resilience-check" rel="noopener noreferrer"&gt;https://github.com/BlackNeuron-ai/dns-resilience-check&lt;/a&gt;&lt;/strong&gt; (a star helps if it is useful to you)&lt;/li&gt;
&lt;li&gt;The deeper piece on DNS under DDoS: &lt;strong&gt;&lt;a href="https://blackneuron.ai/blog/dns-ddos-testing" rel="noopener noreferrer"&gt;https://blackneuron.ai/blog/dns-ddos-testing&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run it and find a surprise (a "second" provider that turns out to be the same ASN is a common one), I would like to hear it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written while building defensive tooling at &lt;a href="https://blackneuron.ai" rel="noopener noreferrer"&gt;BlackNeuron&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>devops</category>
      <category>dns</category>
    </item>
    <item>
      <title>I put Cloudflare in front of my site, then found my origin one DNS record away from undoing it</title>
      <dc:creator>BlackNeuron</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:50:50 +0000</pubDate>
      <link>https://dev.to/blackneuron/i-put-cloudflare-in-front-of-my-site-then-found-my-origin-one-dns-record-away-from-undoing-it-49bf</link>
      <guid>https://dev.to/blackneuron/i-put-cloudflare-in-front-of-my-site-then-found-my-origin-one-dns-record-away-from-undoing-it-49bf</guid>
      <description>&lt;p&gt;You move your site behind Cloudflare (or CloudFront, or any CDN/WAF), watch the dashboard light up green, and feel safe. The edge will soak up the floods now. Right?&lt;/p&gt;

&lt;p&gt;Mostly. But there is a quiet failure mode that undoes the whole thing in one step, and almost nobody tests for it: &lt;strong&gt;origin IP exposure&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one sentence
&lt;/h2&gt;

&lt;p&gt;A CDN only filters the traffic that actually passes &lt;em&gt;through&lt;/em&gt; it. If your origin server still answers on its own public IP, an attacker who learns that IP just connects straight to it, and every layer of DDoS and WAF protection you are paying for is bypassed.&lt;/p&gt;

&lt;p&gt;The edge is protecting a secret (your origin IP), not a wall. And secrets leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the secret leaks
&lt;/h2&gt;

&lt;p&gt;You do not need to breach anything to find an origin. The data is usually already public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificate Transparency logs.&lt;/strong&gt; Every TLS certificate ever issued is logged publicly. Search &lt;code&gt;crt.sh&lt;/code&gt; for a domain and you get a tidy list of its subdomains, including the &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;mail&lt;/code&gt; hosts nobody remembered to put behind the CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boring subdomains.&lt;/strong&gt; &lt;code&gt;mail.&lt;/code&gt;, &lt;code&gt;smtp.&lt;/code&gt;, &lt;code&gt;vpn.&lt;/code&gt;, &lt;code&gt;cpanel.&lt;/code&gt; and friends are hard to proxy through a web CDN, so they frequently resolve straight to the origin, on the same IP as the "protected" site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS history.&lt;/strong&gt; The A record from before you switched to the CDN is often still sitting in historical DNS datasets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find one of those, confirm the IP serves the real site, and the CDN is now optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built a small tool to check my own
&lt;/h2&gt;

&lt;p&gt;I wanted a one-command answer to "is my origin reachable past my CDN, right now?" So I wrote &lt;strong&gt;&lt;code&gt;origin-exposure-check&lt;/code&gt;&lt;/strong&gt; in Rust. It does exactly the discovery an attacker would, against a domain you own, so you find the leak first.&lt;/p&gt;

&lt;p&gt;It is deliberately boring on the network: a few DNS lookups, one &lt;code&gt;crt.sh&lt;/code&gt; query, and a handful of normal GET requests. No flooding, no attacks. Single binary, no config.&lt;/p&gt;

&lt;p&gt;Here is the whole flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the &lt;strong&gt;published edge ranges&lt;/strong&gt; (Cloudflare, CloudFront) so we can &lt;em&gt;exclude&lt;/em&gt; the legitimate edge IPs.&lt;/li&gt;
&lt;li&gt;Fetch a &lt;strong&gt;baseline&lt;/strong&gt; of the site through the CDN, and fingerprint the response.&lt;/li&gt;
&lt;li&gt;Enumerate candidate hosts from &lt;strong&gt;common subdomains + Certificate Transparency&lt;/strong&gt; (&lt;code&gt;crt.sh&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;For every candidate IP that is &lt;strong&gt;not&lt;/strong&gt; an edge IP and &lt;strong&gt;not&lt;/strong&gt; the front door, make a &lt;strong&gt;direct request to that IP while presenting the real hostname&lt;/strong&gt;, deliberately bypassing DNS and the CDN.&lt;/li&gt;
&lt;li&gt;If the origin answers with your site, that is &lt;code&gt;EXPOSED&lt;/code&gt;. If it refuses, &lt;code&gt;CONTAINED&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interesting part is step 4. To test reachability you force a TLS connection to a specific IP but set the SNI/Host to the real domain, so the origin thinks it is a normal request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Force TLS to a specific IP while presenting the real hostname (SNI),&lt;/span&gt;
&lt;span class="c1"&gt;// i.e. deliberately bypass DNS/CDN. Cert validity is ignored on purpose:&lt;/span&gt;
&lt;span class="c1"&gt;// we are probing reachability, not trust.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;connector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;native_tls&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;TlsConnector&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.danger_accept_invalid_certs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.danger_accept_invalid_hostnames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;TcpStream&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&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="c1"&gt;// domain = SNI, ip = where we actually connect&lt;/span&gt;
&lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="nf"&gt;.write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET / HTTP/1.1&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Host: {domain}&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Connection: close&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the bytes that come back match the baseline fingerprint, that IP is serving your real site directly. Caught.&lt;/p&gt;

&lt;p&gt;It also handles the obvious false positive: if &lt;em&gt;every&lt;/em&gt; candidate IP returns byte-identical content, you are on an anycast host platform (Vercel, Netlify, Cloudflare Pages) where the host &lt;em&gt;is&lt;/em&gt; the edge, there is no separate origin to expose, and it says so instead of crying wolf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; example.com
&lt;span class="c"&gt;# or build once, then:&lt;/span&gt;
./target/release/origin-exposure-check example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;CONTAINED&lt;/code&gt; run looks calm. An &lt;code&gt;EXPOSED&lt;/code&gt; run hands you the IPs and the fix:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix: restrict the origin firewall to the CDN's published ranges (or use a private tunnel so there is no public origin IP), then re-run this check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the actual remediation: your origin should only accept connections from your CDN's IP ranges, or have no public inbound listener at all (a tunnel). Allow the world, and the CDN is decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  One important rule
&lt;/h2&gt;

&lt;p&gt;Run it only against domains and infrastructure &lt;strong&gt;you own or are explicitly authorized to test&lt;/strong&gt;. It is a self-audit, and it only consumes already-public data, but point it at your own stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code + the deeper write-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tool (MIT, Rust): &lt;strong&gt;&lt;a href="https://github.com/blackneuron-security/origin-exposure-check" rel="noopener noreferrer"&gt;https://github.com/blackneuron-security/origin-exposure-check&lt;/a&gt;&lt;/strong&gt; (a star is appreciated if it is useful to you ⭐)&lt;/li&gt;
&lt;li&gt;The longer write-up on the mechanism and the fixes: &lt;strong&gt;&lt;a href="https://blackneuron.ai/blog/origin-ip-exposure-cdn-bypass" rel="noopener noreferrer"&gt;https://blackneuron.ai/blog/origin-ip-exposure-cdn-bypass&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run it on your own site and get a surprising &lt;code&gt;EXPOSED&lt;/code&gt;, I would genuinely like to hear how the IP leaked. That is usually the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written while building defensive tooling at &lt;a href="https://blackneuron.ai" rel="noopener noreferrer"&gt;BlackNeuron&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infrastructure</category>
      <category>networking</category>
      <category>security</category>
    </item>
  </channel>
</rss>
