<?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: Daanial Farrukh</title>
    <description>The latest articles on DEV Community by Daanial Farrukh (@daanial).</description>
    <link>https://dev.to/daanial</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%2F3966946%2F9807d012-5e5a-4dc5-8326-fc594b56bc62.png</url>
      <title>DEV Community: Daanial Farrukh</title>
      <link>https://dev.to/daanial</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daanial"/>
    <language>en</language>
    <item>
      <title>Cloudflare Free Plan Security — The Exact Settings I Run on My Own Site</title>
      <dc:creator>Daanial Farrukh</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:15:39 +0000</pubDate>
      <link>https://dev.to/daanial/cloudflare-free-plan-security-the-exact-settings-i-run-on-my-own-site-171o</link>
      <guid>https://dev.to/daanial/cloudflare-free-plan-security-the-exact-settings-i-run-on-my-own-site-171o</guid>
      <description>&lt;p&gt;&lt;em&gt;A field note from my own WordPress site on Cloudflare’s free plan — what I switched on, what I left off on purpose, and the curl checks that prove each setting is really working.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spend a lot of time in customer rooms asking one question about Cloudflare: “Which settings are actually on?”&lt;/p&gt;

&lt;p&gt;Most of the time the answer is a shrug and a logo. “We’re behind Cloudflare.”&lt;/p&gt;

&lt;p&gt;So I owe the same honesty about my own site.&lt;/p&gt;

&lt;p&gt;This blog runs WordPress on shared hosting, behind Cloudflare, on the &lt;strong&gt;Free plan&lt;/strong&gt;. No Pro, no Business, no add-ons. In July a WPScan report on my own site pushed me to do the full pass I normally do for customers.&lt;/p&gt;

&lt;p&gt;I expected the free plan to run out quickly. It didn’t. It ran out in exactly two places — and I’ll show you both.&lt;/p&gt;

&lt;p&gt;Everything below is a real screenshot from my own dashboard, taken today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap — “We’re Behind Cloudflare” Is Not a Setting
&lt;/h2&gt;

&lt;p&gt;That’s the trap. Turning the orange cloud on gives you a proxy, a free certificate and DDoS protection at the edge. It does &lt;strong&gt;not&lt;/strong&gt; give you a strict TLS chain, a WAF policy, security headers or any rule that knows what WordPress is.&lt;/p&gt;

&lt;p&gt;Those are switches. Somebody has to flip them, and somebody has to prove they work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being behind Cloudflare is a starting point, not a security posture.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Free Plan Actually Gives You
&lt;/h2&gt;

&lt;p&gt;Here’s the flow every request takes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Visitor → Cloudflare edge (TLS, WAF, rate limit, headers) → Origin web server → WordPress&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;On the free plan, the parts you control at the edge are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSL/TLS&lt;/strong&gt; — the encryption mode between Cloudflare and your origin, minimum TLS version, HSTS and HTTPS redirects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security rules&lt;/strong&gt; — up to &lt;strong&gt;5 custom WAF rules&lt;/strong&gt; and &lt;strong&gt;1 rate-limiting rule&lt;/strong&gt;, plus Cloudflare’s managed free ruleset and DDoS protection, which are on by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transform rules&lt;/strong&gt; — add security headers to every response and strip the headers that leak your stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five custom rules sounds tiny. It’s enough — if every rule earns its slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Usual Check Misses It
&lt;/h2&gt;

&lt;p&gt;The usual check is opening the site in a browser and seeing the padlock. The padlock only proves the visitor-to-Cloudflare leg. It says nothing about the leg from Cloudflare to your server, nothing about whether &lt;code&gt;/xmlrpc.php&lt;/code&gt; is reachable, and nothing about which headers leave your origin.&lt;/p&gt;

&lt;p&gt;Here’s the part that trips people up: Cloudflare will happily serve a green padlock in &lt;strong&gt;Flexible&lt;/strong&gt; mode while the traffic to your origin is plain HTTP. The dashboard looks healthy. The chain is not.&lt;/p&gt;

&lt;p&gt;So every step below ends with a command, not a screenshot. &lt;strong&gt;The screenshot shows intent. The command shows the truth.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Set It Up — Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: SSL/TLS mode — Full (Strict), nothing less
&lt;/h3&gt;

&lt;p&gt;Cloudflare → your domain → SSL/TLS → Overview → Configure → &lt;strong&gt;Full (strict)&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20vsrxhjn3xdfy1zm4t8.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%2F20vsrxhjn3xdfy1zm4t8.png" alt="The SSL/TLS overview on my free-plan zone: current encryption mode Full (strict), browser to Cloudflare to origin, both legs encrypted." width="800" height="349"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The SSL/TLS overview on my free-plan zone: current encryption mode Full (strict), browser to Cloudflare to origin, both legs encrypted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Full (Strict) means Cloudflare encrypts the leg to your origin &lt;strong&gt;and&lt;/strong&gt; checks that the origin certificate is valid. If your host has no valid certificate, a free Cloudflare Origin Certificate is enough — SSL/TLS → Origin Server → Create Certificate. My zone has one for the domain and its wildcard, valid until 2041.&lt;/p&gt;

&lt;p&gt;Mine was already on Full (Strict). That was the one thing I didn’t have to fix.&lt;/p&gt;

&lt;p&gt;Plain takeaway: if your mode says Flexible, fix that before you touch anything else on this page.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: HTTPS everywhere, TLS 1.2 minimum, HSTS without preload
&lt;/h3&gt;

&lt;p&gt;SSL/TLS → Edge Certificates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always Use HTTPS&lt;/strong&gt; — on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum TLS Version&lt;/strong&gt; — TLS 1.2. Mine was still on &lt;strong&gt;TLS 1.0&lt;/strong&gt;, the old default. That was a real finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS 1.3&lt;/strong&gt; — on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS&lt;/strong&gt; — on, max-age 6 months, include subdomains on, no-sniff on, &lt;strong&gt;preload off&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Friqnjz7h74ym6vnstqk4.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%2Friqnjz7h74ym6vnstqk4.png" alt="Edge Certificates: HSTS on with a 6-month max-age and subdomains included, preload off, and the minimum TLS version set to 1.2." width="800" height="356"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Edge Certificates: HSTS on with a 6-month max-age and subdomains included, preload off, and the minimum TLS version set to 1.2.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Preload is off on purpose. Once a domain is on the browser preload list, getting it off takes months. I want HSTS to stay reversible while the site is still changing hosts and subdomains.&lt;/p&gt;

&lt;p&gt;Plain takeaway: change minimum TLS to 1.2 today. Turn on HSTS only after every subdomain works on HTTPS.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Five WAF custom rules — every slot earns its place
&lt;/h3&gt;

&lt;p&gt;Security → Security rules → Custom rules → Create rule. The free plan gives you five. I used all five.&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%2F27w5rj2y1rwvvmdu6327.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%2F27w5rj2y1rwvvmdu6327.png" alt="Security rules on the free plan: five of five custom rules in use, all set to Block." width="799" height="316"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Security rules on the free plan: five of five custom rules in use, all set to Block.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Block XML-RPC.&lt;/strong&gt; Path equals &lt;code&gt;/xmlrpc.php&lt;/code&gt;. It is the classic pingback-DDoS and login-amplification door, and nothing on my site needs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block info-disclosure files.&lt;/strong&gt; &lt;code&gt;readme.html&lt;/code&gt;, &lt;code&gt;license.txt&lt;/code&gt;, &lt;code&gt;wp-config.php&lt;/code&gt;, the install and setup scripts, anything with &lt;code&gt;/.env&lt;/code&gt; or &lt;code&gt;/.git/&lt;/code&gt;, and files ending in &lt;code&gt;.sql&lt;/code&gt;, &lt;code&gt;.bak&lt;/code&gt; or &lt;code&gt;.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block PHP inside uploads.&lt;/strong&gt; Path starts with &lt;code&gt;/wp-content/uploads/&lt;/code&gt; and ends with &lt;code&gt;.php&lt;/code&gt;. Uploads should only ever hold images and PDFs. A PHP file there is a webshell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block known scanners by user agent.&lt;/strong&gt; wpscan, sqlmap, nikto, masscan, nuclei, wfuzz, dirbuster — matched on the lower-cased user agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block an endpoint I never asked for.&lt;/strong&gt; More on this one below — it was the most surprising finding of the whole pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The expression for rule 3, so you can copy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(starts_with(http.request.uri.path, "/wp-content/uploads/") and ends_with(http.request.uri.path, ".php"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule 4 is the weakest of the five. Any serious attacker changes the user agent. It still removes the lazy automated noise from my logs for free, so it keeps its slot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: The fifth rule — the MCP server a plugin shipped without asking
&lt;/h3&gt;

&lt;p&gt;During the same pass I found a REST route on my own site at &lt;code&gt;/wp-json/mcp/mcp-adapter-default-server&lt;/code&gt;. I had not installed it.&lt;/p&gt;

&lt;p&gt;It turned out my SEO plugin, Rank Math, bundles the official WordPress MCP adapter library and registers it on update. It needs authentication — anonymous requests get a 401 — and it pairs with the Abilities API that now sits in WordPress core. But it can execute abilities, and it accepts &lt;strong&gt;Application Passwords, which bypass 2FA&lt;/strong&gt;. There was no toggle in the plugin to turn it off.&lt;/p&gt;

&lt;p&gt;I want to be fair here: nothing was compromised. My Application Passwords list was empty at the time, so nothing had ever been authorised to use it. It was simply surface I didn’t need.&lt;/p&gt;

