<?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: North Dev</title>
    <description>The latest articles on DEV Community by North Dev (@northwebdev).</description>
    <link>https://dev.to/northwebdev</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%2F4208824%2F9936f601-c5d0-49fc-98e0-2022681222a0.jpg</url>
      <title>DEV Community: North Dev</title>
      <link>https://dev.to/northwebdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/northwebdev"/>
    <language>en</language>
    <item>
      <title>Stop AI scrapers without blocking Google: verified crawlers + a tarpit</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:14:29 +0000</pubDate>
      <link>https://dev.to/northwebdev/stop-ai-scrapers-without-blocking-google-verified-crawlers-a-tarpit-3oeh</link>
      <guid>https://dev.to/northwebdev/stop-ai-scrapers-without-blocking-google-verified-crawlers-a-tarpit-3oeh</guid>
      <description>&lt;p&gt;AI crawlers are hammering your site, ignoring &lt;code&gt;robots.txt&lt;/code&gt;, and running up your bandwidth. The tempting fix — block by User-Agent — also blocks the real Googlebot you &lt;em&gt;want&lt;/em&gt;, because anyone can claim any UA. Here's the approach that separates the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  User-Agent is a claim, not an identity
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;User-Agent: Googlebot&lt;/code&gt; means nothing — a scraper sets it in one line. So a UA allowlist lets impostors in, and a UA blocklist eventually catches a real crawler you cared about. You need to &lt;em&gt;verify&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forward-confirmed reverse DNS
&lt;/h2&gt;

&lt;p&gt;The one check that actually works for the big crawlers: &lt;strong&gt;FCrDNS&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the client IP, do a reverse DNS (PTR) lookup → a hostname.&lt;/li&gt;
&lt;li&gt;Check the hostname ends in the crawler's domain (&lt;code&gt;.googlebot.com&lt;/code&gt;, &lt;code&gt;.search.msn.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Resolve that hostname forward (A/AAAA) and confirm it maps back to the original IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spoofer can set a UA but can't make &lt;code&gt;66.249.x.x&lt;/code&gt; reverse to &lt;code&gt;crawl-*.googlebot.com&lt;/code&gt; and forward back. Verified crawlers pass through untouched. (Range-only crawlers like DuckDuckBot need a published IP-range list instead.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Tarpit the unverified — without hurting humans
&lt;/h2&gt;

&lt;p&gt;For traffic that fails verification &lt;em&gt;and&lt;/em&gt; ignores your rules, serve a &lt;strong&gt;tarpit&lt;/strong&gt;: a cheap, bounded maze of generated links that wastes a scraper's crawl budget. Keep it honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Advertise the trap paths as &lt;code&gt;Disallow&lt;/code&gt; in robots.txt.&lt;/strong&gt; Compliant bots self-exclude; only bots that ignore robots fall in. That's what makes it ethical rather than hostile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail open for humans.&lt;/strong&gt; A normal visitor who stumbles onto a trap path shouldn't get mazed — gate on behavior, not on being unverified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-destructive, synthetic content.&lt;/strong&gt; No real data, size-capped, rate-limited.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;This raises a scraper's cost; it isn't a wall. A determined operator rotating residential IPs defeats per-IP logic. The point is to make &lt;em&gt;your&lt;/em&gt; site not worth the trouble.&lt;/p&gt;




&lt;p&gt;I packaged the verify-good-bots + tarpit-the-rest logic into one Go binary, &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/crawlgate" rel="noopener noreferrer"&gt;crawlgate&lt;/a&gt;&lt;/strong&gt; — reverse proxy or middleware, forward-confirmed verification for Google/Bing/Yandex, rate limiting, and a non-destructive tarpit. Respects robots.txt and real crawlers.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to verify a real Googlebot (User-Agent isn't enough)</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:09:45 +0000</pubDate>
      <link>https://dev.to/northwebdev/how-to-verify-a-real-googlebot-user-agent-isnt-enough-5912</link>
      <guid>https://dev.to/northwebdev/how-to-verify-a-real-googlebot-user-agent-isnt-enough-5912</guid>
      <description>&lt;p&gt;Your access log is full of &lt;code&gt;Googlebot&lt;/code&gt;. Some of it is Google. A lot of it isn't. Here's how to tell them apart — because the User-Agent string is worthless for this, and blocking the fakes the wrong way will deindex you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The User-Agent is a claim anyone can make
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)&lt;/code&gt; is a header. A scraper sets it in one line to inherit Googlebot's reputation (and your allowlist). So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;UA allowlist&lt;/strong&gt; lets every impostor straight in.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;UA blocklist&lt;/strong&gt; eventually catches the &lt;em&gt;real&lt;/em&gt; Googlebot — and now you're not in the index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You have to verify identity, not read a label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forward-confirmed reverse DNS (the method Google itself documents)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Take the client IP. Do a &lt;strong&gt;reverse DNS (PTR)&lt;/strong&gt; lookup → a hostname.&lt;/li&gt;
&lt;li&gt;Check the hostname is under Google's domain: it must end in &lt;strong&gt;&lt;code&gt;.googlebot.com&lt;/code&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;code&gt;.google.com&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Resolve that hostname &lt;strong&gt;forward&lt;/strong&gt; (A/AAAA) and confirm it resolves &lt;strong&gt;back to the original IP&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three must hold. A spoofer can forge the UA but can't make &lt;code&gt;66.249.66.1&lt;/code&gt; reverse to &lt;code&gt;crawl-*.googlebot.com&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; forward-resolve back to &lt;code&gt;66.249.66.1&lt;/code&gt;.&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;# client IP 66.249.66.1&lt;/span&gt;
dig &lt;span class="nt"&gt;-x&lt;/span&gt; 66.249.66.1 +short        &lt;span class="c"&gt;# -&amp;gt; crawl-66-249-66-1.googlebot.com.&lt;/span&gt;
dig crawl-66-249-66-1.googlebot.com +short   &lt;span class="c"&gt;# -&amp;gt; 66.249.66.1  ✓ match&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suffix check in step 2 is not optional — a PTR record is controlled by whoever owns the IP's reverse zone, so &lt;code&gt;evil-googlebot.com.attacker.net&lt;/code&gt; or a lookalike must fail. Confirm the dot-bounded suffix, then forward-confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bing, and the range-only crawlers
&lt;/h2&gt;

