<?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: Cyborgmachine</title>
    <description>The latest articles on DEV Community by Cyborgmachine (@cyborgmachine).</description>
    <link>https://dev.to/cyborgmachine</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%2F4141872%2Ff6444eaa-2a73-436b-afee-5e8397c92ab5.png</url>
      <title>DEV Community: Cyborgmachine</title>
      <link>https://dev.to/cyborgmachine</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cyborgmachine"/>
    <language>en</language>
    <item>
      <title>Why I Use Umami Instead of Google Analytics (And Don't Have a Cookie Banner)</title>
      <dc:creator>Cyborgmachine</dc:creator>
      <pubDate>Tue, 29 Sep 2026 17:26:06 +0000</pubDate>
      <link>https://dev.to/cyborgmachine/why-i-use-umami-instead-of-google-analytics-and-dont-have-a-cookie-banner-o3n</link>
      <guid>https://dev.to/cyborgmachine/why-i-use-umami-instead-of-google-analytics-and-dont-have-a-cookie-banner-o3n</guid>
      <description>&lt;p&gt;When I was setting up analytics for my blog, Google Analytics seemed like the obvious choice. But once I looked at what it actually requires — a tracking script, a cookie consent banner, a cookie policy page, conditional script loading — it felt like way too much for a personal site that doesn't run ads or retargeting.&lt;/p&gt;

&lt;p&gt;So I went with &lt;a href="https://github.com/umami-software/umami" rel="noopener noreferrer"&gt;Umami&lt;/a&gt; instead. It's open-source, self-hosted, and doesn't use cookies at all. No consent banner needed, and my data stays on my own server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Wrong With Google Analytics
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cookies and consent overhead.&lt;/strong&gt; GA sets first-party tracking cookies (&lt;code&gt;_ga&lt;/code&gt;, &lt;code&gt;_ga_&amp;lt;container-id&amp;gt;&lt;/code&gt;). Under GDPR, that means you need a proper cookie consent banner with category-by-category opt-in. And users who reject cookies simply won't be tracked — so your data is incomplete from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your data lives on Google's servers.&lt;/strong&gt; They process it for their own purposes too. You get a "free" analytics tool, and Google gets visibility into your traffic patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's heavy and overcomplicated.&lt;/strong&gt; The GA4 snippet loads around 90–135KB of JavaScript. The dashboard is built for enterprise use cases — overkill if you just want pageviews, referrers, and device stats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Umami
&lt;/h2&gt;

&lt;p&gt;Umami is an open-source, self-hosted analytics platform that gives you the metrics you need without the privacy baggage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No cookies&lt;/strong&gt; — sessions are identified using a hash of IP + user agent + website ID. Nothing personal is stored, no consent banner required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data, your server&lt;/strong&gt; — self-hosting on a VPS means no third party ever sees your analytics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt; — the tracking script is under 2KB (vs GA's 90KB+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean dashboard&lt;/strong&gt; — one page with pageviews, unique visitors, bounce rate, session duration, referrers, browsers, OS, devices, and geo data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to deploy&lt;/strong&gt; — Docker + PostgreSQL, that's it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Set It Up
&lt;/h2&gt;

&lt;p&gt;Here's the Docker Compose config I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;umami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/umami-software/umami:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;umami&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami&lt;/span&gt;
    &lt;span class="na"&gt;APP_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${UMAMI_APP_SECRET}&lt;/span&gt;
    &lt;span class="na"&gt;TWO_FACTOR_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${UMAMI_2FA_KEY}&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;umami-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
  &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/api/heartbeat"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="na"&gt;umami-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;umami-db&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;umami&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;umami&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${UMAMI_DB_PASSWORD}&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;umami-db-data:/var/lib/postgresql/data&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;umami"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;docker compose up -d&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in at port 3000 with &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;umami&lt;/code&gt;, change the password immediately&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings → Websites → Add website&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the tracking script into your site's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In Next.js I use the &lt;code&gt;Script&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Script&lt;/span&gt;
  &lt;span class="nx"&gt;defer&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-umami-instance.com/script.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-website-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;afterInteractive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  No Cookie Banner, By Design
&lt;/h2&gt;

&lt;p&gt;With Google Analytics I would have needed to build consent UI that loads before tracking, implement granular consent categories, write a cookie policy, and add conditional script loading. All of that for a personal blog.&lt;/p&gt;

&lt;p&gt;With Umami the script just loads. Every visitor gets tracked from the start, and nobody has to click through a consent popup. Cleaner UX, complete data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Give Up
&lt;/h2&gt;

&lt;p&gt;To be fair, there are trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No cross-site tracking&lt;/strong&gt; — you can't follow users across domains or build retargeting audiences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Google Ads integration&lt;/strong&gt; — if you run paid campaigns, you'll need GA for conversion attribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler event tracking&lt;/strong&gt; — Umami's custom events don't have GA4-level granularity for complex funnels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting maintenance&lt;/strong&gt; — you're responsible for updates and database backups (minimal, but it's there)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a blog or portfolio, none of these matter. You're not retargeting visitors or running ad funnels.&lt;/p&gt;

&lt;h2&gt;
  
  
  GA vs Umami at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Google Analytics&lt;/th&gt;
&lt;th&gt;Umami&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cookies&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;_ga&lt;/code&gt;, &lt;code&gt;_ga_&amp;lt;id&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie banner required&lt;/td&gt;
&lt;td&gt;Yes (GDPR)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data location&lt;/td&gt;
&lt;td&gt;Google's servers&lt;/td&gt;
&lt;td&gt;Your server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script size&lt;/td&gt;
&lt;td&gt;~90KB+&lt;/td&gt;
&lt;td&gt;&amp;lt;2KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hostable&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free (data as payment)&lt;/td&gt;
&lt;td&gt;Free (open source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Low (Docker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;td&gt;Complex, enterprise-focused&lt;/td&gt;
&lt;td&gt;Simple, one-page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-site tracking&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad integration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're running a personal site, blog, or small project — think about whether you actually need Google Analytics. Umami gives you everything that matters (who visits, where they come from, what they read) without the privacy infrastructure overhead. Setup takes about fifteen minutes if you already have Docker running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/umami-software/umami" rel="noopener noreferrer"&gt;Umami on GitHub →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>selfhosted</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Connecting Cloudflare Is Not Enough: How to Actually Get an A+ on SSL Labs</title>
      <dc:creator>Cyborgmachine</dc:creator>
      <pubDate>Sun, 27 Sep 2026 18:46:24 +0000</pubDate>
      <link>https://dev.to/cyborgmachine/connecting-cloudflare-is-not-enough-how-to-actually-get-an-a-on-ssl-labs-bji</link>
      <guid>https://dev.to/cyborgmachine/connecting-cloudflare-is-not-enough-how-to-actually-get-an-a-on-ssl-labs-bji</guid>
      <description>&lt;p&gt;If you've just added your site to Cloudflare, you might assume your TLS configuration is solid. You'd be wrong.&lt;/p&gt;

&lt;p&gt;By default, Cloudflare sets your minimum TLS version to &lt;strong&gt;1.0&lt;/strong&gt; — a protocol from 1999 that has known vulnerabilities and has been deprecated by every major browser. HSTS is off. Your SSL Labs score is likely sitting at a B or lower, and you don't even know it.&lt;/p&gt;

&lt;p&gt;I learned this the hard way while hardening my self-hosted site. Here's every setting I changed to go from Cloudflare's defaults to an A+ on SSL Labs — and why each one matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Cloudflare's Defaults
&lt;/h2&gt;

&lt;p&gt;Cloudflare is an excellent CDN and security layer, but it optimizes for maximum compatibility out of the box, not maximum security. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum TLS Version&lt;/strong&gt;: TLS 1.0 (vulnerable, formally deprecated by the IETF in 2021)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS&lt;/strong&gt;: Disabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS 1.3&lt;/strong&gt;: Enabled, but undermined by allowing TLS 1.0/1.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a reasonable default for Cloudflare. They serve millions of sites, some of which still need to support ancient clients. But for your modern web application, there's no reason to keep these settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Raise the Minimum TLS Version
&lt;/h2&gt;

&lt;p&gt;This is the single most impactful change. Go to &lt;strong&gt;SSL/TLS → Edge Certificates&lt;/strong&gt; and find "Minimum TLS Version."&lt;/p&gt;

&lt;p&gt;You'll see a dropdown defaulting to TLS 1.0. Change it to &lt;strong&gt;TLS 1.2&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnk4stkt2p1kqze2qzg44.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnk4stkt2p1kqze2qzg44.webp" alt="Cloudflare Minimum TLS Version setting" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why TLS 1.2 and not 1.3? Because TLS 1.2 is the floor — clients that support TLS 1.3 will still negotiate 1.3 automatically. Setting the minimum to 1.2 just cuts off the insecure protocols (1.0 and 1.1) that no modern browser needs anyway. Every major browser dropped TLS 1.0/1.1 support in 2020.&lt;/p&gt;

&lt;p&gt;While you're here, make sure &lt;strong&gt;TLS 1.3&lt;/strong&gt; is toggled on. It should be by default, but verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Enable HSTS
&lt;/h2&gt;

&lt;p&gt;HTTP Strict Transport Security tells browsers to always use HTTPS for your domain — no exceptions, no fallback to HTTP. Without it, a first-time visitor could be intercepted before the HTTPS redirect kicks in.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;SSL/TLS → Edge Certificates&lt;/strong&gt; and click "Change HSTS Settings." Enable everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable HSTS&lt;/strong&gt;: On&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max-Age&lt;/strong&gt;: 12 months (the maximum — shorter values weaken the protection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include Subdomains&lt;/strong&gt;: On (if all your subdomains support HTTPS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preload&lt;/strong&gt;: On&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-Sniff Header&lt;/strong&gt;: On&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fey0rm7tlowr89y49zqiy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fey0rm7tlowr89y49zqiy.webp" alt="Cloudflare HSTS settings" width="800" height="1177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;A word of caution:&lt;/strong&gt; HSTS is a commitment. Once a browser receives the HSTS header, it will refuse to connect over HTTP for the duration of the max-age. If you later need to serve your site over plain HTTP (you shouldn't), you'll have a bad time. Make sure HTTPS works perfectly before enabling this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The "Preload" option adds the &lt;code&gt;preload&lt;/code&gt; directive to your HSTS header, which signals that your domain is eligible for the HSTS Preload List built into browsers. Once on the list, even the very first visit will be forced to HTTPS. But enabling the toggle alone isn't enough — you still need to manually submit your domain at &lt;a href="https://hstspreload.org" rel="noopener noreferrer"&gt;hstspreload.org&lt;/a&gt; after configuring these settings. Inclusion can take weeks or months to propagate through browser releases, and it's the hardest setting to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Force HTTPS Everywhere
&lt;/h2&gt;

&lt;p&gt;Two more toggles in the same section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvav0afo7ev46ttuwtxon.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvav0afo7ev46ttuwtxon.webp" alt="Always Use HTTPS and Automatic HTTPS Rewrites" width="799" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always Use HTTPS:&lt;/strong&gt; On. This redirects all HTTP requests to HTTPS across your entire domain. Simple but essential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic HTTPS Rewrites&lt;/strong&gt;: On. This fixes mixed content issues by rewriting &lt;code&gt;http://&lt;/code&gt; resource URLs to &lt;code&gt;https://&lt;/code&gt; on the fly. It won't fix everything (inline scripts, for example), but it catches most third-party embeds and legacy URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Enable Certificate Transparency Monitoring
&lt;/h2&gt;

&lt;p&gt;This one isn't about your SSL Labs score, but it's worth enabling while you're in the settings. &lt;strong&gt;Certificate Transparency Monitoring&lt;/strong&gt; sends you an email whenever a Certificate Authority issues a certificate for your domain.&lt;/p&gt;

&lt;p&gt;Why does this matter? If someone manages to get a fraudulent certificate for your domain (through a compromised CA or a social engineering attack), you'll know about it immediately. It's free, it's a toggle, and there's no reason not to turn it on.&lt;/p&gt;

&lt;p&gt;Don't forget to actually add your email address to the notification field — the toggle alone doesn't do anything without a recipient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudo0k7bc1eewe00s3ntp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudo0k7bc1eewe00s3ntp.webp" alt="Certificate Transparency Monitoring" width="799" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Harden Your Origin Server
&lt;/h2&gt;

&lt;p&gt;Cloudflare handles the connection between the visitor and Cloudflare's edge, but there's a second TLS connection between Cloudflare and your origin server. If your SSL/TLS encryption mode isn't set to &lt;strong&gt;Full (Strict)&lt;/strong&gt;, this connection might not be properly validated.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;SSL/TLS → Overview&lt;/strong&gt; and set the mode to &lt;strong&gt;Full (Strict)&lt;/strong&gt;. This requires a valid certificate on your origin — either from a public CA (Let's Encrypt via Certbot works perfectly) or a Cloudflare Origin Certificate.&lt;/p&gt;

&lt;p&gt;If you're running Nginx, make sure your origin config only allows modern protocols too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mirrors what Cloudflare negotiates on the edge and ensures there's no weak link in the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After making these changes, run your domain through &lt;a href="https://www.ssllabs.com/ssltest/" rel="noopener noreferrer"&gt;SSL Labs&lt;/a&gt; and you should see an &lt;strong&gt;A+&lt;/strong&gt; rating.&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%2Flv26knjz7osawr1rhkuy.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%2Flv26knjz7osawr1rhkuy.png" alt="SSL Labs test result showing A+ rating" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Minimum TLS version → TLS 1.2&lt;/li&gt;
&lt;li&gt;✅ TLS 1.3 → On&lt;/li&gt;
&lt;li&gt;✅ HSTS → On (Max-Age 12 months, includeSubDomains, Preload)&lt;/li&gt;
&lt;li&gt;✅ Always Use HTTPS → On&lt;/li&gt;
&lt;li&gt;✅ Automatic HTTPS Rewrites → On&lt;/li&gt;
&lt;li&gt;✅ Certificate Transparency Monitoring → On&lt;/li&gt;
&lt;li&gt;✅ SSL/TLS mode → Full (Strict)&lt;/li&gt;
&lt;li&gt;✅ Origin server → TLSv1.2 + TLSv1.3 only, strong ciphers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One Thing You Can't Fix on the Free Plan
&lt;/h2&gt;

&lt;p&gt;You might still see some CBC cipher suites in your SSL Labs report under TLS 1.2. These are weaker than the preferred GCM and CHACHA20 ciphers but still considered safe in practice.&lt;/p&gt;

&lt;p&gt;On Cloudflare's free plan, you can't customize the edge cipher suite — that requires the Business or Enterprise plan. This won't prevent you from getting an A+, but it's worth knowing about if you're aiming for a theoretically perfect configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother?
&lt;/h2&gt;

&lt;p&gt;A fair question. Your site works fine with Cloudflare's defaults, and the average user will never notice the difference.&lt;/p&gt;

&lt;p&gt;But TLS hardening isn't about showing off a green badge. It's about eliminating known-weak protocols that exist only for backward compatibility with clients that shouldn't be accessing your site anyway. TLS 1.0 has known vulnerabilities (BEAST), and older SSL protocols it often coexists with are vulnerable to POODLE. HSTS prevents SSL stripping attacks. These aren't theoretical risks — they're well-documented attack vectors.&lt;/p&gt;

&lt;p&gt;It takes about five minutes to change these settings. There's no performance penalty. The only "cost" is dropping support for Internet Explorer on Windows XP — and that's a feature, not a bug.&lt;/p&gt;




&lt;p&gt;Originally on &lt;a href="https://cyborgmachine.dev" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. More stuff on web dev, devops, privacy, LLMs, and self-hosting there.&lt;/p&gt;

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