&lt;p&gt;So I closed it twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At Cloudflare:&lt;/strong&gt; rule 5 blocks paths starting with &lt;code&gt;/wp-json/mcp&lt;/code&gt; and any query string containing &lt;code&gt;rest_route=/mcp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At the origin:&lt;/strong&gt; a small must-use plugin removes the &lt;code&gt;mcp&lt;/code&gt; routes through WordPress’s &lt;code&gt;rest_endpoints&lt;/code&gt; filter, so the namespace is gone from &lt;code&gt;/wp-json/&lt;/code&gt; even if someone reaches the server directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check your &lt;code&gt;/wp-json/&lt;/code&gt; index for namespaces you don’t recognise.&lt;/strong&gt; Plugins add surface on update, quietly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Rate limiting — the free plan’s honest limit
&lt;/h3&gt;

&lt;p&gt;Security → Security rules → Rate limiting rules. One rule on the free plan. Mine protects the login form:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /wp-login.php&lt;/code&gt; — more than 3 requests in 10 seconds from the same IP → Block.&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%2Fud8z2njws0zpmspw5hgb.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%2Fud8z2njws0zpmspw5hgb.png" alt="The single free-plan rate-limiting rule: POST requests to wp-login.php, action Block." width="797" height="111"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The single free-plan rate-limiting rule: POST requests to wp-login.php, action Block.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s the honest part. On the free plan the counting period is locked to 10 seconds and the block lasts only 10 seconds. The API rejects anything longer as “not entitled”. A patient brute-force script that sends two guesses every ten seconds never trips it.&lt;/p&gt;

&lt;p&gt;So I treat this rule as a speed bump, not a lock. &lt;strong&gt;The real login defence lives at the origin:&lt;/strong&gt; a security plugin’s lockout after failed logins, and TOTP two-factor on the only admin account.&lt;/p&gt;

&lt;p&gt;Plain takeaway: on the free plan, rate limiting slows a burst. It does not stop a brute-force. Plan for that.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Security headers at the edge — add the good ones, strip the leaky ones
&lt;/h3&gt;

&lt;p&gt;Rules → Transform Rules → Modify Response Header. One rule, applied to every response:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adds&lt;/strong&gt; &lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, &lt;code&gt;Permissions-Policy&lt;/code&gt; and &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removes&lt;/strong&gt; &lt;code&gt;x-powered-by&lt;/code&gt;, &lt;code&gt;platform&lt;/code&gt; and &lt;code&gt;panel&lt;/code&gt; — headers my host added that advertised details of the hosting stack to anyone who asked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this at the edge means the headers are the same on cached pages and uncached pages, and I never have to edit server config on shared hosting.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 7: The switches I left off — on purpose
&lt;/h3&gt;

&lt;p&gt;This is the section most hardening guides skip.&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%2F5thaqbaftjyjjo0vxqxl.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%2F5thaqbaftjyjjo0vxqxl.png" alt="Bot Fight Mode is off on this zone, deliberately." width="800" height="286"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Bot Fight Mode is off on this zone, deliberately.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bot Fight Mode — off.&lt;/strong&gt; It challenges SEO crawlers like Ahrefs, Semrush and Moz, and uptime monitors too. This site exists to be found, so I chose targeted WAF rules over a blanket bot challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI crawlers — allowed.&lt;/strong&gt; Being cited in AI answers is traffic I want. AI Labyrinth stays off for the same reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS preload — off,&lt;/strong&gt; as explained in Step 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security level stays at Medium and Browser Integrity Check stays on. Those cost nothing in reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maximum security is not every switch on. It is every switch decided.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 8: The origin — where the free plan stops
&lt;/h3&gt;

&lt;p&gt;Cloudflare only protects traffic that goes through Cloudflare. If someone finds your origin IP, they can skip the edge completely.&lt;/p&gt;

&lt;p&gt;Two honest notes from the field:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticated Origin Pulls is off on my zone.&lt;/strong&gt; It makes Cloudflare present a client certificate to your origin, so the origin can refuse anything that isn’t Cloudflare. The switch is free. The work is on the origin web server, which has to be configured to demand that certificate — and on shared hosting that is not my server to configure. So I compensate at the origin: the security plugin’s firewall runs in its extended mode, XML-RPC is disabled there too as a backup to the edge rule, and unused themes are deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never lock admin ports to Cloudflare’s IP ranges.&lt;/strong&gt; On a customer’s cPanel server, I once locked myself out of WHM. The firewall allowed WHM only from my admin IP. I opened it through a hostname that was proxied by Cloudflare — and Cloudflare proxies the cPanel and WHM ports. The server saw a Cloudflare IP, not mine, and dropped the connection. SSH on port 22 still worked, which was the clue: Cloudflare’s normal proxy doesn’t carry it.&lt;/p&gt;

&lt;p&gt;The fix was not to allow Cloudflare’s ranges. That would open WHM to anyone who routes through Cloudflare. The fix was to reach WHM on the origin IP directly, and keep the server’s own hostname grey-cloud (DNS only). Public sites stay orange. &lt;strong&gt;Only the admin path skips Cloudflare.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Verify It — Where Most Tutorials Stop
&lt;/h2&gt;

&lt;p&gt;This step is where most tutorials stop and where mine starts. Replace &lt;code&gt;example.com&lt;/code&gt; with your domain. Every command is read-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocked paths should return 403:&lt;/strong&gt;&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 -o /dev/null -w "%{http_code}\n" https://example.com/xmlrpc.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-content/uploads/test.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-json/mcp/mcp-adapter-default-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my site all three return &lt;code&gt;403&lt;/code&gt;, and so do &lt;code&gt;/readme.html&lt;/code&gt; and &lt;code&gt;/?rest_route=/mcp&lt;/code&gt;. The home page returns &lt;code&gt;200&lt;/code&gt;. If a blocked path returns &lt;code&gt;200&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt;, the rule is not matching — check the expression, then wait 30 seconds and test again with a cache-busting query string like &lt;code&gt;?x=1&lt;/code&gt;, because rule changes take a moment to reach every edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A scanner user agent should be refused:&lt;/strong&gt;&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 -o /dev/null -w "%{http_code}\n" -A "WPScan v3.8" https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;403&lt;/code&gt;. The same URL with a normal browser user agent returns &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old TLS should fail, current TLS should pass:&lt;/strong&gt;&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 -o /dev/null -w "%{http_code}\n" --tlsv1.0 --tls-max 1.0 https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: no page at all — a handshake error, and a status of &lt;code&gt;000&lt;/code&gt;. Run it again with &lt;code&gt;--tlsv1.2 --tls-max 1.2&lt;/code&gt; and you should get &lt;code&gt;200&lt;/code&gt;. This error is not a bug. It is the verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headers should be added and leaks removed:&lt;/strong&gt;&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 -D - -o /dev/null https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;Strict-Transport-Security: max-age=15552000; includeSubDomains&lt;/code&gt;, &lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, &lt;code&gt;Permissions-Policy&lt;/code&gt; and &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt;. And look for what is &lt;strong&gt;not&lt;/strong&gt; there: no &lt;code&gt;x-powered-by&lt;/code&gt;, no &lt;code&gt;panel&lt;/code&gt;, no &lt;code&gt;platform&lt;/code&gt;. Use a real GET like this one, not &lt;code&gt;curl -I&lt;/code&gt; — a HEAD request can take a different path through your cache rules and fool you.&lt;/p&gt;

&lt;p&gt;On Windows, if curl fails with a certificate revocation error before it ever reaches the site, add &lt;code&gt;--ssl-no-revoke&lt;/code&gt;. That is a local Windows check, not a problem with your site.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free-Plan Checklist I Now Run
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encryption mode is Full (Strict).&lt;/strong&gt; SSL/TLS → Overview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum TLS is 1.2 and Always Use HTTPS is on.&lt;/strong&gt; The &lt;code&gt;--tls-max 1.0&lt;/code&gt; test fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS is on, preload is a decision.&lt;/strong&gt; &lt;code&gt;Strict-Transport-Security&lt;/code&gt; is in the response headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All five custom rules are used, each with a reason.&lt;/strong&gt; Every blocked path returns &lt;code&gt;403&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rate-limit rule exists, and you know it is only a speed bump.&lt;/strong&gt; Login lockout and 2FA live at the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers are added and leaks are stripped at the edge.&lt;/strong&gt; No &lt;code&gt;x-powered-by&lt;/code&gt; in the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every switch left off is written down with its reason.&lt;/strong&gt; Bot Fight Mode, AI crawlers, preload.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The admin path does not go through Cloudflare, and Cloudflare’s ranges are never on an admin allow-list.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I Tell Every Customer Now
&lt;/h2&gt;

&lt;p&gt;The free plan is not the weak link. The undecided switch is.&lt;/p&gt;

&lt;p&gt;So in every review I ask one question:&lt;/p&gt;

&lt;p&gt;“Can you show me, with a command, that each Cloudflare rule you think you have is actually blocking something?”&lt;/p&gt;

&lt;p&gt;If the answer is a list of &lt;code&gt;403&lt;/code&gt;s, they’re in good shape. If the answer is “it’s in the dashboard,” we’re not done yet.&lt;/p&gt;

&lt;p&gt;I write about the questions I ask in real customer rooms — the ones that change architectures and surface the problems no one else noticed. If you found this useful, drop your email below and I’ll send the next one when it’s ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the Cloudflare free plan enough to secure a WordPress site?
&lt;/h3&gt;

&lt;p&gt;For a small site, it covers a lot: Full (Strict) TLS, HSTS, five custom WAF rules, one rate-limiting rule, managed rules and DDoS protection. It runs short on rate limiting, which is locked to a 10-second window, and it cannot protect an origin that attackers reach directly. Pair it with a login lockout and 2FA at the origin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Cloudflare SSL mode should I use?
&lt;/h3&gt;