&lt;p&gt;Bing (&lt;code&gt;.search.msn.com&lt;/code&gt;), Apple, and Yandex support the same FCrDNS check. Some crawlers (e.g. DuckDuckBot) don't publish reverse DNS and instead publish an &lt;strong&gt;IP range list&lt;/strong&gt; — for those, verify the IP is in the official, published ranges, and refresh that list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't punish humans on the way
&lt;/h2&gt;

&lt;p&gt;Once you can verify good bots, the temptation is to tarpit or block everything unverified. Keep it honest: a real person hitting your site isn't a verified crawler either. Gate on behavior (ignoring robots, abusive rate) — not on "failed bot verification" — so you slow abusive scrapers without breaking normal visitors.&lt;/p&gt;




&lt;p&gt;I put forward-confirmed verification (Google/Bing/Yandex/Apple), rate limiting, and an ethical tarpit for the unverified-and-abusive into one Go binary, &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/crawlgate" rel="noopener noreferrer"&gt;crawlgate&lt;/a&gt;&lt;/strong&gt; — reverse proxy or middleware, respects robots.txt and real crawlers.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The nginx security headers checklist (2026), and how to verify them</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:09:08 +0000</pubDate>
      <link>https://dev.to/northwebdev/the-nginx-security-headers-checklist-2026-and-how-to-verify-them-88d</link>
      <guid>https://dev.to/northwebdev/the-nginx-security-headers-checklist-2026-and-how-to-verify-them-88d</guid>
      <description>&lt;p&gt;Most nginx configs are one &lt;code&gt;add_header&lt;/code&gt; away from leaking more than they should. Here's a 2026 security-header checklist you can actually verify — plus the inheritance gotcha that silently disables half of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headers that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict-Transport-Security&lt;/strong&gt; — force HTTPS for future visits. &lt;code&gt;max-age=63072000; includeSubDomains; preload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Content-Type-Options: nosniff&lt;/strong&gt; — stop MIME sniffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content-Security-Policy&lt;/strong&gt; — the big one. Even a minimal &lt;code&gt;frame-ancestors 'self'&lt;/code&gt; (clickjacking) and a scoped &lt;code&gt;default-src&lt;/code&gt; beats nothing. Build it per app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referrer-Policy: strict-origin-when-cross-origin&lt;/strong&gt; — stop leaking full URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Frame-Options: SAMEORIGIN&lt;/strong&gt; — legacy clickjacking guard (CSP &lt;code&gt;frame-ancestors&lt;/code&gt; supersedes it, but harmless to keep).&lt;/li&gt;