&lt;p&gt;Full (Strict). It encrypts the connection from Cloudflare to your server and checks that the server’s certificate is valid. A free Cloudflare Origin Certificate on your server is enough to make it work. Flexible mode leaves that leg unencrypted while still showing visitors a padlock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the free plan’s rate limiting not stop brute-force attacks?
&lt;/h3&gt;

&lt;p&gt;On the free plan the counting period and the block duration are both locked to 10 seconds. A slow script that stays under the threshold is never blocked, and a blocked one is free again after 10 seconds. Use it to slow bursts, and rely on an origin-side lockout and two-factor login for real protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I turn on Bot Fight Mode?
&lt;/h3&gt;

&lt;p&gt;Only if being crawled is not important to you. Bot Fight Mode also challenges SEO crawlers and uptime monitors. If your site depends on search traffic, targeted WAF rules for the paths and tools you actually want to block are the better trade.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check that my Cloudflare WAF rules are working?
&lt;/h3&gt;

&lt;p&gt;Request a path the rule should block with curl and read the status code. A working block rule returns 403. If you see 200 or 404, the expression is not matching; wait about 30 seconds after any change and add a cache-busting query string before you test again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I whitelist Cloudflare IP ranges for cPanel or WHM access?
&lt;/h3&gt;

&lt;p&gt;No. That opens your admin ports to anyone who routes through Cloudflare. Reach admin panels on the origin IP directly, keep the server’s own hostname DNS-only (grey cloud), and allow only your admin IP in the firewall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://quabin.com/wp-content/uploads/2026/10/quabin-cloudflare-free-plan-security-settings.pdf" rel="noopener noreferrer"&gt;Download the PDF&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://quabin.com/cloudflare-free-plan-security-settings/" rel="noopener noreferrer"&gt;quabin.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>security</category>
      <category>wordpress</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Outage That Wasn't a DDoS</title>
      <dc:creator>Daanial Farrukh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:42:10 +0000</pubDate>
      <link>https://dev.to/daanial/the-outage-that-wasnt-a-ddos-381f</link>
      <guid>https://dev.to/daanial/the-outage-that-wasnt-a-ddos-381f</guid>
      <description>&lt;p&gt;&lt;em&gt;A presales war story about a website that kept dying while the control panel stayed perfectly healthy — and the two numbers in the logs that nobody had compared.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The message came in the same way these messages always do:&lt;/p&gt;

&lt;p&gt;“The site is down again. Webmail too. We think it’s a DDoS.”&lt;/p&gt;

&lt;p&gt;Then, a minute later: “But WHM is working fine.”&lt;/p&gt;

&lt;p&gt;I asked one question: “How many PHP processes is the site allowed to run?”&lt;/p&gt;

&lt;p&gt;Silence. Then: “What does that have to do with a DDoS?”&lt;/p&gt;

&lt;p&gt;Everything, as it turned out. The site was a busy public portal on a cPanel server. Twice in two days it had become unreachable for real users for twenty to thirty minutes, then recovered on its own. Webmail on the same box died with it. The control panel on port 2087 never blinked. The team had already restarted Apache, checked the firewall, and started drafting a request for DDoS protection.&lt;/p&gt;

&lt;p&gt;That’s the trap. &lt;strong&gt;An outage with no attacker, no error and no crash looks exactly like an attack.&lt;/strong&gt; It isn’t. It hides in plain sight, in two numbers that live in different files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why “WHM Works” Tells You Nothing
&lt;/h2&gt;

&lt;p&gt;The first thing I corrected was the assumption that the server was healthy because the control panel loaded.&lt;/p&gt;

&lt;p&gt;WHM is served by &lt;code&gt;cpsrvd&lt;/code&gt;, a separate daemon. It does not pass through Apache at all. So on a cPanel box, WHM on port 2087 being fast while the website on port 443 is dead means precisely one thing: &lt;strong&gt;the problem is inside the web stack, and the server itself is fine.&lt;/strong&gt; That is a useful clue, not a reassurance.&lt;/p&gt;

&lt;p&gt;Webmail was the second clue. On this server, webmail is proxied through Apache. When Apache has no free worker, webmail dies along with the site. Two symptoms, one bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Absence of Errors Is Not Health
&lt;/h2&gt;

&lt;p&gt;The team’s strongest argument for “it must be an attack” was the error log. It was empty. Apache had not crashed, had not restarted, and had logged nothing during either incident.&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="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt; /usr/local/apache/logs/error_log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing. And that is expected. &lt;strong&gt;Saturation is not an error.&lt;/strong&gt; When every worker is busy, Apache does not fail — it waits. New connections queue. Users see a spinner, then a timeout. Nothing in that sequence is an error from Apache’s point of view, so nothing gets logged.&lt;/p&gt;

&lt;p&gt;If you only look for errors, you will never find a capacity problem. You have to look at the traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Count Requests Per Minute
&lt;/h2&gt;

&lt;p&gt;The access log for the domain is where the story lives. On cPanel the HTTPS traffic is in the &lt;code&gt;-ssl_log&lt;/code&gt; file, not the plain one.&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="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $4}'&lt;/span&gt; /usr/local/apache/domlogs/example.com-ssl_log | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2&lt;/span&gt;,3 | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints a request count for every minute of the day. The outage window stood out immediately: a flat line of roughly 700 requests a minute, every minute, for twenty minutes. Then it fell to 20 in a single minute, and the site came back.&lt;/p&gt;

&lt;p&gt;Here is the detail that settled the argument about causation: &lt;strong&gt;real traffic reappeared in the same minute the flood stopped.&lt;/strong&gt; Images, scripts, the homepage, the first webmail login — all of it returned at the moment the flood ended, not one minute earlier. Whatever that traffic was, it was locking users out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Find Out What the Traffic Was Asking For
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'10:1[0-9]:'&lt;/span&gt; /usr/local/apache/domlogs/example.com-ssl_log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of the 7,000 requests in that window — 100 percent — hit the same page: the public listing page of the portal. And every URL was unique. They looked 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;/page-listing?list=36?list=42?list=42?list=42?list=42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid URL can contain only one &lt;code&gt;?&lt;/code&gt;. Anything after the first one is the query string. These URLs had a second, a third, a tenth. They were malformed, and they were growing: each request was one &lt;code&gt;?list=&lt;/code&gt; longer than the previous one.&lt;/p&gt;

&lt;p&gt;That pattern has only one explanation. A crawler was taking each category link on the page and appending it to the address it was already on, instead of replacing the query string. Every page it loaded minted new links, every new link was unique, and the crawl never ended.&lt;/p&gt;

&lt;p&gt;I want to be precise about who was at fault here, because the team’s first instinct was to send a developer to “fix the links”. &lt;strong&gt;The site’s links were correct.&lt;/strong&gt; The category links were clean absolute URLs. The click handler on the listing rows only filled a subscribe form; the navigation line in it was commented out and never ran. The concatenation was happening inside the crawler. The site’s only failing was answering a malformed URL with &lt;code&gt;200 OK&lt;/code&gt;, which tells any crawler “valid, keep going”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rule Out the DDoS Properly
&lt;/h2&gt;

&lt;p&gt;A DDoS is a volume problem from many sources. So I checked the sources.&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="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'10:1[0-9]:'&lt;/span&gt; /usr/local/apache/domlogs/example.com-ssl_log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The busiest IP address in the entire window had made three requests. Three. The load came from more than two thousand addresses in datacenter and proxy ranges, each making two or three requests. By volume per IP, nothing was happening. No firewall rate limit on earth would have triggered.&lt;/p&gt;

&lt;p&gt;Then I checked the user agent.&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="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'10:1[0-9]:'&lt;/span&gt; /usr/local/apache/domlogs/example.com-ssl_log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"Mozilla[^"]*"'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7,024 of 7,027 requests shared a single string — one specific desktop Chrome build on one specific macOS version. Two thousand “different visitors” with an identical browser fingerprint is not two thousand visitors. It is one scraping operation rotating through a proxy pool, dressed as a browser.&lt;/p&gt;

&lt;p&gt;Two more things confirmed it was a tool, not a crowd. Both incidents sat in a tight band of 676 to 718 requests per minute, every minute — organic crawling is never that even. And the tool did not identify itself as a bot, which means &lt;code&gt;robots.txt&lt;/code&gt; was never going to help.&lt;/p&gt;

&lt;p&gt;So: not a DDoS. One client, rate-limited by its own configuration, requesting about 11.7 pages per second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The Number Nobody Had Compared
&lt;/h2&gt;

&lt;p&gt;Here is the question the team could not answer in that first message, and it is the whole story.&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'pm.max_children|pm.max_requests'&lt;/span&gt; /opt/cpanel/ea-php81/root/etc/php-fpm.d/example.com.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;pm&lt;/span&gt;.&lt;span class="n"&gt;max_children&lt;/span&gt; = &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;pm&lt;/span&gt;.&lt;span class="n"&gt;max_requests&lt;/span&gt; = &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Five.&lt;/strong&gt; The entire site was allowed five concurrent PHP processes. At roughly half a second per page, that is a hard ceiling of about ten PHP requests per second.&lt;/p&gt;

&lt;p&gt;The scraper was asking for 11.7 PHP requests per second. More than 100 percent of the site’s capacity, sustained for twenty minutes.&lt;/p&gt;

&lt;p&gt;Now the chain is obvious. All five PHP-FPM slots fill. The next request waits for a slot. While it waits, it holds an Apache worker. Apache has 150 workers. Within a minute they are all holding a request that is waiting for PHP. Every connection through Apache — the site, the images, the webmail proxy — stalls. A few real requests trickle through when a slot happens to free up at the right moment, which is why the outage was severe but never quite total.&lt;/p&gt;

&lt;p&gt;Apache never crashed because Apache was never the bottleneck. It was the waiting room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 20 Percent More Traffic Took the Site Down
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised the team. The site normally served around 35,000 requests an hour at peak. The scraper added about 42,000 an hour. More, yes, but not ten times more. How does a modest increase cause a total outage?&lt;/p&gt;

&lt;p&gt;Because the two numbers are not the same kind of request.&lt;/p&gt;

&lt;p&gt;Normal traffic is mostly images, CSS and JavaScript. Apache serves those from disk without touching PHP. A busy hour of real visitors might generate only a few PHP requests per second. The scraper’s traffic was &lt;strong&gt;100 percent PHP&lt;/strong&gt;, and because every URL was unique, nothing was ever served from cache. Every single hit ran the application from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare PHP request rates, not total request rates.&lt;/strong&gt; Total requests per hour is the number everyone quotes and the number that tells you nothing about PHP capacity.&lt;/p&gt;

&lt;p&gt;A warning if you go looking for this on your own server: measure during the flood, not after. When I ran the listing page on the idle server it answered in half a second and looked perfectly healthy. The pages were never slow to process. They were waiting for a slot that never opened.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Fix This
&lt;/h2&gt;

&lt;p&gt;There were two independent problems. The scraper alone would have been harmless if the page did not mint infinite URLs. The infinite URLs alone would have been harmless without the scraper. Fixing either one breaks the outage. Fix both.&lt;/p&gt;

&lt;p&gt;Step 1: Reject malformed URLs at the door.&lt;/p&gt;

&lt;p&gt;Any request whose query string contains a second &lt;code&gt;?&lt;/code&gt; is malformed by definition, so refusing it carries no risk to legitimate traffic. Two lines in the site’s &lt;code&gt;.htaccess&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} \?
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [F]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the server answers &lt;code&gt;403&lt;/code&gt; instead of &lt;code&gt;200&lt;/code&gt;, the crawler has nothing new to follow, and the loop collapses on its own. No code change on the page is needed, because the page was never broken.&lt;/p&gt;

&lt;p&gt;Verify it before you trust it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/page-listing?list=36?list=42'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want &lt;code&gt;403&lt;/code&gt;. Then run the same check against the clean URL with one &lt;code&gt;?&lt;/code&gt; and confirm it still returns &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Step 2: Give PHP a realistic ceiling.&lt;/p&gt;

&lt;p&gt;Five children is far too low for a site doing 35,000 requests an hour. Any genuine burst of real visitors would have caused the same outage with no scraper in sight. The server had almost 12 GB of RAM and about 9 GB free; one FPM child measured 16 MB idle, so budgeting 50 MB each leaves room for about 30. In WHM that is MultiPHP Manager › PHP-FPM pool options for the domain, or the same file we read above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pm.max_children = 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This change is about capacity, not about the scraper. At 30 children the ceiling rises from about 10 to about 60 PHP requests per second, so this particular scraper would no longer cause an outage — but a faster one still would. Step 1 is what stops the loop. Step 2 is what stops the next real traffic spike from looking like one.&lt;/p&gt;

&lt;p&gt;Change &lt;code&gt;pm.max_requests&lt;/code&gt; separately, later. The cPanel default of 20 makes every child respawn constantly; 500 is saner. Keep the two changes apart so you can tell which one did what.&lt;/p&gt;

&lt;p&gt;Step 3: Put something back in front of the origin.&lt;/p&gt;

&lt;p&gt;This domain had been switched to DNS-only on Cloudflare a few weeks earlier to fix a different performance problem, which quietly turned off the edge rate limiting and put the origin IP in public DNS. The scraper landed directly on the server with nothing in between. Whatever you use in front — Cloudflare, a load balancer, a WAF — make sure a per-client rate limit is actually active after every DNS change, not just the day it was set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Log Gotchas That Cost an Hour
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Webmail requests on this server were in the main domain’s &lt;code&gt;-ssl_log&lt;/code&gt;, not in the webmail subdomain’s log, which had been empty for months. Identify webmail hits by &lt;code&gt;roundcube&lt;/code&gt; or &lt;code&gt;cpsess&lt;/code&gt; in the URL.&lt;/li&gt;
&lt;li&gt;The live log is rotated around 17:00 and does not hold earlier days. A count of zero for an older window usually means rotation, not an outage. Confirm which hours a file covers before calling a gap real.&lt;/li&gt;
&lt;li&gt;Apache prints a “reached MaxRequestWorkers” line once per event, not once per minute. Grepping a single minute of the error log proves nothing. Tail it and read the timeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Tell Every Customer Now
&lt;/h2&gt;

&lt;p&gt;Before anyone writes “DDoS” in an incident report, I ask one question:&lt;/p&gt;

&lt;p&gt;“What is the busiest single IP in the outage window, and how many PHP workers is the site allowed?”&lt;/p&gt;

&lt;p&gt;If the busiest IP made three requests and the answer to the second half is a single digit, it was not an attack. It was a ceiling. And the fix is two lines in a config file, not a procurement request.&lt;/p&gt;

&lt;p&gt;I write about the questions I ask in real customer rooms — the ones that change architectures and surface the problems no one else noticed. If you found this useful, drop your email below and I’ll send the next one when it’s ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Was it really not a DDoS? It took the site down.
&lt;/h3&gt;

&lt;p&gt;No. A DDoS is a volume attack from many sources, and the firewall sees it as high request rates per IP. Here the busiest IP made three requests in twenty minutes. The outage came from one rate-limited scraper requesting about 11.7 PHP pages per second against a site that could process about 10. That is a capacity ceiling, not an attack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did WHM keep working while the website was down?
&lt;/h3&gt;

&lt;p&gt;WHM runs on its own daemon, cpsrvd, on port 2087, and does not pass through Apache. When Apache’s workers are all busy waiting for PHP, the website and anything proxied through Apache (such as webmail) stall, but WHM is unaffected. “WHM works” only tells you the server is up, not that the web stack is healthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why was there nothing in the Apache error log?
&lt;/h3&gt;

&lt;p&gt;Because nothing failed. When all workers are busy, Apache queues new connections rather than erroring. Saturation is silent. You find it in the access log by counting requests per minute and by looking at what those requests asked for, not in the error log.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the developer fix the page so the URLs stop growing?
&lt;/h3&gt;

&lt;p&gt;Not in this case. The page’s links were correct absolute URLs; the concatenation happened inside the crawler. The one thing the site should change is to answer a URL with a second question mark with 403 instead of 200, which a two-line rewrite rule does. That removes the signal that keeps the crawler looping.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a sensible pm.max_children value?
&lt;/h3&gt;

&lt;p&gt;Measure one idle PHP-FPM child’s memory on your server, budget about three times that per child to allow for real page loads, and divide the RAM you are willing to give PHP by that figure. On this server, 16 MB idle, a 50 MB budget and about 9 GB free pointed to roughly 30. Then confirm under real load, not on an idle box.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know if my server is close to this ceiling right now?
&lt;/h3&gt;

&lt;p&gt;Count PHP requests per second during your real peak, not total requests, and compare it with pm.max_children divided by your average PHP response time in seconds. If the peak PHP rate is more than about half of that ceiling, you are one scraper or one marketing campaign away from an outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://quabin.com/wp-content/uploads/2026/10/quabin-outage-that-was-not-a-ddos-scraper-php-fpm.pdf" rel="noopener noreferrer"&gt;Download the PDF&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://quabin.com/outage-that-was-not-a-ddos-scraper-php-fpm/" rel="noopener noreferrer"&gt;quabin.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>presales</category>
      <category>apache</category>
      <category>phpfpm</category>
      <category>cpanel</category>
    </item>
    <item>
      <title>Load Testing the Server Before Deploying Your Critical App</title>
      <dc:creator>Daanial Farrukh</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:13:54 +0000</pubDate>
      <link>https://dev.to/daanial/load-testing-the-server-before-deploying-your-critical-app-34fa</link>
      <guid>https://dev.to/daanial/load-testing-the-server-before-deploying-your-critical-app-34fa</guid>
      <description>&lt;p&gt;&lt;em&gt;A presales war story about CPU steal time — the metric almost nobody checks before go-live — and the full stress-test checklist I now run before any app touches production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A customer once told me, with total confidence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We’re ready. Load testing passed, QA signed off, we’re deploying Friday.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I asked one question: “What VM size are you deploying on, and have you checked CPU steal time under load?”&lt;/p&gt;

&lt;p&gt;Silence. Then: “What’s steal time?”&lt;/p&gt;

&lt;p&gt;That Friday deployment went live. By Monday, response times had tripled during peak hours, support tickets were piling up, and nobody could explain why — CPU usage on the app server looked fine. Not maxed out. Not even close.&lt;/p&gt;

&lt;p&gt;That’s the trap. &lt;strong&gt;CPU steal time doesn’t show up as high CPU usage.&lt;/strong&gt; It hides in plain sight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CPU Steal Time Actually Is
&lt;/h2&gt;

&lt;p&gt;If your app runs on a cloud VM — AWS, Azure, GCP, or any shared hypervisor host — your VM is not the only thing running on that physical server. Other customers’ VMs share the same physical CPU cores. The hypervisor decides, moment to moment, which VM gets CPU time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steal time is the percentage of time your VM wanted to run on the CPU but couldn’t&lt;/strong&gt;, because the hypervisor gave that time to a neighbouring VM instead.&lt;/p&gt;