&lt;li&gt;Drop &lt;strong&gt;&lt;code&gt;server_tokens off&lt;/code&gt;&lt;/strong&gt; to stop advertising your exact nginx version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The gotcha that breaks everything
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;add_header&lt;/code&gt; &lt;strong&gt;does not inherit&lt;/strong&gt; into a block that defines its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;add_header&lt;/code&gt;. If you set security headers at &lt;code&gt;server&lt;/code&gt; level and then add a single &lt;code&gt;add_header&lt;/code&gt; inside a &lt;code&gt;location&lt;/code&gt; (say, a cache header), &lt;strong&gt;every security header vanishes for that location&lt;/strong&gt; — silently, no error.&lt;/p&gt;

&lt;p&gt;Fix: define headers once at the broadest scope and don't re-declare &lt;code&gt;add_header&lt;/code&gt; in child blocks, or repeat them (an &lt;code&gt;include&lt;/code&gt; snippet of headers is the clean pattern).&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't serve your dotfiles
&lt;/h2&gt;

&lt;p&gt;A classic exposure — &lt;code&gt;.git/&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.htpasswd&lt;/code&gt; reachable over HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&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;
  
  
  Verify, don't assume
&lt;/h2&gt;

&lt;p&gt;Check what you actually send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'strict-transport|content-security|x-content-type|referrer-policy'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a header you configured isn't there, you probably hit the &lt;code&gt;add_header&lt;/code&gt; inheritance trap.&lt;/p&gt;




&lt;p&gt;I put these rules (plus TLS protocol/cipher and &lt;code&gt;server_name&lt;/code&gt; checks) into &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/nginx-doctor" rel="noopener noreferrer"&gt;nginx-doctor&lt;/a&gt;&lt;/strong&gt; — point it at your config and it prints each finding with the fix. Zero-config Go binary, read-only.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop AI scrapers without blocking Google: verified crawlers + a tarpit</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:01:44 +0000</pubDate>
      <link>https://dev.to/northwebdev/stop-ai-scrapers-without-blocking-google-verified-crawlers-a-tarpit-5em</link>
      <guid>https://dev.to/northwebdev/stop-ai-scrapers-without-blocking-google-verified-crawlers-a-tarpit-5em</guid>
      <description>&lt;p&gt;AI crawlers are hammering your site, ignoring &lt;code&gt;robots.txt&lt;/code&gt;, and running up your bandwidth. The tempting fix — block by User-Agent — also blocks the real Googlebot you &lt;em&gt;want&lt;/em&gt;, because anyone can claim any UA. Here's the approach that separates the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  User-Agent is a claim, not an identity
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;User-Agent: Googlebot&lt;/code&gt; means nothing — a scraper sets it in one line. So a UA allowlist lets impostors in, and a UA blocklist eventually catches a real crawler you cared about. You need to &lt;em&gt;verify&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forward-confirmed reverse DNS
&lt;/h2&gt;