&lt;p&gt;Here’s the part that trips people up: your VM’s own CPU usage metric only measures what happens inside your VM. It has no visibility into the time it spent waiting outside, in the hypervisor’s queue. So you can have an app server reporting 40% CPU usage, technically “healthy,” while it’s actually being starved of the CPU cycles it needs — and every request is slower because of it.&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%2F93s3u3qu7kvg4amm8mpb.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%2F93s3u3qu7kvg4amm8mpb.png" alt="Diagram showing a physical CPU shared between your VM and two neighbouring VMs, with the 40 percent gap marked as steal time invisible to the VM" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your VM only measures what happens inside itself. The time the hypervisor hands to neighbouring VMs never appears in your CPU usage graph.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the classic &lt;strong&gt;“noisy neighbour”&lt;/strong&gt; problem. It’s most common on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Burstable instance types (AWS T-series, Azure B-series) that are designed to be oversubscribed&lt;/li&gt;
&lt;li&gt;Cheap shared-core VPS plans&lt;/li&gt;
&lt;li&gt;Any host running close to capacity with many tenants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check it right now on a Linux box with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;top
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;strong&gt;%st&lt;/strong&gt; value in the CPU line. Anything consistently above 10% means your app is losing real CPU time to other tenants — and that number gets worse exactly when you need performance most: during your own traffic spikes, because that’s usually when everyone else’s spikes too.&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%2Fq5t7n4mk9afkqpmjl6by.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%2Fq5t7n4mk9afkqpmjl6by.png" alt="Linux top command output with the %st steal time value of 23.4 circled in the CPU line" width="800" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;23.4 %st in top — nearly a quarter of this box’s CPU time is going to other tenants on the same host.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Never Shows Up in Normal Testing
&lt;/h2&gt;

&lt;p&gt;Here’s why so many teams miss it. Load testing usually runs in a staging environment. Staging is often on a quieter host, provisioned separately, with little or no other tenant load. The app performs beautifully. Everyone signs off.&lt;/p&gt;

&lt;p&gt;Production is a different host, possibly a cheaper tier, possibly shared with far more noisy tenants — and the first time anyone discovers the steal time problem is when real customers are already inside the app, complaining.&lt;/p&gt;

&lt;p&gt;The fix isn’t complicated once you know to look for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check %st on the actual production instance type&lt;/strong&gt;, not staging, under real load — not just idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move to dedicated-core instances&lt;/strong&gt; (e.g. AWS m/c/r families instead of the t family) for anything customer-facing and latency-sensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on steal time, not just CPU usage.&lt;/strong&gt; Set a CloudWatch or Azure Monitor alarm on it. Most teams alert on CPU% and never even collect steal time as a metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you must use burstable instances&lt;/strong&gt;, understand your CPU credit balance and what happens when it runs out — that’s a related but separate trap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That customer from the story above moved their app server from a &lt;strong&gt;t3.medium&lt;/strong&gt; to an &lt;strong&gt;m6i.large&lt;/strong&gt; two weeks after launch. Steal time went from spiking above 30% during peak hours to a flat 0%. No code changed. The app was never the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson: Steal Time Is Just One Item on a Longer List
&lt;/h2&gt;

&lt;p&gt;CPU steal time is the one nobody checks. But it’s not the only thing that passes fine in staging and breaks in production. Here’s the full checklist I now walk through with every customer before a go-live date gets locked in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CPU steal time&lt;/strong&gt; (covered above). Test on the actual production instance family, under real concurrent load, not idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check where your CDN edge actually is.&lt;/strong&gt; If your site sits behind Cloudflare, the edge serving your visitors is not always in your country. When the edge sits in a different region from both your users and your origin, every request takes an international round trip, and page loads can slip by whole seconds. Open &lt;code&gt;https://yourdomain.com/cdn-cgi/trace&lt;/code&gt; and compare &lt;strong&gt;colo=&lt;/strong&gt; (the Cloudflare edge serving you) against &lt;strong&gt;loc=&lt;/strong&gt; (your own location). If those two are far apart, you are paying that latency on every single asset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Disk IOPS and latency.&lt;/strong&gt; Cloud disks (EBS gp3, Azure managed disks, and so on) have provisioned IOPS and throughput caps. An app that writes logs, session data, or database records can silently queue up once it hits the disk’s IOPS ceiling — and the symptom looks like “the app is slow,” not “the disk is throttled.” Check your disk’s IOPS and throughput limit against what your app actually generates at peak load, not average load.&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="nb"&gt;sudo &lt;/span&gt;fio &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;iops-test &lt;span class="nt"&gt;--directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/root &lt;span class="nt"&gt;--size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1G &lt;span class="nt"&gt;--numjobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;--rw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;randwrite &lt;span class="nt"&gt;--bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4k &lt;span class="nt"&gt;--iodepth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;32 &lt;span class="nt"&gt;--ioengine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;libaio &lt;span class="nt"&gt;--runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;60 &lt;span class="nt"&gt;--time_based&lt;/span&gt; &lt;span class="nt"&gt;--group_reporting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain words: this starts 4 parallel jobs, each writing its own 1 GB file into &lt;code&gt;/root&lt;/code&gt; — 4 GB in total — in 4 KB blocks, and it keeps writing for 60 seconds. At the end it gives you a reading. Two numbers matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IOPS — writes per second.&lt;/strong&gt; You will see something like &lt;code&gt;iops: min=4749, max=448901, avg=31436.63&lt;/code&gt;. Judge the disk on &lt;strong&gt;avg&lt;/strong&gt; — about 31,000 writes per second here. The &lt;strong&gt;min&lt;/strong&gt; of 4,749 is the single worst sample: useful as a warning about how bad the dips get, but it is not the disk’s normal speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency — how long one write takes to finish.&lt;/strong&gt; The value to read is &lt;strong&gt;clat&lt;/strong&gt;, completion latency: &lt;code&gt;clat (usec): min=18, max=18354k, avg=5319.06&lt;/code&gt;. That max is 18,354,000 microseconds — over 18 seconds for a single write. An average of 5 ms is healthy; an 18-second outlier is something your app will feel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Network bandwidth and connection limits.&lt;/strong&gt; Cloud instances have a documented per-instance network throughput cap, and it usually scales with instance size — look it up on your provider’s spec sheet for the exact instance type going to production. To see what the box actually gets, install a speed test on it:&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="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;fast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just type &lt;code&gt;fast&lt;/code&gt; and hit enter. That gives you real download throughput from the server, which is enough to catch a box performing nowhere near its rated bandwidth. It will not tell you your concurrent connection ceiling, though — for that, check the limits on whatever sits in front of the app (load balancer, nginx &lt;code&gt;worker_connections&lt;/code&gt;, database &lt;code&gt;max_connections&lt;/code&gt;) against your real peak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. 502 / 504 gateway errors and the OOM killer.&lt;/strong&gt; When the box runs out of RAM, or a sudden spike hits, Linux has no choice but to kill a process — and it goes for the one using the most memory, which is very often the database. The DB dies and restarts, and while it is coming back your workers and agents cannot open a connection to it. What you see at the front is a 502 or 504, which looks like a web server problem and is not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dmesg &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'killed process\|out of memory'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Autoscaling trigger behaviour.&lt;/strong&gt; Don’t just confirm autoscaling is “enabled.” Test it. Push real load and watch the scale-out actually happen. Many teams discover, only during a real incident, that their scaling policy’s cooldown period is too long, or that the metric it scales on (CPU%) doesn’t reflect the real bottleneck — which, as above, might be steal time, disk, or connections instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Failure and degradation paths, not just the happy path.&lt;/strong&gt; Kill a dependency mid-test. Cut off the database. Throttle the cache. A stress test that only proves the app is fast when everything works tells you nothing about what happens the moment one thing doesn’t — and in production, something always eventually doesn’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Third-party API rate limits.&lt;/strong&gt; If your app calls out to a payment gateway, SMS provider, or any external API, confirm their rate limit against your real peak concurrent call volume. This is invisible in staging if staging uses a sandbox key with a different (often higher) limit than production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tell Every Customer Now
&lt;/h2&gt;

&lt;p&gt;Before I sign off on any go-live date, I ask one question that covers most of this list in one shot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Have you load tested on the exact production instance type, at 2x your forecasted peak concurrency, while deliberately breaking one dependency at a time?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, they’re in good shape. If the answer is “we tested on staging and it was fine,” we’re not done yet — and now you know exactly what to go check first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://quabin.com/load-testing-server-before-deploying-critical-app/" rel="noopener noreferrer"&gt;quabin.com&lt;/a&gt; — field notes from a presales solutions architect.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>performance</category>
      <category>devops</category>
    </item>
    <item>
      <title>The cPanel Mail Server Hardening Checklist I Never Skip</title>
      <dc:creator>Daanial Farrukh</dc:creator>
      <pubDate>Sat, 27 Jun 2026 18:11:17 +0000</pubDate>
      <link>https://dev.to/daanial/the-cpanel-mail-server-hardening-checklist-i-never-skip-46eb</link>
      <guid>https://dev.to/daanial/the-cpanel-mail-server-hardening-checklist-i-never-skip-46eb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You work hard for your business, and all the while leads are slipping away — leads that never reached you — because of how the email server was set up. And the greater irony? Nobody knows it’s happening.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of my clients recently asked me to diagnose a problem. The message was short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Hi Daanial, we’re not receiving email sometimes from one particular sender. Can you please look into it and fix or diagnose what’s wrong?”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple enough. So I opened the mail logs — and my heart sank. &lt;strong&gt;More than 8,000 emails had been discarded at the SMTP level&lt;/strong&gt;: refused at the front door before they ever reached a single inbox. The customer had no idea. As far as they could tell, those messages were simply never sent.&lt;/p&gt;

&lt;p&gt;That is the quiet killer of email hosting. It isn’t a dramatic outage you can see — it’s a loss of revenue you never knew you had.&lt;/p&gt;

&lt;p&gt;That afternoon is exactly why I built the checklist in this article. It’s a must-read for email hosting providers, and a good read for anyone who cares whether email actually gets delivered. Below is every step I now run on a cPanel mail server before it ever touches a customer’s domain — in plain language.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, think in layers — not one magic switch
&lt;/h2&gt;

&lt;p&gt;The biggest mistake I see is treating email security as a single setting. It isn’t. A hardened mail server checks every message at several points, like a building with a guest list, a metal detector, and a locked back door. If one layer misses something, the next one catches it.&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%2F1wysw2uia3ovgx8sdg7o.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%2F1wysw2uia3ovgx8sdg7o.png" alt="Every incoming email passes through several checkpoints before it reaches the inbox." width="799" height="282"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every incoming email passes through several checkpoints before it reaches the inbox.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Switch on the spam engine
&lt;/h2&gt;

&lt;p&gt;The first thing I do is turn on &lt;strong&gt;Apache SpamAssassin&lt;/strong&gt; for the whole server. Out of the box it’s often off, or only on for some accounts. I force it on globally so no mailbox is left unprotected. In WHM: &lt;strong&gt;Home › Email › Apache SpamAssassin → Forced Global ON&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fooyrc6ca22jvfnuabv66.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%2Fooyrc6ca22jvfnuabv66.png" alt="WHM: turning Apache SpamAssassin on for the entire server." width="800" height="342"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;WHM: turning Apache SpamAssassin on for the entire server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; this is the smoke detector for email.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — Tune the spam score so you don’t lose real mail
&lt;/h2&gt;

&lt;p&gt;A spam filter that’s too aggressive is worse than no filter, because it eats real email. In cPanel, under &lt;strong&gt;Spam Filters › Spam Threshold Score&lt;/strong&gt;, set it to &lt;strong&gt;“Passive — Only Very Obvious Spam (10)”&lt;/strong&gt;. The higher the number, the more lenient — a value of 10 means only the most blatantly spammy messages get flagged.&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%2Fmqyid3m4gnsrqfoayqzu.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%2Fmqyid3m4gnsrqfoayqzu.png" alt="Setting the spam threshold to 10 so genuine mail is never lost to false positives." width="799" height="343"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Setting the spam threshold to 10 so genuine mail is never lost to false positives.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; start lenient. You can always tighten later — but you can never un-lose a customer’s email.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3 — Add the global “known bad sender” lists
&lt;/h2&gt;

&lt;p&gt;Blocklists (RBLs) are shared, worldwide lists of IP addresses already caught sending spam. I switch on the two I trust most — &lt;strong&gt;Spamhaus&lt;/strong&gt; and &lt;strong&gt;SpamCop&lt;/strong&gt; — under &lt;strong&gt;Exim Configuration Manager › RBLs&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6lx18sybd41kakrm9g8u.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%2F6lx18sybd41kakrm9g8u.png" alt="Enabling the Spamhaus and SpamCop blocklists in WHM." width="800" height="340"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Enabling the Spamhaus and SpamCop blocklists in WHM.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One catch that trips up almost everyone: Spamhaus refuses to answer if you query it through a public DNS like Google’s 8.8.8.8. The server must use its &lt;strong&gt;own&lt;/strong&gt; local resolver:&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;# point the server at its own resolver&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/resolv.conf
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"nameserver 127.0.0.1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/resolv.conf
chattr +i /etc/resolv.conf   &lt;span class="c"&gt;# lock the file so nothing overwrites it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then prove it works — this test query should return a result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig @127.0.0.1 2.0.0.127.zen.spamhaus.org +short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; blocklists stop the worst senders for free — but only if your server asks them the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Make the server verify DKIM on incoming mail
&lt;/h2&gt;

&lt;p&gt;DKIM is a digital signature legitimate senders attach to their email. Verifying it on incoming mail confirms a message really came from who it claims. Find it in WHM under &lt;strong&gt;Exim Configuration Manager › Basic Editor&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4vu2hbmqxpadtu0n56zk.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%2F4vu2hbmqxpadtu0n56zk.png" alt="Turning on DKIM verification for incoming mail in Exim." width="799" height="340"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Turning on DKIM verification for incoming mail in Exim.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; this is how the server checks a sender’s ID card instead of taking their word for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5 — Put a guard on attachments (antivirus)
&lt;/h2&gt;

&lt;p&gt;Spam is annoying; a virus is a liability. Install an antivirus/anti-malware scanner (ClamAV) so every attachment is checked before it lands in a mailbox: &lt;strong&gt;Home › cPanel › Manage Plugins&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffxgq3c8ph0w8mg5l0e4h.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%2Ffxgq3c8ph0w8mg5l0e4h.png" alt="Installing the ClamAV antivirus scanner from WHM’s plugin manager." width="800" height="341"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Installing the ClamAV antivirus scanner from WHM’s plugin manager.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; the metal detector for attachments.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 6 — Stop your own server from becoming the spammer
&lt;/h2&gt;

&lt;p&gt;One mailbox password leaks, an attacker logs in, and the server sends tens of thousands of spam messages in an hour — that’s how servers get blocklisted. Three settings in WHM &lt;strong&gt;Tweak Settings&lt;/strong&gt; do the heavy lifting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap hourly emails per account.&lt;/strong&gt; One hacked account can no longer blast the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject mail instead of silently dropping it.&lt;/strong&gt; Silent dropping means email vanishes and nobody knows why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail immediately instead of queueing.&lt;/strong&gt; Bad mail is refused on the spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzzvy8w3oeapkfn5fzjoz.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%2Fzzvy8w3oeapkfn5fzjoz.png" alt="Setting a maximum number of emails each account can send per hour." width="800" height="345"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Setting a maximum number of emails each account can send per hour.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oetov6oi5lirf51lkkj.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%2F3oetov6oi5lirf51lkkj.png" alt="Reject undeliverable mail and fail immediately, rather than discard or queue it." width="800" height="342"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Reject undeliverable mail and fail immediately, rather than discard or queue it.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7k4lp18hp67us8p2wdt.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%2Fm7k4lp18hp67us8p2wdt.png" alt="Outbound rate-limiting and sender tracking to catch a compromised account early." width="799" height="340"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Outbound rate-limiting and sender tracking to catch a compromised account early.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; a good host doesn’t just block incoming spam — it makes sure it can never be the SOURCE of spam.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 7 — Get the server’s identity right
&lt;/h2&gt;