&lt;p&gt;The one check that actually works for the big crawlers: &lt;strong&gt;FCrDNS&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the client IP, do a reverse DNS (PTR) lookup → a hostname.&lt;/li&gt;
&lt;li&gt;Check the hostname ends in the crawler's domain (&lt;code&gt;.googlebot.com&lt;/code&gt;, &lt;code&gt;.search.msn.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Resolve that hostname forward (A/AAAA) and confirm it maps back to the original IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spoofer can set a UA but can't make &lt;code&gt;66.249.x.x&lt;/code&gt; reverse to &lt;code&gt;crawl-*.googlebot.com&lt;/code&gt; and forward back. Verified crawlers pass through untouched. (Range-only crawlers like DuckDuckBot need a published IP-range list instead.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Tarpit the unverified — without hurting humans
&lt;/h2&gt;

&lt;p&gt;For traffic that fails verification &lt;em&gt;and&lt;/em&gt; ignores your rules, serve a &lt;strong&gt;tarpit&lt;/strong&gt;: a cheap, bounded maze of generated links that wastes a scraper's crawl budget. Keep it honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Advertise the trap paths as &lt;code&gt;Disallow&lt;/code&gt; in robots.txt.&lt;/strong&gt; Compliant bots self-exclude; only bots that ignore robots fall in. That's what makes it ethical rather than hostile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail open for humans.&lt;/strong&gt; A normal visitor who stumbles onto a trap path shouldn't get mazed — gate on behavior, not on being unverified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-destructive, synthetic content.&lt;/strong&gt; No real data, size-capped, rate-limited.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;This raises a scraper's cost; it isn't a wall. A determined operator rotating residential IPs defeats per-IP logic. The point is to make &lt;em&gt;your&lt;/em&gt; site not worth the trouble.&lt;/p&gt;




&lt;p&gt;I packaged the verify-good-bots + tarpit-the-rest logic into one Go binary, &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/crawlgate" rel="noopener noreferrer"&gt;crawlgate&lt;/a&gt;&lt;/strong&gt; — reverse proxy or middleware, forward-confirmed verification for Google/Bing/Yandex, rate limiting, and a non-destructive tarpit. Respects robots.txt and real crawlers.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>The nginx security headers checklist (2026), and how to verify them</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:00:01 +0000</pubDate>
      <link>https://dev.to/northwebdev/the-nginx-security-headers-checklist-2026-and-how-to-verify-them-333a</link>
      <guid>https://dev.to/northwebdev/the-nginx-security-headers-checklist-2026-and-how-to-verify-them-333a</guid>
      <description>&lt;p&gt;Most nginx configs are one &lt;code&gt;add_header&lt;/code&gt; away from leaking more than they should. Here's a 2026 security-header checklist you can actually verify — plus the inheritance gotcha that silently disables half of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headers that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict-Transport-Security&lt;/strong&gt; — force HTTPS for future visits. &lt;code&gt;max-age=63072000; includeSubDomains; preload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Content-Type-Options: nosniff&lt;/strong&gt; — stop MIME sniffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content-Security-Policy&lt;/strong&gt; — the big one. Even a minimal &lt;code&gt;frame-ancestors 'self'&lt;/code&gt; (clickjacking) and a scoped &lt;code&gt;default-src&lt;/code&gt; beats nothing. Build it per app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referrer-Policy: strict-origin-when-cross-origin&lt;/strong&gt; — stop leaking full URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Frame-Options: SAMEORIGIN&lt;/strong&gt; — legacy clickjacking guard (CSP &lt;code&gt;frame-ancestors&lt;/code&gt; supersedes it, but harmless to keep).&lt;/li&gt;
&lt;li&gt;Drop &lt;strong&gt;&lt;code&gt;server_tokens off&lt;/code&gt;&lt;/strong&gt; to stop advertising your exact nginx version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The gotcha that breaks everything
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;add_header&lt;/code&gt; &lt;strong&gt;does not inherit&lt;/strong&gt; into a block that defines its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;add_header&lt;/code&gt;. If you set security headers at &lt;code&gt;server&lt;/code&gt; level and then add a single &lt;code&gt;add_header&lt;/code&gt; inside a &lt;code&gt;location&lt;/code&gt; (say, a cache header), &lt;strong&gt;every security header vanishes for that location&lt;/strong&gt; — silently, no error.&lt;/p&gt;

&lt;p&gt;Fix: define headers once at the broadest scope and don't re-declare &lt;code&gt;add_header&lt;/code&gt; in child blocks, or repeat them (an &lt;code&gt;include&lt;/code&gt; snippet of headers is the clean pattern).&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't serve your dotfiles
&lt;/h2&gt;

&lt;p&gt;A classic exposure — &lt;code&gt;.git/&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.htpasswd&lt;/code&gt; reachable over HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&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;
  
  
  Verify, don't assume
&lt;/h2&gt;

&lt;p&gt;Check what you actually send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'strict-transport|content-security|x-content-type|referrer-policy'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a header you configured isn't there, you probably hit the &lt;code&gt;add_header&lt;/code&gt; inheritance trap.&lt;/p&gt;




&lt;p&gt;I put these rules (plus TLS protocol/cipher and &lt;code&gt;server_name&lt;/code&gt; checks) into &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/nginx-doctor" rel="noopener noreferrer"&gt;nginx-doctor&lt;/a&gt;&lt;/strong&gt; — point it at your config and it prints each finding with the fix. Zero-config Go binary, read-only.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why your PHP-FPM throws 502 under load (pm.max_children, explained)</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:54:54 +0000</pubDate>
      <link>https://dev.to/northwebdev/why-your-php-fpm-throws-502-under-load-pmmaxchildren-explained-4ckl</link>
      <guid>https://dev.to/northwebdev/why-your-php-fpm-throws-502-under-load-pmmaxchildren-explained-4ckl</guid>
      <description>&lt;p&gt;Your PHP site runs fine, then traffic spikes and nginx starts throwing &lt;strong&gt;502 Bad Gateway&lt;/strong&gt;. Logs blame the upstream. The cause is almost always one number in your PHP-FPM pool: &lt;code&gt;pm.max_children&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens
&lt;/h2&gt;

&lt;p&gt;Nginx hands PHP requests to PHP-FPM. FPM has a fixed ceiling of worker processes — &lt;code&gt;pm.max_children&lt;/code&gt;. When every worker is busy and the listen backlog fills, FPM can't accept the connection, nginx's &lt;code&gt;fastcgi_pass&lt;/code&gt; fails, and the user gets a &lt;strong&gt;502&lt;/strong&gt;. Under &lt;code&gt;pm = dynamic&lt;/code&gt;, you'll also see this in the FPM status page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max children reached: 37
listen queue: 120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That counter is the smoking gun: you've hit the ceiling 37 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to size max_children (stop guessing)
&lt;/h2&gt;

&lt;p&gt;The honest formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max_children = (RAM budget for FPM) / (average worker RSS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Measure a real worker's resident memory (RSS) under your actual app, not &lt;code&gt;memory_limit&lt;/code&gt; (that's a cap, not usage).&lt;/li&gt;
&lt;li&gt;Pick a RAM budget — total RAM minus what the DB, cache, and OS need. Leave headroom.&lt;/li&gt;
&lt;li&gt;Divide. That's your ceiling. Set &lt;code&gt;pm.max_children&lt;/code&gt; to it, not to a number you copied from a blog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Too &lt;strong&gt;high&lt;/strong&gt; and a traffic spike spawns more workers than RAM allows → the OOM killer starts reaping processes (worse than a 502). Too &lt;strong&gt;low&lt;/strong&gt; and you 502 under load you could have served.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared-pool 502 storm
&lt;/h2&gt;

&lt;p&gt;The nastiest version: multiple sites share one FPM master and one &lt;code&gt;max_children&lt;/code&gt;. One busy site starves the others, and they &lt;em&gt;all&lt;/em&gt; 502 — even the idle ones. Fix: give each site its own pool with its own ceiling, sized from its own RSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also worth setting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pm.max_requests&lt;/code&gt; (recycle workers to bound memory leaks).&lt;/li&gt;
&lt;li&gt;Watch &lt;code&gt;slow requests&lt;/code&gt; in the status page and the &lt;strong&gt;slowlog&lt;/strong&gt; — a few slow endpoints hold workers hostage and fake a capacity problem.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I turned this into a read-only CLI, &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/fpm-doctor" rel="noopener noreferrer"&gt;fpm-doctor&lt;/a&gt;&lt;/strong&gt; — it reads your pool configs + live status, measures real worker RSS, and recommends &lt;code&gt;max_children&lt;/code&gt; from your actual memory budget, flags the shared-pool pattern, and ranks your slowlog. Composer + &lt;code&gt;.phar&lt;/code&gt;, never writes your config.&lt;/p&gt;

</description>
      <category>php</category>
      <category>devops</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>SPF "too many DNS lookups" (PermError): what it means and how to fix it</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:54:18 +0000</pubDate>
      <link>https://dev.to/northwebdev/spf-too-many-dns-lookups-permerror-what-it-means-and-how-to-fix-it-1fbh</link>
      <guid>https://dev.to/northwebdev/spf-too-many-dns-lookups-permerror-what-it-means-and-how-to-fix-it-1fbh</guid>
      <description>&lt;p&gt;Your mail quietly started landing in spam, and when you checked SPF you found this: &lt;code&gt;PermError: too many DNS lookups&lt;/code&gt;. Here's exactly what that means and how to fix it without guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule nobody tells you about
&lt;/h2&gt;

&lt;p&gt;SPF (RFC 7208) caps the number of DNS-lookup-causing terms in your record at &lt;strong&gt;10&lt;/strong&gt;. Not 10 records — 10 &lt;em&gt;lookups&lt;/em&gt;, counted recursively. Blow past it and a correct receiver returns &lt;strong&gt;PermError&lt;/strong&gt;, which many treat as "SPF failed" — so your legitimate mail gets downgraded or junked.&lt;/p&gt;

&lt;p&gt;What costs a lookup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;include:&lt;/code&gt; — 1 each, &lt;strong&gt;and you recurse into it&lt;/strong&gt; (its own includes count too).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt; — 1 each.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirect=&lt;/code&gt; — 1 (when there's no &lt;code&gt;all&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ip4:&lt;/code&gt; / &lt;code&gt;ip6:&lt;/code&gt; / &lt;code&gt;all&lt;/code&gt; — &lt;strong&gt;0&lt;/strong&gt;. These are free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trap is &lt;code&gt;include:&lt;/code&gt;. One &lt;code&gt;include:_spf.google.com&lt;/code&gt; can itself expand into several more includes. Add a few SaaS senders (&lt;code&gt;include:mailgun.org&lt;/code&gt;, &lt;code&gt;include:servers.mcsv.net&lt;/code&gt;, &lt;code&gt;include:_spf.salesforce.com&lt;/code&gt;…) and you silently cross 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose it
&lt;/h2&gt;

&lt;p&gt;Resolve your TXT record and walk the includes, adding up the lookup-causing terms across the whole tree. If the total is &amp;gt;10, that's your bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT example.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then follow every &lt;code&gt;include:&lt;/code&gt;/&lt;code&gt;redirect=&lt;/code&gt; and count. (This is tedious by hand, which is the point — tooling should do it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove senders you no longer use.&lt;/strong&gt; Old ESPs linger in SPF for years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flatten&lt;/strong&gt; a stable include into its &lt;code&gt;ip4:&lt;/code&gt;/&lt;code&gt;ip6:&lt;/code&gt; ranges (free lookups) — but only for senders whose IPs rarely change, and re-check periodically, or you'll authorize stale IPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegate by subdomain.&lt;/strong&gt; Send bulk/marketing from &lt;code&gt;mail.example.com&lt;/code&gt; with its own SPF, keeping your root record lean.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;-all&lt;/code&gt; (hard fail) once you're confident; &lt;code&gt;~all&lt;/code&gt; (soft fail) while testing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Don't stop at SPF
&lt;/h2&gt;

&lt;p&gt;SPF alone doesn't survive forwarding. Pair it with &lt;strong&gt;DKIM&lt;/strong&gt; (a signature that survives relays) and a &lt;strong&gt;DMARC&lt;/strong&gt; policy that moves from &lt;code&gt;p=none&lt;/code&gt; (monitoring) to &lt;code&gt;p=quarantine&lt;/code&gt;/&lt;code&gt;p=reject&lt;/code&gt; once your reports are clean. All three is what actually keeps you out of spam.&lt;/p&gt;




&lt;p&gt;I bundled the SPF lookup-counter (and DKIM/DMARC/TLS checks) into a zero-config CLI, &lt;strong&gt;&lt;a href="https://github.com/North-web-dev/domain-doctor" rel="noopener noreferrer"&gt;domain-doctor&lt;/a&gt;&lt;/strong&gt; — &lt;code&gt;domain-doctor example.com&lt;/code&gt; prints a graded report with the fix for each problem. MIT, single Go binary.&lt;/p&gt;

</description>
      <category>email</category>
      <category>dns</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why Chrome's JA3 is useless now (and what to use instead)</title>
      <dc:creator>North Dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:39:48 +0000</pubDate>
      <link>https://dev.to/northwebdev/why-chromes-ja3-is-useless-now-and-what-to-use-instead-1o8o</link>
      <guid>https://dev.to/northwebdev/why-chromes-ja3-is-useless-now-and-what-to-use-instead-1o8o</guid>
      <description>&lt;p&gt;If you've ever tried to make an HTTP client "look like Chrome" to get past a WAF, you've probably copied a Chrome JA3 hash from somewhere, hard-coded it, verified it once against a reflector, and shipped. And then, weeks later, you got blocked anyway — with the exact fingerprint you "matched."&lt;/p&gt;

&lt;p&gt;Here's what happened, and what to do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your TLS handshake talks before your HTTP does
&lt;/h2&gt;

&lt;p&gt;Before a single byte of your HTTP request goes out, your TLS &lt;strong&gt;ClientHello&lt;/strong&gt; is already on the wire: the version you offer, your cipher list, your extensions, your curves and signature algorithms. A CDN reads that and forms a fingerprint. If it says "Go stdlib" or "python-requests" instead of "Chrome," you can send the most perfect browser headers in the world and still eat a 403.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JA3&lt;/strong&gt; (Salesforce, 2017): an MD5 over five ClientHello fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JA4&lt;/strong&gt; (FoxIO, the modern successor): a structured, human-readable fingerprint — and it &lt;strong&gt;sorts&lt;/strong&gt; the extension list before hashing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sorting is the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome's JA3 is non-deterministic on purpose
&lt;/h2&gt;

&lt;p&gt;Since Chrome 110 (2023), Chrome &lt;strong&gt;randomizes its TLS extension order on every connection&lt;/strong&gt; — a deliberate anti-ossification measure. The side effect: Chrome's &lt;strong&gt;JA3 changes every connection.&lt;/strong&gt; There is no "the" Chrome JA3. Hard-code one and you've pinned a single permutation real Chrome almost never reproduces — so you look &lt;em&gt;less&lt;/em&gt; like Chrome.&lt;/p&gt;

&lt;p&gt;Same client, three consecutive handshakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run 1: JA3 0a9d85cc...   JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd
run 2: JA3 b7ca61f3...   JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd
run 3: JA3 6910cc00...   JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JA3 is all over the place. &lt;strong&gt;JA4 is constant&lt;/strong&gt; — the sort washes out the shuffle. Match JA4, not JA3. (Firefox/Safari/iOS don't shuffle the same way, so their JA3 stays stable — this is a Chrome/Chromium thing.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The third fingerprint most people miss
&lt;/h2&gt;

&lt;p&gt;Even with byte-perfect TLS, you get flagged by your &lt;strong&gt;HTTP/2 fingerprint&lt;/strong&gt; (the "Akamai h2" string): SETTINGS values, WINDOW_UPDATE, stream priority, and pseudo-header order. Most HTTP libraries never think about frame order, so this is where "I set the right ciphers" setups quietly fail. The real checklist is three numbers: &lt;strong&gt;JA4&lt;/strong&gt; (TLS), &lt;strong&gt;Akamai h2&lt;/strong&gt; (HTTP/2), and matching headers — not just a User-Agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching them in practice
&lt;/h2&gt;

&lt;p&gt;I got tired of re-solving this from both ends, so I put it in one small Go binary, &lt;strong&gt;fp&lt;/strong&gt; (&lt;a href="https://github.com/North-web-dev/fpkit" rel="noopener noreferrer"&gt;fpkit&lt;/a&gt;, MIT):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fp get &amp;lt;url&amp;gt; --profile chrome&lt;/code&gt; — fetch with a byte-exact Chrome ClientHello + HTTP/2 settings. No headless browser, no CGo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fp inspect&lt;/code&gt; — a reflector showing a caller's JA3/JA4/JA4H/Akamai-h2 with a plain-English verdict.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fp check &amp;lt;url&amp;gt; --profile chrome&lt;/code&gt; — a CI guard that fails when your client stops looking like a browser.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fp db ja4 &amp;lt;value&amp;gt;&lt;/code&gt; — reverse-look-up a JA4 against bundled browser + known-client fingerprints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try the reflector with no install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--http2&lt;/span&gt; https://fp.shshbshsnang.shop/api/all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or open &lt;a href="https://fp.shshbshsnang.shop/" rel="noopener noreferrer"&gt;https://fp.shshbshsnang.shop/&lt;/a&gt; and it'll tell you whether you read as a browser or a bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't hard-code a Chrome JA3 — it's non-deterministic by design. Match &lt;strong&gt;JA4&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;TLS isn't enough; match the &lt;strong&gt;HTTP/2 (Akamai)&lt;/strong&gt; fingerprint too.&lt;/li&gt;
&lt;li&gt;Measure against a reflector, and re-measure — fingerprints drift with every browser release.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Honest limit:&lt;/strong&gt; a careful spoofer can forge any JA4, so an inbound JA4 guard raises cost, it isn't a wall. If you've seen a profile drift, or a CDN keying off something I haven't documented, I'd like to hear it.&lt;/p&gt;

</description>
      <category>tls</category>
      <category>go</category>
      <category>webscraping</category>
      <category>security</category>
    </item>
  </channel>
</rss>