&lt;p&gt;Big mail providers are suspicious of servers that look anonymous. I always set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A real hostname&lt;/strong&gt; (e.g. &lt;code&gt;your-host.example.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matching reverse DNS (PTR)&lt;/strong&gt; — ask your datacenter. Mismatched rDNS is an instant spam signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fresh SSL certificate&lt;/strong&gt; (cPanel › SSL).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The correct server time.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A complete DNS record set:&lt;/strong&gt; A (root, www, mail, webmail), MX, and — most importantly — &lt;strong&gt;SPF, DKIM, and DMARC&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those last three are the trust trio: &lt;strong&gt;SPF&lt;/strong&gt; says which servers may send for your domain, &lt;strong&gt;DKIM&lt;/strong&gt; signs your messages so they can’t be forged, and &lt;strong&gt;DMARC&lt;/strong&gt; tells the world what to do if a message fails. Skip them and your mail lands in spam, full stop. One more Exim tweak: reject remote mail sent to the server’s own hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; a server with a clean, honest identity gets trusted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 8 — Lock the doors (firewall rules)
&lt;/h2&gt;

&lt;p&gt;Think of the firewall as a guest list for the building: visitors come in through their own doors, the control room opens only for staff, every other door stays locked.&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%2Fst3rfe5nrigen2ibxjup.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%2Fst3rfe5nrigen2ibxjup.png" alt="The firewall guest list: who can reach which port on the server." width="800" height="351"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The firewall guest list: who can reach which port on the server.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website traffic&lt;/strong&gt; (80, 443): ideally only from Cloudflare’s IP ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email ports&lt;/strong&gt; (25, 465, 587, 993, 995): open to all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cPanel 2083 / WHM 2087 / SSH 22:&lt;/strong&gt; restricted to the admin’s own IP only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else: denied&lt;/strong&gt; by default; outbound allowed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more lock I never skip: turn on &lt;strong&gt;Two-Factor Authentication (MFA) for WHM&lt;/strong&gt; under &lt;strong&gt;Home › Security Center › Two-Factor Authentication&lt;/strong&gt;. Even if someone steals the password and is on an allowed IP, they still can’t get in without the code from your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; open the doors people need, lock the rest, add MFA on the admin door.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 9 — Make webmail fast and reliable
&lt;/h2&gt;

&lt;p&gt;Security that makes email painfully slow won’t last. In Dovecot I disable IPv6 if unused, set spare auth processes to 10 / max to 100, tune LMTP concurrency and cache, and use balanced compression. I also raise PHP limits (execution 300s, memory 512 MB, upload/POST 100 MB), and on an email-only box I stop the cPanel PHP-FPM service for a lighter Roundcube.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; a hardened server should still feel fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 10 — The gotcha that bites everyone: port 25
&lt;/h2&gt;

&lt;p&gt;Remember the 8,000 emails my client lost? &lt;em&gt;This&lt;/em&gt; is the kind of thing that causes it. Mail “just hangs” — no error, no bounce — and the server looks spotless. The datacenter’s network can quietly drop inbound connections on port 25 before they reach your server, so messages are discarded at the SMTP level and nobody is told. &lt;strong&gt;Ask your datacenter/NOC to whitelist inbound SMTP (port 25) for your server’s IP.&lt;/strong&gt; Before you call, confirm what you’re seeing:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"data timeout"&lt;/span&gt; /var/log/exim_mainlog | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lines like &lt;em&gt;“SMTP data timeout (message abandoned)”&lt;/em&gt; mean mail reached you but got cut off mid-transfer — a network-path problem, not the mailbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; sometimes the mail server is perfect and the network is the villain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11 — Prove it works, then keep it current
&lt;/h2&gt;

&lt;p&gt;Force a full update so every component is patched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/local/cpanel/scripts/upcp &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then confirm the auto-update cron is scheduled:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"upcp"&lt;/span&gt; /etc/cron&lt;span class="k"&gt;*&lt;/span&gt; /var/spool/cron&lt;span class="k"&gt;*&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plain takeaway:&lt;/strong&gt; hardening is not a one-time event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;None of these steps is hard on its own. The value is in doing all of them, every time, in the same order — so no server ever ships with the factory defaults still on.&lt;/p&gt;

&lt;p&gt;📋 &lt;strong&gt;A printable PDF version of this entire checklist is available &lt;a href="https://quabin.com/wp-content/uploads/2026/06/cPanel-Mail-Server-Hardening-Checklist.pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://quabin.com/cpanel-mail-server-hardening-checklist/" rel="noopener noreferrer"&gt;quabin.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cpanel</category>
      <category>email</category>
      <category>sysadmin</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Customer Thinks Their Site Has HTTPS. It Probably Doesn't.</title>
      <dc:creator>Daanial Farrukh</dc:creator>
      <pubDate>Wed, 03 Jun 2026 17:40:13 +0000</pubDate>
      <link>https://dev.to/daanial/your-customer-thinks-their-site-has-https-it-probably-doesnt-4j73</link>
      <guid>https://dev.to/daanial/your-customer-thinks-their-site-has-https-it-probably-doesnt-4j73</guid>
      <description>&lt;p&gt;&lt;em&gt;A presales story about the Cloudflare SSL setting almost everyone gets wrong — and how to verify, with proof, that you've actually fixed it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few weeks ago, I was in a workshop with a customer's CTO and his technical team. The room was confident. They had spent the last month tightening their stack, ticking off security items one by one. Authentication — done. WAF rules — done. Logging — done.&lt;/p&gt;

&lt;p&gt;Then I got to the question I always ask: &lt;em&gt;"Is encryption in place?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Yes," the CTO said. "We're on HTTPS."&lt;/p&gt;

&lt;p&gt;"How are you managing it?"&lt;/p&gt;

&lt;p&gt;"Through Cloudflare."&lt;/p&gt;

&lt;p&gt;It was an incomplete answer, and I wanted to clear my own head, so I asked the obvious follow-up: &lt;em&gt;"How?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"We've enabled Cloudflare's free SSL by keeping the proxy on."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"And?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The CTO looked at me. &lt;em&gt;"And what?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everyone in the room turned to me. I looked back at them — in shock.&lt;/p&gt;

&lt;p&gt;Because what they had configured wasn't end-to-end encryption. It was something much weaker. And every single person in that room thought it was the same thing.&lt;/p&gt;

&lt;p&gt;This article is for every CTO, architect, and engineer who has ever clicked the orange cloud icon, switched on Cloudflare SSL, and assumed the job was finished. It isn't. And the gap between what you think you have and what you actually have is exactly the kind of gap attackers wait for.&lt;/p&gt;

&lt;p&gt;I'll walk you through the misconception, the fix, and — critically — how to &lt;em&gt;verify&lt;/em&gt; that the fix actually works. I even set this up on quabin.com, so every command and screenshot below is from a real, running production configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Misconception That Lives Behind the Orange Cloud
&lt;/h2&gt;

&lt;p&gt;If you believe you are protected just because you proxied your domain through Cloudflare and "SSL is in place," you are doing half the work. And in cyber security, half the work is no work.&lt;/p&gt;

&lt;p&gt;Here's why.&lt;/p&gt;

&lt;p&gt;When you put Cloudflare in front of your website, two distinct network connections come into existence — not one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visitor → Cloudflare&lt;/strong&gt; — the browser talks to Cloudflare's edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare → Origin&lt;/strong&gt; — Cloudflare talks to your actual server (your VM on AWS, Hetzner, your hosting provider, wherever it lives).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The padlock you see in the browser bar reflects only the &lt;strong&gt;first&lt;/strong&gt; connection. It tells the user nothing — absolutely nothing — about what happens between Cloudflare and your origin server.&lt;/p&gt;

&lt;p&gt;Most teams configure the first connection correctly and never look at the second one. And that's where the breakdown lives.&lt;/p&gt;

&lt;p&gt;If the second leg is unencrypted, your traffic — login credentials, customer PII, session tokens, payment data, whatever flows through — passes through every router, ISP, and intermediate network between Cloudflare's data center and yours, in plaintext. A man-in-the-middle on any of those hops can read it, modify it, or sell it on the dark web. We hear about these breaches every quarter. This is one of the ways they happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloudflare SSL Modes — and Which One You're Actually On
&lt;/h2&gt;

&lt;p&gt;Cloudflare offers five SSL/TLS settings — one automatic and four manual. Most teams never look past the one they picked on day one, and that early choice is usually wrong for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Automatic&lt;/strong&gt; — This is now Cloudflare's default mode, and it's quietly impressive engineering. For years Cloudflare ran a separate tool called SSL Recommender that scanned each origin's TLS capability and prompted the user to pick the safest setting. Most users ignored the prompts. So Cloudflare went a step further: Automatic mode now performs that same check on every site by default. The moment it detects an origin that can handle TLS, it upgrades the connection to Full (option 4 below) — without breaking the site or surfacing a single certificate error to the user. It's the kind of pragmatic engineering most platforms don't bother to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Off&lt;/strong&gt; — No encryption anywhere. Self-explanatory. Don't use this in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Flexible&lt;/strong&gt; — Encrypted from visitor to Cloudflare. &lt;strong&gt;Plaintext from Cloudflare to your origin.&lt;/strong&gt; This is the trap. The browser shows the padlock. The user feels safe. Your traffic is exposed on the back half of the journey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Full&lt;/strong&gt; — Encrypted both legs. But Cloudflare does not validate your origin's certificate. A self-signed certificate works. So does an expired one. So does an attacker's certificate if they manage to intercept the connection. Better than Flexible — but still vulnerable to man-in-the-middle (MitM) attacks. If you assume no one would bother running such an attack on your traffic — that assumption is exactly the false sense of security this mode creates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Full (Strict)&lt;/strong&gt; — Encrypted both legs &lt;em&gt;and&lt;/em&gt; Cloudflare validates the origin's certificate against a trusted chain. This is real end-to-end TLS. This is what your CTO thinks you have when they say "we're on HTTPS via Cloudflare."&lt;/p&gt;

&lt;p&gt;Most teams I work with are on &lt;strong&gt;Flexible&lt;/strong&gt; or &lt;strong&gt;Full&lt;/strong&gt; when they think they are on &lt;strong&gt;Full (Strict)&lt;/strong&gt;. They click the orange cloud, see the padlock, ship, and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the CTO Thought vs. What Actually Happened
&lt;/h2&gt;

&lt;p&gt;Here is the picture the CTO had in his head:&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%2Fle1clwcbr83f168564oj.jpg" 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%2Fle1clwcbr83f168564oj.jpg" alt="End-to-end encryption — Full (Strict) mode" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;Full (Strict)&lt;/strong&gt; mode. End-to-end (E2E) TLS. The architecture is sound. No plaintext anywhere on the wire.&lt;/p&gt;

&lt;p&gt;Here is what was actually configured:&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%2F5fc47f3emxwxp7jmr5zd.jpg" 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%2F5fc47f3emxwxp7jmr5zd.jpg" alt="Flexible mode — traffic exposed between Cloudflare and origin" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;Flexible&lt;/strong&gt; mode. The encrypted leg ends at Cloudflare's edge. From Cloudflare onwards, the traffic flows in clear plain text across the public internet to wherever the origin lives.&lt;/p&gt;

&lt;p&gt;The terminology we use in security — "end-to-end encryption (E2E)" — was not fulfilled. The architecture was not secure. And finding the origin to attack it is not hard: an attacker can pull historical DNS records, scan certificate transparency logs, or use a handful of well-known reconnaissance techniques to identify the hosting data center, then probe the network path for an exposed link.&lt;/p&gt;

&lt;p&gt;For any enterprise that genuinely needs to protect data in transit — and that means anyone handling cardholder data under PCI DSS, healthcare data under HIPAA, or personal data under GDPR/ISO-27018 — &lt;strong&gt;Full (Strict)&lt;/strong&gt; is the only correct setting. The others are not options. They are gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Want to Honor Cloudflare Here
&lt;/h2&gt;

&lt;p&gt;The work those engineers do is excellent. Cloudflare has done more to make HTTPS accessible to the average website than almost any other company on the internet.&lt;/p&gt;

&lt;p&gt;The reason Flexible mode exists is practical: many origins genuinely cannot run TLS on day one, and Cloudflare offers Flexible so those sites can still get partial protection while they figure the rest out. If Cloudflare forced Full (Strict) as a default, a large share of new users would hit complex certificate errors on day one and walk away. The setting exists for a real reason.&lt;/p&gt;

&lt;p&gt;The core issue is that teams pick it as a permanent solution, and tell themselves they are "fully encrypted". I once flagged this exact pattern at another organization — only to find that their third-party security audit had missed it entirely, with the "encryption in transit" checkbox ticked clean in the final report.&lt;/p&gt;

&lt;p&gt;Whenever I sit with a customer, I push to understand the &lt;strong&gt;traffic flow from origin to destination&lt;/strong&gt; — every hop, every termination, every place encryption begins and ends. That single discipline surfaces more architecture problems and produces better solutions than almost any other discovery exercise I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Fix This
&lt;/h2&gt;

&lt;p&gt;If you read this and you don't know which mode you're in, you're probably not on Full (Strict). Here is how to get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Generate an origin certificate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The easiest path is Cloudflare's Origin CA. It's free, available from inside your Cloudflare dashboard under &lt;strong&gt;SSL/TLS → Origin Server → Create Certificate&lt;/strong&gt;, and the certificate is valid for up to 15 years. The defaults are fine: RSA 2048, hostnames &lt;code&gt;yourdomain.com&lt;/code&gt; and &lt;code&gt;*.yourdomain.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the certificate generates, Cloudflare will display two text blocks: the &lt;strong&gt;Origin Certificate&lt;/strong&gt; and the &lt;strong&gt;Private Key&lt;/strong&gt;. Save both to plain text files immediately. Cloudflare will never show the private key again, and losing it means regenerating from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Install the certificate on your origin server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For nginx, point &lt;code&gt;ssl_certificate&lt;/code&gt; and &lt;code&gt;ssl_certificate_key&lt;/code&gt; at the new files and reload. For Apache, the equivalent &lt;code&gt;SSLCertificateFile&lt;/code&gt; and &lt;code&gt;SSLCertificateKeyFile&lt;/code&gt; directives. On managed hosts like Hostinger, look for &lt;strong&gt;SSL → Install Custom SSL&lt;/strong&gt;, paste the certificate and private key into the corresponding fields, and leave the CA bundle field empty. Cloudflare Origin Certificates do not require a CA bundle because they are not meant to be publicly trusted — only Cloudflare's infrastructure trusts them. That's the whole point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Switch the Cloudflare SSL/TLS mode to Full (Strict).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your Cloudflare dashboard, navigate to &lt;strong&gt;SSL/TLS → Overview&lt;/strong&gt;, and select &lt;strong&gt;Full (Strict)&lt;/strong&gt;. This is the single click that matters most. If your origin certificate is installed correctly, your site will continue loading without errors. If it isn't, you'll see a 525 SSL Handshake Failed error — which is itself a useful signal that the cert install needs revisiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Verify with curl.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This step is where most tutorials stop and where mine starts. Open a terminal and run:&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;-vk&lt;/span&gt; &lt;span class="nt"&gt;--head&lt;/span&gt; &lt;span class="nt"&gt;--resolve&lt;/span&gt; yourdomain.com:443:YOUR_ORIGIN_IP https://yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--resolve&lt;/code&gt; flag tells curl to bypass Cloudflare and connect directly to your origin server's IP, while still requesting your domain name. The &lt;code&gt;-k&lt;/code&gt; flag tells curl to proceed even if the certificate is not trusted by your local machine.&lt;/p&gt;

&lt;p&gt;You should see two things in the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A successful TLS handshake&lt;/strong&gt; followed by &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt; (or whatever status your homepage returns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response headers identifying your origin&lt;/strong&gt; — for example, &lt;code&gt;Server: LiteSpeed&lt;/code&gt;, &lt;code&gt;platform: hostinger&lt;/code&gt;, or whatever your stack reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;* Trying 141.xxx.xx.xxx:443...
* Connected to yourdomain.com (141.xxx.xx.xxx) port 443
&amp;lt; &lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; Server: LiteSpeed&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; platform: hostinger&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; X-LiteSpeed-Cache: hit&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; alt-svc: h3=":443"; ma=2592000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handshake succeeded over TLS, the origin returned a clean 200, and the response identifies Hostinger's LiteSpeed server. That confirms the Cloudflare Origin Certificate is installed and being served correctly.&lt;/p&gt;

&lt;p&gt;Now run the same command &lt;strong&gt;without&lt;/strong&gt; the &lt;code&gt;-k&lt;/code&gt; flag:&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;-v&lt;/span&gt; &lt;span class="nt"&gt;--resolve&lt;/span&gt; yourdomain.com:443:YOUR_ORIGIN_IP https://yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now see a failure with this kind of error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schannel: SEC_E_UNTRUSTED_ROOT (0x80090325) - The certificate chain was issued by an authority that is not trusted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This error is not a bug. It is the verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What it means: the Cloudflare Origin Certificate is signed by Cloudflare Origin CA, which is intentionally not trusted by browsers, operating systems, or any public PKI. Only Cloudflare's edge servers trust it. So, when you connect directly to your origin, your local machine correctly says "I don't recognize this CA, refusing to trust this connection."&lt;/p&gt;

&lt;p&gt;That's exactly the design. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Random visitors who somehow discover your origin IP and try to connect directly will be rejected by their own TLS clients&lt;/li&gt;
&lt;li&gt;Only traffic flowing through Cloudflare will accept the certificate and proceed&lt;/li&gt;
&lt;li&gt;Combined with &lt;strong&gt;Full (Strict)&lt;/strong&gt; at the edge, you now have a closed loop — Cloudflare trusts only your origin cert, and only Cloudflare's edge talks to your origin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ran this curl test and the TLS handshake &lt;strong&gt;succeeded without &lt;code&gt;-k&lt;/code&gt;&lt;/strong&gt;, that means your origin is serving a publicly trusted certificate (Let's Encrypt, your host's default SSL, etc.) — which is &lt;em&gt;fine&lt;/em&gt; but means you have not yet swapped to the Cloudflare Origin Certificate. Go back and reinstall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 (bonus): Lock the origin down further.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enable &lt;strong&gt;Authenticated Origin Pulls&lt;/strong&gt; in Cloudflare so your origin will refuse any connection that does not present a Cloudflare client certificate. Combined with Full (Strict), this gives you mutual TLS between Cloudflare and your origin — even an attacker who got hold of your origin IP and somehow worked around the cert validation problem could not establish a connection.&lt;/p&gt;

&lt;p&gt;The whole exercise takes an experienced engineer about an hour. Most teams put it off for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But My Browser Shows a Different Certificate Than the One I Installed"
&lt;/h2&gt;

&lt;p&gt;This is the question every careful reader asks after walking through the fix. I asked it myself.&lt;/p&gt;

&lt;p&gt;You install the Cloudflare Origin Certificate on your server. You set Full (Strict). You open your site in a browser, click the padlock — and see a certificate &lt;strong&gt;issued by Let's Encrypt&lt;/strong&gt;, not Cloudflare.&lt;/p&gt;

&lt;p&gt;Did the installation fail? No. This is correct, and here's why:&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;two&lt;/strong&gt; certificates in this architecture, serving two different connection legs:&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%2F562an95fg7op0syr7mpb.jpg" 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%2F562an95fg7op0syr7mpb.jpg" alt="Two certificates — visitor-facing Let's Encrypt and origin-facing Cloudflare Origin CA" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cert A (visitor-facing):&lt;/strong&gt; Cloudflare automatically issues and presents this to your browser, via Let's Encrypt or Google Trust Services. It's publicly trusted. It's the cert your browser sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cert B (origin-facing):&lt;/strong&gt; The Cloudflare Origin Certificate you installed on your server. Cloudflare uses it when &lt;em&gt;it&lt;/em&gt; talks to your origin. Browsers never see this certificate. They have no reason to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, when you click the padlock and see Let's Encrypt — that's Cert A doing its job. The Origin Certificate (Cert B) is invisible to the browser by design. The curl test in Step 4 is the only direct way to confirm it from outside Cloudflare.&lt;/p&gt;

&lt;p&gt;If you don't internalize this two-certificate model, you'll be confused. Once you see it clearly, every other Cloudflare configuration question gets easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened After
&lt;/h2&gt;

&lt;p&gt;I explained all of this to the CTO and his team in that room. We walked through the modes. We drew the diagrams. We mapped what they had against what they thought they had.&lt;/p&gt;

&lt;p&gt;The CTO was thankful. The team was thankful. Within a week they had moved to Full (Strict), enabled Authenticated Origin Pulls, and added an item to their security review checklist so this would never sit unaddressed again.&lt;/p&gt;

&lt;p&gt;Honestly, moments like that bring me great joy. The job of a cloud presales solutions architect is not to sell the biggest possible solution. It is to ask the one question that turns a confident &lt;em&gt;"we are secure"&lt;/em&gt; into an honest &lt;em&gt;"we have work to do"&lt;/em&gt; — and then to help them do that work.&lt;/p&gt;

&lt;p&gt;If you take only one thing from this article, take this: &lt;strong&gt;the padlock in the browser is not a guarantee of end-to-end encryption.&lt;/strong&gt; It is a guarantee that the browser is talking to &lt;em&gt;something&lt;/em&gt; over TLS. What that something does next is up to you to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Question to Ask in Your Next Architecture Review
&lt;/h2&gt;

&lt;p&gt;The next time someone tells you &lt;em&gt;"we're HTTPS, we're on Cloudflare,"&lt;/em&gt; ask them this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"What SSL/TLS mode are you running in Cloudflare — Off, Flexible, Full, or Full (Strict)? And have you verified the origin certificate with curl?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If they don't know the answer to the first half, you already know the answer. They're not on Full (Strict). If they look puzzled at the second half, they haven't verified the fix even if they claim it's in place.&lt;/p&gt;

&lt;p&gt;Help them get there. The curl one-liner takes thirty seconds and removes all ambiguity.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://quabin.com/cloudflare-ssl-flexible-vs-full-strict/" rel="noopener noreferrer"&gt;quabin.com&lt;/a&gt; — I write about the questions I ask in real customer rooms, the ones that change architectures and surface the problems no one else noticed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>security</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
