<?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: Håkan Fägnell</title>
    <description>The latest articles on DEV Community by Håkan Fägnell (@hkan_fgnell_322b4aba3d6).</description>
    <link>https://dev.to/hkan_fgnell_322b4aba3d6</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%2F4006570%2F4c0347f7-187a-4a19-8b8f-b0ea6540060a.jpg</url>
      <title>DEV Community: Håkan Fägnell</title>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hkan_fgnell_322b4aba3d6"/>
    <language>en</language>
    <item>
      <title>ERR_CERT_DATE_INVALID: Fix It on Any Device | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Mon, 14 Sep 2026 04:46:19 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/errcertdateinvalid-fix-it-on-any-device-fortifynet-4egn</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/errcertdateinvalid-fix-it-on-any-device-fortifynet-4egn</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1524592094714-0f0654e20314%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1524592094714-0f0654e20314%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="A hand holding a wristwatch, symbolising the device clock that Chrome checks a certificate's dates against" width="1600" height="1162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NET::ERR_CERT_DATE_INVALID is Chrome's way of saying the website's certificate is not valid right now: either the certificate has expired (or is not yet valid), or the clock on your own device is wrong, so a perfectly good certificate looks expired. Google's own data shows the second cause is far more common than most people expect: on Windows, a wrong system clock is behind 33.5% of all Chrome certificate warnings, versus 4.23% for certificates that have really expired (Acer et al., ACM CCS 2017). This guide shows you how to tell the two apart in under a minute, how to fix each one as a visitor or a site owner, and how to stop the error coming back.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What NET::ERR_CERT_DATE_INVALID actually means
&lt;/h2&gt;

&lt;p&gt;Every TLS certificate carries two timestamps: a start date (notBefore) and an end date (notAfter). When Chrome, Edge or any Chromium-based browser opens an HTTPS connection, it compares those two dates with the current time as reported by your device. If "now" falls outside that window, the browser refuses to trust the certificate and shows a full-page warning with the code NET::ERR_CERT_DATE_INVALID.&lt;/p&gt;

&lt;p&gt;There are only three ways to land outside the window, and they point at different people:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Whose problem&lt;/th&gt;
&lt;th&gt;What Chrome shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your device clock is wrong (usually years behind)&lt;/td&gt;
&lt;td&gt;The visitor&lt;/td&gt;
&lt;td&gt;"Your clock is behind" or "Your clock is ahead", with no option to proceed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The site's certificate has expired&lt;/td&gt;
&lt;td&gt;The site owner&lt;/td&gt;
&lt;td&gt;"Your connection is not private", NET::ERR_CERT_DATE_INVALID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The certificate was issued minutes ago and your clock is slightly behind&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;"Your connection is not private", NET::ERR_CERT_DATE_INVALID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other browsers report the same underlying condition under different names. Firefox uses SEC_ERROR_EXPIRED_CERTIFICATE for an expired certificate and MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE for one whose start date is still in the future. Safari shows "This Connection Is Not Private" and mentions the expiry only in the details. The fixes below apply to all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visitor or website owner? Find out in 30 seconds
&lt;/h2&gt;

&lt;p&gt;Before changing anything, work out which side of the connection is wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open two or three other HTTPS sites.&lt;/strong&gt; If they all show the same warning, the problem is your device, almost certainly its clock. If only one site fails, the problem is on that server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the date in your taskbar or menu bar.&lt;/strong&gt; A device that has just replaced its battery or sat unused for months often boots with a date years in the past. If the year is wrong, you have found the cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the site from another device or network.&lt;/strong&gt; If your phone on mobile data opens the site without a warning, your computer is the odd one out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you own the site, check the certificate from outside.&lt;/strong&gt; A &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;free FortifyNet scan&lt;/a&gt; reads the live certificate from the internet and shows its exact notBefore and notAfter dates, independent of any device clock.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this error is more often your clock than the website
&lt;/h2&gt;

&lt;p&gt;Google and Carnegie Mellon researchers studied more than 300 million Chrome certificate warning reports to find out what really triggers them. The result, published as "Where the Wild Warnings Are" at ACM CCS 2017, is striking: on Windows, an incorrect system clock caused 33.5% of all certificate warnings, eight times more than certificates that had really expired. On macOS the split was 8.71% versus 4.39%, and on Android 8.46% versus 2.73%.&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%2Fquickchart.io%2Fchart%3Fv%3D3%26w%3D700%26h%3D400%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Windows%2522%252C%2522macOS%2522%252C%2522ChromeOS%2522%252C%2522Android%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Wrong%2520device%2520clock%2522%252C%2522data%2522%253A%255B33.5%252C8.71%252C1.72%252C8.46%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Expired%2520or%2520not%2520yet%2520valid%2520server%2520certificate%2522%252C%2522data%2522%253A%255B4.23%252C4.39%252C2.8%252C2.73%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Chrome%2520certificate%2520date%2520warnings%2520by%2520cause%2520%2528percent%2520of%2520reports%2529%2522%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Atrue%252C%2522position%2522%253A%2522bottom%2522%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Percent%2520of%2520warning%2520reports%2522%257D%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fv%3D3%26w%3D700%26h%3D400%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522Windows%2522%252C%2522macOS%2522%252C%2522ChromeOS%2522%252C%2522Android%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Wrong%2520device%2520clock%2522%252C%2522data%2522%253A%255B33.5%252C8.71%252C1.72%252C8.46%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%252C%257B%2522label%2522%253A%2522Expired%2520or%2520not%2520yet%2520valid%2520server%2520certificate%2522%252C%2522data%2522%253A%255B4.23%252C4.39%252C2.8%252C2.73%255D%252C%2522backgroundColor%2522%253A%2522%2523f59e0b%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Chrome%2520certificate%2520date%2520warnings%2520by%2520cause%2520%2528percent%2520of%2520reports%2529%2522%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Atrue%252C%2522position%2522%253A%2522bottom%2522%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Percent%2520of%2520warning%2520reports%2522%257D%257D%257D%257D%257D" alt="Grouped bar chart comparing the share of Chrome certificate warnings caused by a wrong device clock with the share caused by an expired or not yet valid server certificate, for Windows, macOS, ChromeOS and Android" width="1400" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Share of Chrome certificate warning reports caused by a wrong device clock versus a genuinely expired or not yet valid server certificate, by operating system. Source: Acer et al., "Where the Wild Warnings Are", ACM CCS 2017, Table 1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two more details from the same study matter for the fix. Nearly all wrong clocks were in the past, not the future: the client clock was more than 24 hours behind in 6.7% of reports and more than 24 hours ahead in only 0.05%. And when a certificate had genuinely expired, it was usually a recent lapse: 57% of expired certificates had expired less than 30 days earlier, which is the signature of a renewal that somebody forgot, not a site that was abandoned.&lt;/p&gt;

&lt;p&gt;The Chrome team used this data to build the "Your clock is behind" screen you may have seen. It first relied on a simple heuristic that compared the system time with the browser's build date, which caught only 68% of clock errors. In May 2017 Chrome shipped a secure time service that asks a Google server for the real time when it sees a date error, raising detection to 96%. When Chrome shows the clock-specific screen, 53% of users fixed their clock before dismissing it, compared with 3.9% when they only saw the generic warning. That screen deliberately has no "proceed anyway" link, so the only way past it is to correct the clock.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fix ERR_CERT_DATE_INVALID as a visitor
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Windows 10 and 11
&lt;/h3&gt;

&lt;p&gt;Open Settings, go to Time &amp;amp; language, then Date &amp;amp; time. Turn on "Set time automatically" and "Set time zone automatically", then click "Sync now" under Additional settings. If the clock drifts again after every restart, the CMOS battery on the motherboard is probably flat; the researchers named dying CMOS batteries as one likely reason so many Windows clocks are wrong. From an administrator command prompt you can also force a resync with &lt;code&gt;w32tm /resync&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;

&lt;p&gt;Open System Settings, then General, then Date &amp;amp; Time, and turn on "Set time and date automatically". Make sure the time zone is also set automatically. If the option is greyed out, disconnect from any VPN first and try again.&lt;/p&gt;
&lt;h3&gt;
  
  
  Android and iPhone
&lt;/h3&gt;

&lt;p&gt;On Android, open Settings, then System, then Date &amp;amp; time, and enable "Set time automatically" (some manufacturers call it "Use network-provided time"). On iPhone and iPad, go to Settings, General, Date &amp;amp; Time, and enable "Set Automatically". A device that has been powered off for a long time or had its SIM removed can boot with a stale date.&lt;/p&gt;
&lt;h3&gt;
  
  
  If the clock is right and the error persists
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old devices and expired roots.&lt;/strong&gt; Certificates are trusted through a chain that ends at a root certificate on your device, and roots expire too. When the cross-signed Let's Encrypt chain expired on 30 September 2024, devices running Android 7.0 and earlier stopped trusting sites that use Let's Encrypt certificates, while Android 7.1 and newer, which already trusted ISRG Root X1 directly, were unaffected (&lt;a href="https://letsencrypt.org/2023/07/10/cross-sign-expiration.html" rel="noopener noreferrer"&gt;Let's Encrypt&lt;/a&gt;). On a device that cannot be updated, Firefox is often the workaround, because it ships its own root store instead of using the operating system's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Antivirus HTTPS scanning.&lt;/strong&gt; Security products that inspect HTTPS traffic install their own root certificate and re-sign every site you visit. The CCS 2017 study traced a wave of date errors on properly configured sites to antivirus software whose own root certificate had expired. Temporarily disabling HTTPS or "web shield" scanning tells you whether that is the cause; updating the product usually fixes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the browser and clear the SSL state.&lt;/strong&gt; An outdated browser may lack newer root certificates. On Windows, Internet Options, Content, "Clear SSL state" clears cached certificates that Chrome and Edge share with the system.&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%2Fimages.unsplash.com%2Fphoto-1434493789847-2f02dc6ca35d%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1434493789847-2f02dc6ca35d%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="A laptop open on a desk, the device whose system clock determines whether a certificate looks valid" width="1600" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Fix ERR_CERT_DATE_INVALID as a site owner
&lt;/h2&gt;

&lt;p&gt;If visitors are reporting the error and the site fails from more than one device, the certificate itself is the problem. Work through these steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: read the real dates.&lt;/strong&gt; Query the certificate the server is actually serving with OpenSSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl s_client -connect example.com:443 -servername example.com 2&amp;gt;/dev/null | openssl x509 -noout -dates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output prints &lt;code&gt;notBefore&lt;/code&gt; and &lt;code&gt;notAfter&lt;/code&gt;. If &lt;code&gt;notAfter&lt;/code&gt; is in the past, the certificate has expired. If &lt;code&gt;notBefore&lt;/code&gt; is in the future, it is not yet valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: renew and reload.&lt;/strong&gt; With an ACME client such as Certbot, &lt;code&gt;sudo certbot renew&lt;/code&gt; obtains a new certificate, but the web server keeps serving the old one from memory until you reload it (&lt;code&gt;sudo systemctl reload nginx&lt;/code&gt; or the Apache equivalent). Our &lt;a href="https://fortifynet.com/blog/how-to-renew-ssl-certificate" rel="noopener noreferrer"&gt;renewal guide&lt;/a&gt; covers every major platform and CDN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: check the whole chain.&lt;/strong&gt; Your leaf certificate can be valid while an intermediate in the chain has expired. Deploy the full chain the CA gave you (&lt;code&gt;fullchain.pem&lt;/code&gt;, not &lt;code&gt;cert.pem&lt;/code&gt;), and check every endpoint that terminates TLS: the origin server, the load balancer and the CDN edge can each hold a different copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: beware the "not yet valid" trap.&lt;/strong&gt; A certificate that becomes valid at the exact minute it is issued will fail for every visitor whose clock is even slightly behind. The CCS study recorded a spike in clock-related warnings on Windows in September 2016 when Google rolled out newly issued certificates at short notice. Most public CAs now backdate the start time slightly for this reason, but if you run a private CA, set notBefore at least an hour in the past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: verify from outside.&lt;/strong&gt; There is no propagation delay for certificates. As soon as the server is reloaded, an external scan shows the new dates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;How to recognise it&lt;/th&gt;
&lt;th&gt;Who fixes it&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Certificate expired&lt;/td&gt;
&lt;td&gt;notAfter in the past; error on every device&lt;/td&gt;
&lt;td&gt;Site owner&lt;/td&gt;
&lt;td&gt;Renew, install full chain, reload server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate not yet valid&lt;/td&gt;
&lt;td&gt;notBefore in the future; error for some visitors right after deployment&lt;/td&gt;
&lt;td&gt;Site owner&lt;/td&gt;
&lt;td&gt;Wait, or issue with a backdated start time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermediate or root expired&lt;/td&gt;
&lt;td&gt;Leaf looks fine, chain check fails&lt;/td&gt;
&lt;td&gt;Site owner (or CA)&lt;/td&gt;
&lt;td&gt;Install the updated chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device clock wrong&lt;/td&gt;
&lt;td&gt;Every HTTPS site fails; Chrome says "Your clock is behind"&lt;/td&gt;
&lt;td&gt;Visitor&lt;/td&gt;
&lt;td&gt;Enable automatic time, replace CMOS battery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old device missing new roots&lt;/td&gt;
&lt;td&gt;Only newer sites or certain CAs fail&lt;/td&gt;
&lt;td&gt;Visitor&lt;/td&gt;
&lt;td&gt;Update OS, or use a browser with its own root store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antivirus root expired&lt;/td&gt;
&lt;td&gt;Error on well-configured sites, disappears when HTTPS scanning is off&lt;/td&gt;
&lt;td&gt;Visitor&lt;/td&gt;
&lt;td&gt;Update or reconfigure the security product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why date errors will get more frequent, not less
&lt;/h2&gt;

&lt;p&gt;Certificate lifetimes are shrinking. Under CA/Browser Forum ballot SC-081v3, the maximum lifetime of a public TLS certificate dropped from 398 to 200 days on 15 March 2026, falls to 100 days in March 2027 and to 47 days in March 2029 (&lt;a href="https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/" rel="noopener noreferrer"&gt;CA/Browser Forum&lt;/a&gt;). Let's Encrypt is moving its default from 90 to 45 days (&lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45" rel="noopener noreferrer"&gt;Let's Encrypt&lt;/a&gt;). Every renewal is a chance to miss the deadline, so a site that renews by hand will meet NET::ERR_CERT_DATE_INVALID several times a year. Automating issuance with ACME, configuring the reload hook, and monitoring expiry from outside are no longer optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/your-connection-is-not-private" rel="noopener noreferrer"&gt;"Your connection is not private": what it means and how to fix it&lt;/a&gt; covers the warning page this error appears on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/ssl-certificate-expired" rel="noopener noreferrer"&gt;SSL certificate expired: what happens and how to fix it fast&lt;/a&gt; goes deeper on the server-side cause.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/how-to-renew-ssl-certificate" rel="noopener noreferrer"&gt;How to renew an SSL certificate&lt;/a&gt; walks through renewal on every major platform.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/err-cert-authority-invalid" rel="noopener noreferrer"&gt;ERR_CERT_AUTHORITY_INVALID: causes and fixes&lt;/a&gt; handles the chain errors that often follow a rushed renewal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is NET::ERR_CERT_DATE_INVALID dangerous?&lt;/strong&gt;&lt;br&gt;
Usually not, but Chrome cannot tell a harmless clock error from an attack, so it blocks the page. Fix the clock or wait for the site to renew instead of clicking through, especially on banking or login pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does Chrome say "Your clock is behind" on one site but "connection is not private" on another?&lt;/strong&gt;&lt;br&gt;
Chrome only shows the clock-specific screen when its secure time check confirms your clock is wrong. If the check cannot complete within three seconds, or the difference is small, you get the generic warning with the same error code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I bypass the error?&lt;/strong&gt;&lt;br&gt;
On the generic warning you can click Advanced and proceed, unless the site uses HSTS, which removes that option. On the "Your clock is behind" screen there is no bypass by design; correcting the clock is the only way through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My clock is correct and only one site fails. What now?&lt;/strong&gt;&lt;br&gt;
The site's certificate has expired or its chain is broken. Nothing on your device will fix that; tell the site owner, or check the site's certificate dates yourself with an external scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I stop this happening on my own site?&lt;/strong&gt;&lt;br&gt;
Automate renewal with ACME, configure the deploy hook that reloads the server, and monitor expiry from outside with alerts at 30, 14 and 7 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your certificate dates before your visitors do
&lt;/h2&gt;

&lt;p&gt;A wrong clock is the visitor's problem; an expired certificate is yours. &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;Run a free FortifyNet scan&lt;/a&gt; to see your certificate's exact validity dates, the full chain, protocol configuration, security headers, DNS and email authentication in about 60 seconds. No signup required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sources: Acer et al., &lt;a href="https://research.google/pubs/where-the-wild-warnings-are-root-causes-of-chrome-certificate-errors/" rel="noopener noreferrer"&gt;"Where the Wild Warnings Are: Root Causes of Chrome HTTPS Certificate Errors"&lt;/a&gt;, ACM CCS 2017; &lt;a href="https://letsencrypt.org/2023/07/10/cross-sign-expiration.html" rel="noopener noreferrer"&gt;Let's Encrypt, Shortening the Chain of Trust&lt;/a&gt;; &lt;a href="https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/" rel="noopener noreferrer"&gt;CA/Browser Forum, Ballot SC-081v3&lt;/a&gt;; &lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45" rel="noopener noreferrer"&gt;Let's Encrypt, From 90 to 45&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/err-cert-date-invalid" rel="noopener noreferrer"&gt;fortifynet.com/blog/err-cert-date-invalid&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssl</category>
      <category>certificates</category>
      <category>browsererrors</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>How to Read DMARC Reports (Aggregate XML Guide) | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Fri, 11 Sep 2026 04:46:27 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/how-to-read-dmarc-reports-aggregate-xml-guide-fortifynet-1jj8</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/how-to-read-dmarc-reports-aggregate-xml-guide-fortifynet-1jj8</guid>
      <description>&lt;p&gt;To read a DMARC report, open the compressed XML attachment, look at &lt;code&gt;policy_published&lt;/code&gt; to confirm which policy receivers applied, then go through every &lt;code&gt;record&lt;/code&gt; block: the &lt;code&gt;source_ip&lt;/code&gt; and &lt;code&gt;count&lt;/code&gt; tell you who sent how much, &lt;code&gt;policy_evaluated&lt;/code&gt; gives the DMARC verdict, and &lt;code&gt;auth_results&lt;/code&gt; shows the raw SPF and DKIM results underneath it. A row that passes SPF or DKIM but fails DMARC has an alignment problem. A row that fails everything is either a forgotten sender or someone spoofing you. This guide walks through a real report field by field, shows the seven patterns you will see most often, and explains what to do about each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a DMARC report actually is
&lt;/h2&gt;

&lt;p&gt;Every mail receiver that supports DMARC (Google, Microsoft 365, Yahoo, Mail.ru, Fastmail, Zoho and many others) counts the messages it sees claiming your domain in the From header. Once per UTC day it packs those counts into an XML file, compresses it, and emails it to the address in the &lt;code&gt;rua=&lt;/code&gt; tag of your DMARC record. These are aggregate reports, sometimes called RUA reports. They contain no message content, only counts per sending IP with the authentication results for each.&lt;/p&gt;

&lt;p&gt;Per &lt;a href="https://datatracker.ietf.org/doc/rfc9990/" rel="noopener noreferrer"&gt;RFC 9990&lt;/a&gt;, published by the IETF in May 2026 as the Standards Track specification for aggregate reporting, the reporting period is typically a single UTC day starting at 0000 UTC, the XML must be gzip compressed in normal circumstances, and each report covers exactly one policy domain. Reports arrive whether your policy is &lt;code&gt;p=none&lt;/code&gt;, &lt;code&gt;p=quarantine&lt;/code&gt; or &lt;code&gt;p=reject&lt;/code&gt;. They are the only feedback loop DMARC gives you, and reading them is the difference between enforcing safely and blocking your own invoices.&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%2Fquickchart.io%2Fchart%3Fv%3D3%26w%3D760%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522DMARC%2520record%2522%252C%2522Aggregate%2520%2528rua%2529%2520reporting%2522%252C%2522Enforcement%2520%2528quarantine%252Freject%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Domains%2522%252C%2522data%2522%253A%255B937931%252C553000%252C411935%255D%252C%2522backgroundColor%2522%253A%255B%2522%25232563eb%2522%252C%2522%2523f59e0b%2522%252C%2522%2523059669%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522DMARC%2520maturity%2520funnel%252C%2520top%25201.8M%2520domains%2520%2528Mar%25202026%2529%2522%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Domains%2522%257D%252C%2522beginAtZero%2522%253Atrue%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fv%3D3%26w%3D760%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522DMARC%2520record%2522%252C%2522Aggregate%2520%2528rua%2529%2520reporting%2522%252C%2522Enforcement%2520%2528quarantine%252Freject%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Domains%2522%252C%2522data%2522%253A%255B937931%252C553000%252C411935%255D%252C%2522backgroundColor%2522%253A%255B%2522%25232563eb%2522%252C%2522%2523f59e0b%2522%252C%2522%2523059669%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522DMARC%2520maturity%2520funnel%252C%2520top%25201.8M%2520domains%2520%2528Mar%25202026%2529%2522%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Domains%2522%257D%252C%2522beginAtZero%2522%253Atrue%257D%257D%257D%257D" alt="Bar chart showing the DMARC maturity funnel across the top 1.8 million domains in March 2026: 937,931 with a DMARC record, about 553,000 with aggregate reporting, 411,935 at enforcement"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Of the top 1.8 million domains, 937,931 (52.1%) publish DMARC, over 553,000 collect aggregate reports, and 411,935 enforce. Only around 9% do both. Source: EasyDMARC, 2026 DMARC Adoption Report, 30 March 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The gap in that chart is the problem this article solves. EasyDMARC's March 2026 study of the top 1.8 million domains found that only about 9% of domains combine an enforcement policy with reporting. A separate DMARCguard scan of 5,499,028 Tranco domains on 27 February 2026 found that 776,918 of the 1,670,975 DMARC domains (46.5%) have no &lt;code&gt;rua=&lt;/code&gt; tag at all. Most domain owners who do receive reports never open them, because raw XML looks hostile. It is not. There are only three sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: open the attachment
&lt;/h2&gt;

&lt;p&gt;A genuine report email has a subject like &lt;code&gt;Report Domain: example.com Submitter: google.com Report-ID: 1234567890&lt;/code&gt; and a single attachment. RFC 9990 section 3.5.2 fixes the filename format as &lt;code&gt;receiver!policy-domain!begin-timestamp!end-timestamp.xml.gz&lt;/code&gt;, for example &lt;code&gt;google.com!example.com!1757462400!1757548799.xml.gz&lt;/code&gt;. The two numbers are Unix timestamps for the start and end of the reporting window, so you can tell the covered day before you open anything.&lt;/p&gt;

&lt;p&gt;Decompress it on the command line:&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;gunzip &lt;/span&gt;google.com!example.com!1757462400!1757548799.xml.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some senders, including Microsoft, use zip instead of gzip. In that case use &lt;code&gt;unzip&lt;/code&gt; on the file. To pretty-print the result for reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xmllint &lt;span class="nt"&gt;--format&lt;/span&gt; google.com!example.com!1757462400!1757548799.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any text editor works too. What you will see is a single &lt;code&gt;feedback&lt;/code&gt; element containing &lt;code&gt;report_metadata&lt;/code&gt;, &lt;code&gt;policy_published&lt;/code&gt; and one or more &lt;code&gt;record&lt;/code&gt; blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: read the three sections
&lt;/h2&gt;

&lt;p&gt;Here is a report trimmed to one record, using the structure from RFC 9990 Appendix B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;feedback&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"urn:ietf:params:xml:ns:dmarc-2.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;report_metadata&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;org_name&amp;gt;&lt;/span&gt;google.com&lt;span class="nt"&gt;&amp;lt;/org_name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;email&amp;gt;&lt;/span&gt;noreply-dmarc-support@google.com&lt;span class="nt"&gt;&amp;lt;/email&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;report_id&amp;gt;&lt;/span&gt;1757548799.example.com&lt;span class="nt"&gt;&amp;lt;/report_id&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;date_range&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;begin&amp;gt;&lt;/span&gt;1757462400&lt;span class="nt"&gt;&amp;lt;/begin&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;end&amp;gt;&lt;/span&gt;1757548799&lt;span class="nt"&gt;&amp;lt;/end&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/date_range&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/report_metadata&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;policy_published&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;example.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;adkim&amp;gt;&lt;/span&gt;r&lt;span class="nt"&gt;&amp;lt;/adkim&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;aspf&amp;gt;&lt;/span&gt;r&lt;span class="nt"&gt;&amp;lt;/aspf&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;quarantine&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;sp&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/sp&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/policy_published&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;record&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;row&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;source_ip&amp;gt;&lt;/span&gt;192.0.2.123&lt;span class="nt"&gt;&amp;lt;/source_ip&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;count&amp;gt;&lt;/span&gt;123&lt;span class="nt"&gt;&amp;lt;/count&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;policy_evaluated&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;disposition&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/disposition&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dkim&amp;gt;&lt;/span&gt;pass&lt;span class="nt"&gt;&amp;lt;/dkim&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;spf&amp;gt;&lt;/span&gt;fail&lt;span class="nt"&gt;&amp;lt;/spf&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/policy_evaluated&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/row&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;identifiers&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;envelope_from&amp;gt;&lt;/span&gt;example.com&lt;span class="nt"&gt;&amp;lt;/envelope_from&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;header_from&amp;gt;&lt;/span&gt;example.com&lt;span class="nt"&gt;&amp;lt;/header_from&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/identifiers&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;auth_results&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;dkim&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;example.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;result&amp;gt;&lt;/span&gt;pass&lt;span class="nt"&gt;&amp;lt;/result&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;selector&amp;gt;&lt;/span&gt;abc123&lt;span class="nt"&gt;&amp;lt;/selector&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/dkim&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;spf&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;example.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;result&amp;gt;&lt;/span&gt;fail&lt;span class="nt"&gt;&amp;lt;/result&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/spf&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/auth_results&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/record&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/feedback&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older reports use the namespace &lt;code&gt;http://dmarc.org/dmarc-xml/0.1&lt;/code&gt; from RFC 7489. The fields are the same, so read them the same way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;th&gt;How to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;report_metadata&lt;/code&gt; / &lt;code&gt;org_name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Which receiver generated the report&lt;/td&gt;
&lt;td&gt;Group findings per receiver; Google and Microsoft together usually cover most of your volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unix timestamps of the covered window&lt;/td&gt;
&lt;td&gt;Match spikes to campaigns or incidents on that day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;policy_published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your DMARC record as the receiver read it&lt;/td&gt;
&lt;td&gt;If &lt;code&gt;p&lt;/code&gt; differs from what you published, DNS had not propagated or you have two records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The IP that delivered the message to the receiver&lt;/td&gt;
&lt;td&gt;Reverse-lookup it; this is your sender inventory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Messages from that IP with identical results&lt;/td&gt;
&lt;td&gt;Big counts are your real mail streams; tiny counts are noise or probes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;policy_evaluated&lt;/code&gt; / &lt;code&gt;dkim&lt;/code&gt; and &lt;code&gt;spf&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Aligned DKIM and aligned SPF results&lt;/td&gt;
&lt;td&gt;This is the DMARC verdict, after alignment, not the raw check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disposition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What the receiver did: &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;quarantine&lt;/code&gt;, &lt;code&gt;reject&lt;/code&gt;, or &lt;code&gt;pass&lt;/code&gt; in RFC 9990 reports&lt;/td&gt;
&lt;td&gt;Tells you what actually happened to the mail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Why a receiver overrode your policy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mailing_list&lt;/code&gt;, &lt;code&gt;trusted_forwarder&lt;/code&gt;, &lt;code&gt;local_policy&lt;/code&gt;, &lt;code&gt;policy_test_mode&lt;/code&gt; or &lt;code&gt;other&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;header_from&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The domain in the visible From header&lt;/td&gt;
&lt;td&gt;The domain that DMARC is protecting in this row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envelope_from&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The RFC5321.MailFrom domain SPF checked&lt;/td&gt;
&lt;td&gt;If it differs from &lt;code&gt;header_from&lt;/code&gt;, SPF cannot align&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;auth_results&lt;/code&gt; / &lt;code&gt;dkim&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Raw DKIM result, signing domain and selector&lt;/td&gt;
&lt;td&gt;A pass here with a fail in &lt;code&gt;policy_evaluated&lt;/code&gt; means the &lt;code&gt;d=&lt;/code&gt; domain is not yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;auth_results&lt;/code&gt; / &lt;code&gt;spf&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Raw SPF result and the domain checked&lt;/td&gt;
&lt;td&gt;A pass here with a fail above means the envelope domain is not aligned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one distinction that trips up almost everyone: &lt;code&gt;auth_results&lt;/code&gt; is the raw check, &lt;code&gt;policy_evaluated&lt;/code&gt; is the check after alignment. DMARC only counts SPF or DKIM as passing if the authenticated domain matches your &lt;code&gt;header_from&lt;/code&gt; domain. A marketing platform signing with its own domain will show DKIM &lt;code&gt;pass&lt;/code&gt; in &lt;code&gt;auth_results&lt;/code&gt; and DKIM &lt;code&gt;fail&lt;/code&gt; in &lt;code&gt;policy_evaluated&lt;/code&gt;. That is not a broken signature. It is an unaligned one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: classify every row
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1553877522-43269d4ea984%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1553877522-43269d4ea984%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" alt="Two people reviewing DMARC report data on laptops at a shared desk"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the fields make sense, reading a report is pattern matching. These seven patterns cover nearly every row you will encounter.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;What the row shows&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;SPF aligned pass, DKIM aligned pass, high count&lt;/td&gt;
&lt;td&gt;Your own mail server or a correctly configured service&lt;/td&gt;
&lt;td&gt;Nothing. This is the target state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SPF aligned pass, DKIM fail or missing&lt;/td&gt;
&lt;td&gt;A service you authorised in SPF that is not signing with your domain&lt;/td&gt;
&lt;td&gt;Enable DKIM at that service and publish its CNAME or TXT selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;SPF fail, DKIM aligned pass, &lt;code&gt;envelope_from&lt;/code&gt; is another domain&lt;/td&gt;
&lt;td&gt;Forwarding, a mailing list, or a platform using its own bounce domain&lt;/td&gt;
&lt;td&gt;Normal. DMARC still passes. Do not add the forwarder to SPF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Raw DKIM pass with signing domain not yours, aligned DKIM fail&lt;/td&gt;
&lt;td&gt;Third-party sender signing with its own default key&lt;/td&gt;
&lt;td&gt;Ask the vendor for a custom signing domain (a CNAME under yours)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Raw SPF pass for &lt;code&gt;envelope_from&lt;/code&gt;, aligned SPF fail&lt;/td&gt;
&lt;td&gt;Vendor uses its own return-path domain&lt;/td&gt;
&lt;td&gt;Set a custom return-path or bounce domain at the vendor, or rely on DKIM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Everything fails, &lt;code&gt;source_ip&lt;/code&gt; belongs to a cloud or hosting range you do not recognise, count in the hundreds or thousands&lt;/td&gt;
&lt;td&gt;Spoofing or a phishing campaign using your domain&lt;/td&gt;
&lt;td&gt;Leave it failing. Move toward &lt;code&gt;p=reject&lt;/code&gt; so receivers drop it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Everything fails, low count, &lt;code&gt;reason&lt;/code&gt; says &lt;code&gt;mailing_list&lt;/code&gt; or &lt;code&gt;trusted_forwarder&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Receiver exempted the message from your policy&lt;/td&gt;
&lt;td&gt;Informational. Nothing to fix on your side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a row you cannot place, reverse-lookup the IP with &lt;code&gt;dig -x 192.0.2.123 +short&lt;/code&gt; or &lt;code&gt;host 192.0.2.123&lt;/code&gt;, then check the SPF include for your suspected vendor. A PTR record ending in &lt;code&gt;outbound.protection.outlook.com&lt;/code&gt;, &lt;code&gt;google.com&lt;/code&gt;, &lt;code&gt;amazonses.com&lt;/code&gt;, &lt;code&gt;sendgrid.net&lt;/code&gt; or &lt;code&gt;mailgun.org&lt;/code&gt; usually points straight at the responsible service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading reports at volume
&lt;/h2&gt;

&lt;p&gt;A domain with a few senders produces a handful of files a day, and hand reading works. Above that, use an analyser. A typical analyser ingests the &lt;code&gt;rua=&lt;/code&gt; mailbox, decompresses each file, and rolls the rows up by source, so you see "Microsoft 365: 12,400 messages, 100% aligned" rather than forty XML files. Free options exist, and FortifyNet's own scan will tell you whether a &lt;code&gt;rua=&lt;/code&gt; tag is even present and syntactically correct.&lt;/p&gt;

&lt;p&gt;Two habits matter more than the tool. First, watch &lt;code&gt;policy_published&lt;/code&gt; after every DNS change: receivers cache DMARC records, so a report covering the day you changed &lt;code&gt;p=&lt;/code&gt; may show the old policy, and RFC 9990 section 3.3 explicitly allows a mix. Second, keep reading after you reach &lt;code&gt;p=reject&lt;/code&gt;. New SaaS tools, rotated DKIM keys and edited SPF records all show up as a new pattern 2, 4 or 5 row the next day, long before your customers notice bounces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RFC 9990 changed for readers
&lt;/h2&gt;

&lt;p&gt;The May 2026 standard, which obsoletes the reporting parts of RFC 7489 from 2015, matters if you parse reports yourself or evaluate a vendor.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field or rule&lt;/th&gt;
&lt;th&gt;RFC 7489 (2015)&lt;/th&gt;
&lt;th&gt;RFC 9990 (2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XML namespace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://dmarc.org/dmarc-xml/0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urn:ietf:params:xml:ns:dmarc-2.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM &lt;code&gt;selector&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Required whenever a DKIM result is reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;disposition&lt;/code&gt; values&lt;/td&gt;
&lt;td&gt;none, quarantine, reject&lt;/td&gt;
&lt;td&gt;Adds &lt;code&gt;pass&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Override reasons&lt;/td&gt;
&lt;td&gt;forwarded, sampled_out, trusted_forwarder, mailing_list, local_policy, other&lt;/td&gt;
&lt;td&gt;local_policy, mailing_list, other, policy_test_mode, trusted_forwarder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discovery_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not present&lt;/td&gt;
&lt;td&gt;Optional: &lt;code&gt;psl&lt;/code&gt; or &lt;code&gt;treewalk&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not present&lt;/td&gt;
&lt;td&gt;Optional name and version of the reporting software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;envelope_from&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Optional, may be empty for bounces with a null sender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown elements&lt;/td&gt;
&lt;td&gt;Undefined&lt;/td&gt;
&lt;td&gt;Must be ignored, not treated as errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every major receiver still sends legacy-shaped reports in September 2026, and will for a long time. Read both formats the same way and never discard a report because it has a field you do not recognise.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why do I get DMARC reports for mail I never sent?&lt;/strong&gt;&lt;br&gt;
Because someone else is sending it. Aggregate reports cover every message that claimed your domain in the From header, including spoofed ones. A row with a high count, an unknown IP and failures on both SPF and DKIM is exactly the spoofing you set up DMARC to catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many reports should I expect per day?&lt;/strong&gt;&lt;br&gt;
One per reporting receiver per day per domain, so typically 3 to 15 files for a small business. RFC 9990 sets the period to a single UTC day. Very large receivers may split a day into several files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does disposition none mean when I have p=quarantine?&lt;/strong&gt;&lt;br&gt;
Either the message passed DMARC, so no policy applied, or the receiver overrode your policy and the &lt;code&gt;reason&lt;/code&gt; element says why. Check &lt;code&gt;policy_evaluated&lt;/code&gt;: if both aligned results are &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt; is the correct disposition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do DMARC reports contain email content or personal data?&lt;/strong&gt;&lt;br&gt;
Aggregate reports contain sending IPs, counts and authentication results, nothing from the message body or headers beyond the From domain. Failure reports (RUF, defined in RFC 9991) can contain headers and are rarely sent by major providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I stop receiving reports?&lt;/strong&gt;&lt;br&gt;
Remove the &lt;code&gt;rua=&lt;/code&gt; tag and receivers stop within a day. You lose all visibility into spoofing and misconfiguration when you do, so pointing &lt;code&gt;rua=&lt;/code&gt; at an analyser is the better option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check whether your reports are even arriving
&lt;/h2&gt;

&lt;p&gt;Before you spend an evening with XML, it helps to know whether your DMARC record asks for reports at all, whether the &lt;code&gt;rua=&lt;/code&gt; address is authorised, and whether SPF and DKIM are set up on the senders you already know about. FortifyNet's free scan reads your DMARC, SPF and DKIM records and flags what is missing in about 60 seconds, with no signup. &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;Run a free scan at FortifyNet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related guides:&lt;/strong&gt; &lt;a href="https://fortifynet.com/blog/dmarc-monitoring" rel="noopener noreferrer"&gt;DMARC Monitoring: How to Reach p=reject Safely&lt;/a&gt; · &lt;a href="https://fortifynet.com/blog/how-to-set-up-dmarc" rel="noopener noreferrer"&gt;How to Set Up DMARC, Step by Step&lt;/a&gt; · &lt;a href="https://fortifynet.com/blog/what-is-dkim" rel="noopener noreferrer"&gt;What Is DKIM?&lt;/a&gt; · &lt;a href="https://fortifynet.com/blog/spf-record-checker" rel="noopener noreferrer"&gt;SPF Record Checker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/how-to-read-dmarc-reports" rel="noopener noreferrer"&gt;fortifynet.com/blog/how-to-read-dmarc-reports&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dmarc</category>
      <category>dmarcreports</category>
      <category>emailauthentication</category>
      <category>rua</category>
    </item>
    <item>
      <title>How to Renew an SSL Certificate (2026 Guide) | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Sat, 05 Sep 2026 04:46:45 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/how-to-renew-an-ssl-certificate-2026-guide-fortifynet-3027</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/how-to-renew-an-ssl-certificate-2026-guide-fortifynet-3027</guid>
      <description>&lt;h1&gt;
  
  
  How to Renew an SSL Certificate: Step-by-Step Guide for 2026
&lt;/h1&gt;

&lt;p&gt;To renew an SSL certificate, you generate a fresh certificate for the same domain before the old one expires, install it on your server or platform, and confirm the new expiry date. If you use Let's Encrypt, one command (&lt;code&gt;certbot renew&lt;/code&gt;) does all of it and a timer runs it for you twice a day. If you bought a certificate from a commercial CA, you create a new CSR, complete domain validation again, download the new files and swap them in. This guide covers both paths, plus hosting panels and CDNs, and shows how to verify the renewal worked.&lt;/p&gt;

&lt;p&gt;The stakes are rising. Since 15 March 2026 no public CA may issue a certificate valid for more than 200 days, and the cap drops to 47 days in 2029 (&lt;a href="https://groups.google.com/a/groups.cabforum.org/g/servercert-wg/c/bvWh5RN6tYI" rel="noopener noreferrer"&gt;CA/Browser Forum ballot SC-081v3&lt;/a&gt;, approved April 2025). Renewal is no longer a once-a-year chore. It is becoming a continuous process, and the only realistic way to keep up is automation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1506784365847-bbad939e9335%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1506784365847-bbad939e9335%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="Wall calendar with a marker pen, symbolising the recurring SSL certificate renewal schedule" width="1600" height="1070"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SSL certificate renewal matters more in 2026
&lt;/h2&gt;

&lt;p&gt;An expired certificate does not degrade gracefully. Browsers block the page with a full-screen warning (&lt;code&gt;NET::ERR_CERT_DATE_INVALID&lt;/code&gt; in Chrome), API clients fail their TLS handshake, and email and monitoring integrations start throwing errors. A single forgotten renewal took down Microsoft Teams for roughly three hours on 3 February 2020 (&lt;a href="https://techcrunch.com/2020/02/03/microsoft-teams-goes-down-after-microsoft-forgot-to-renew-a-certificate/" rel="noopener noreferrer"&gt;TechCrunch&lt;/a&gt;), and an expired certificate in Ericsson equipment knocked out mobile data for millions of O2 and SoftBank customers on 6 December 2018 (&lt;a href="https://www.theregister.com/2018/12/06/ericsson_o2_outage/" rel="noopener noreferrer"&gt;The Register&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Shorter lifetimes multiply the number of renewals you have to get right. The table below shows the industry schedule that every public CA must follow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effective date&lt;/th&gt;
&lt;th&gt;Maximum certificate validity&lt;/th&gt;
&lt;th&gt;Maximum domain-validation reuse&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before 15 March 2026&lt;/td&gt;
&lt;td&gt;398 days&lt;/td&gt;
&lt;td&gt;398 days&lt;/td&gt;
&lt;td&gt;CA/B Forum Baseline Requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 March 2026 (in force now)&lt;/td&gt;
&lt;td&gt;200 days&lt;/td&gt;
&lt;td&gt;200 days&lt;/td&gt;
&lt;td&gt;SC-081v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 March 2027&lt;/td&gt;
&lt;td&gt;100 days&lt;/td&gt;
&lt;td&gt;100 days&lt;/td&gt;
&lt;td&gt;SC-081v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 March 2029&lt;/td&gt;
&lt;td&gt;47 days&lt;/td&gt;
&lt;td&gt;10 days&lt;/td&gt;
&lt;td&gt;SC-081v3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's Encrypt, which reported serving more than 700 million websites and issuing over ten million certificates on a single day in late 2025 (&lt;a href="https://letsencrypt.org/2025/12/09/10-years" rel="noopener noreferrer"&gt;Let's Encrypt, 9 December 2025&lt;/a&gt;), is moving faster than the mandate: its default profile switches from 90-day to 64-day certificates on 10 February 2027 and to 45-day certificates on 16 February 2028 (&lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45" rel="noopener noreferrer"&gt;Let's Encrypt, 2 December 2025&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D450%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522398%2520days%2520%2528pre-Mar%25202026%2529%2522%252C%2522200%2520days%2520%2528Mar%25202026%2529%2522%252C%2522100%2520days%2520%2528Mar%25202027%2529%2522%252C%252290%2520days%2520%2528Let%2527s%2520Encrypt%2520today%2529%2522%252C%252247%2520days%2520%2528Mar%25202029%2529%2522%252C%252245%2520days%2520%2528Let%2527s%2520Encrypt%25202028%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Renewals%2520per%2520year%2522%252C%2522data%2522%253A%255B0.9%252C1.8%252C3.7%252C4.1%252C7.8%252C8.1%255D%252C%2522backgroundColor%2522%253A%255B%2522%252394a3b8%2522%252C%2522%252360a5fa%2522%252C%2522%25233b82f6%2522%252C%2522%25232563eb%2522%252C%2522%25231d4ed8%2522%252C%2522%25231e3a8a%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Renewals%2520per%2520year%2520per%2520certificate%252C%2520by%2520maximum%2520lifetime%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522Renewals%2520per%2520year%2522%257D%257D%255D%252C%2522xAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522fontSize%2522%253A10%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fw%3D800%26h%3D450%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522398%2520days%2520%2528pre-Mar%25202026%2529%2522%252C%2522200%2520days%2520%2528Mar%25202026%2529%2522%252C%2522100%2520days%2520%2528Mar%25202027%2529%2522%252C%252290%2520days%2520%2528Let%2527s%2520Encrypt%2520today%2529%2522%252C%252247%2520days%2520%2528Mar%25202029%2529%2522%252C%252245%2520days%2520%2528Let%2527s%2520Encrypt%25202028%2529%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522Renewals%2520per%2520year%2522%252C%2522data%2522%253A%255B0.9%252C1.8%252C3.7%252C4.1%252C7.8%252C8.1%255D%252C%2522backgroundColor%2522%253A%255B%2522%252394a3b8%2522%252C%2522%252360a5fa%2522%252C%2522%25233b82f6%2522%252C%2522%25232563eb%2522%252C%2522%25231d4ed8%2522%252C%2522%25231e3a8a%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Renewals%2520per%2520year%2520per%2520certificate%252C%2520by%2520maximum%2520lifetime%2522%252C%2522fontSize%2522%253A16%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522scales%2522%253A%257B%2522yAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522beginAtZero%2522%253Atrue%257D%252C%2522scaleLabel%2522%253A%257B%2522display%2522%253Atrue%252C%2522labelString%2522%253A%2522Renewals%2520per%2520year%2522%257D%257D%255D%252C%2522xAxes%2522%253A%255B%257B%2522ticks%2522%253A%257B%2522fontSize%2522%253A10%257D%257D%255D%257D%252C%2522plugins%2522%253A%257B%2522datalabels%2522%253A%257B%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522top%2522%257D%257D%257D%257D" alt="Bar chart showing how many renewals per year each certificate needs at 398, 200, 100, 90, 47 and 45 day lifetimes, rising from under one to more than eight" width="1600" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Renewals per year for one certificate at each maximum lifetime (365 divided by the lifetime in days). Sources: CA/Browser Forum SC-081v3; Let's Encrypt, 2 December 2025. A site with ten certificates on 47-day lifetimes needs about 78 successful renewals a year.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find out how your certificate was issued
&lt;/h2&gt;

&lt;p&gt;The renewal method depends entirely on where the certificate came from. Run this on the server to see the issuer and expiry date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl s_client -connect example.com:443 -servername example.com &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 -noout -issuer -dates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;issuer&lt;/code&gt; line tells you which path to follow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issuer shown&lt;/th&gt;
&lt;th&gt;Typical setup&lt;/th&gt;
&lt;th&gt;How you renew&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Let's Encrypt (R10, R11, E5, E6 etc.)&lt;/td&gt;
&lt;td&gt;Certbot, acme.sh, Caddy, Traefik&lt;/td&gt;
&lt;td&gt;Automated via ACME; check the timer, never renew by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigiCert, Sectigo, GlobalSign, GoDaddy&lt;/td&gt;
&lt;td&gt;Purchased DV/OV/EV certificate&lt;/td&gt;
&lt;td&gt;New CSR, re-validate domain, install new files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cPanel, Inc. or Sectigo via AutoSSL&lt;/td&gt;
&lt;td&gt;Shared hosting with cPanel&lt;/td&gt;
&lt;td&gt;Automatic; check the SSL/TLS Status page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare, Google Trust Services, Amazon&lt;/td&gt;
&lt;td&gt;CDN or cloud edge certificate&lt;/td&gt;
&lt;td&gt;Managed by the platform; confirm DNS or CNAME validation still resolves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own company name&lt;/td&gt;
&lt;td&gt;Internal CA&lt;/td&gt;
&lt;td&gt;Reissue from your PKI; not covered here&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 2a: Renew a Let's Encrypt certificate with Certbot
&lt;/h2&gt;

&lt;p&gt;Let's Encrypt certificates are valid for 90 days today and Certbot's packaged timer tries to renew them twice a day, renewing only when fewer than 30 days remain (&lt;a href="https://eff-certbot.readthedocs.io/en/stable/using.html#renewing-certificates" rel="noopener noreferrer"&gt;Certbot documentation&lt;/a&gt;). If a site is showing an expired Let's Encrypt certificate, the automation has been failing for at least a month.&lt;/p&gt;

&lt;p&gt;First list what Certbot knows about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo certbot certificates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test a renewal without touching anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo certbot renew --dry-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the dry run passes, run the real renewal and reload your web server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo certbot renew
sudo systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot will say "Cert not yet due for renewal" if more than 30 days remain; that is normal. To renew early regardless (for example after changing the key size), add &lt;code&gt;--force-renewal&lt;/code&gt; to a single certificate with &lt;code&gt;certbot renew --cert-name example.com --force-renewal&lt;/code&gt;, but never leave it in a cron job, because Let's Encrypt rate-limits duplicate certificates to 5 per week.&lt;/p&gt;

&lt;p&gt;Check that the timer is active so this keeps happening without you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl list-timers | grep certbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two changes since 2025 matter here. Let's Encrypt stopped sending expiry-warning emails on 4 June 2025 and deleted the addresses it held (&lt;a href="https://letsencrypt.org/2025/06/26/expiration-notification-service-has-ended" rel="noopener noreferrer"&gt;Let's Encrypt, 26 June 2025&lt;/a&gt;), so nobody will nudge you. And modern ACME clients can ask the CA for a suggested renewal window through ACME Renewal Information (ARI, RFC 9773), which Certbot uses from version 2.9 onward. Keep the client updated so that it follows ARI rather than a hard-coded interval; Let's Encrypt explicitly warns that renewing at a fixed 60 days will not survive the move to 45-day certificates.&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%2Fimages.unsplash.com%2Fphoto-1484417894907-623942c8ee29%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1484417894907-623942c8ee29%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" alt="Laptop on a desk with a terminal window open, representing server-side certificate renewal work" width="1200" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2b: Renew a purchased certificate (DigiCert, Sectigo, GoDaddy and others)
&lt;/h2&gt;

&lt;p&gt;Commercial certificates cannot be "extended". Renewal means the CA issues a brand-new certificate, so the process mirrors the original order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generate a new private key and CSR on the server.&lt;/strong&gt; Reusing the old key is allowed but weakens the security benefit of renewing.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl req -new -newkey rsa:2048 -nodes -keyout example.com.key -out example.com.csr -subj "/CN=example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Submit the CSR in the CA portal&lt;/strong&gt; under Renew or Reissue. Most CAs let you start 30 to 90 days early and carry the remaining days over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete domain validation again.&lt;/strong&gt; Since 15 March 2026 a validation can only be reused for 200 days, so expect an email, HTTP file or DNS TXT challenge on almost every renewal. OV and EV certificates also need organisation checks; start those two weeks ahead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download the certificate and the intermediate chain.&lt;/strong&gt; Missing intermediates cause &lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;/code&gt; on some devices even when the leaf certificate is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install and reload.&lt;/strong&gt; Replace the certificate and chain files referenced in your server config, keep the new key alongside, and reload the service. On nginx that is &lt;code&gt;ssl_certificate&lt;/code&gt; and &lt;code&gt;ssl_certificate_key&lt;/code&gt;; on Apache, &lt;code&gt;SSLCertificateFile&lt;/code&gt; and &lt;code&gt;SSLCertificateKeyFile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update every copy.&lt;/strong&gt; Load balancers, mail servers, CDN origins and monitoring probes often hold their own copy of the same certificate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2c: Renew on hosting panels, CDNs and cloud platforms
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;What renews it&lt;/th&gt;
&lt;th&gt;What can break renewal&lt;/th&gt;
&lt;th&gt;Where to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cPanel AutoSSL&lt;/td&gt;
&lt;td&gt;Daily cron on the host&lt;/td&gt;
&lt;td&gt;Domain not pointing at the server, .htaccess blocking /.well-known/&lt;/td&gt;
&lt;td&gt;SSL/TLS Status in cPanel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plesk (Let's Encrypt extension)&lt;/td&gt;
&lt;td&gt;Built-in scheduled task&lt;/td&gt;
&lt;td&gt;Same as above; expired domain&lt;/td&gt;
&lt;td&gt;Websites and Domains, SSL/TLS Certificates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Universal SSL&lt;/td&gt;
&lt;td&gt;Cloudflare, automatically&lt;/td&gt;
&lt;td&gt;Proxy turned off (grey cloud); CAA record excluding Cloudflare's CAs&lt;/td&gt;
&lt;td&gt;SSL/TLS, Edge Certificates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Certificate Manager&lt;/td&gt;
&lt;td&gt;ACM, automatically for DNS-validated certs&lt;/td&gt;
&lt;td&gt;Validation CNAME removed from Route 53&lt;/td&gt;
&lt;td&gt;ACM console, certificate status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes cert-manager&lt;/td&gt;
&lt;td&gt;Controller, renews at two thirds of lifetime by default&lt;/td&gt;
&lt;td&gt;Ingress class mismatch; solver pod cannot be reached&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl describe certificate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caddy / Traefik&lt;/td&gt;
&lt;td&gt;Built-in ACME client&lt;/td&gt;
&lt;td&gt;Port 80 or 443 unreachable from the internet&lt;/td&gt;
&lt;td&gt;Server logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 3: Verify the renewal actually worked
&lt;/h2&gt;

&lt;p&gt;Renewing and deploying are two different things. A renewed file sitting on disk does nothing until the server process has loaded it. Confirm from the outside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo | openssl s_client -connect example.com:443 -servername example.com 2&amp;gt;/dev/null | openssl x509 -noout -enddate -serial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;notAfter&lt;/code&gt; date must be the new one and the serial number must differ from the old certificate. Then check the full chain and every host name (www and bare domain, API subdomains, mail hosts). A free FortifyNet scan checks the chain, expiry date, protocol versions and HSTS in one pass, which is faster than testing each host by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common SSL renewal errors and fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error or symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Certbot: "Some challenges have failed" (HTTP-01)&lt;/td&gt;
&lt;td&gt;Port 80 blocked, redirect loop, or DNS pointing elsewhere&lt;/td&gt;
&lt;td&gt;Allow port 80 inbound, keep /.well-known/acme-challenge/ reachable over plain HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certbot: "too many certificates already issued"&lt;/td&gt;
&lt;td&gt;Forced renewals in a loop&lt;/td&gt;
&lt;td&gt;Remove &lt;code&gt;--force-renewal&lt;/code&gt; from scheduled jobs, wait for the weekly window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS-01 "no TXT record found"&lt;/td&gt;
&lt;td&gt;Propagation delay or wrong zone&lt;/td&gt;
&lt;td&gt;Add a propagation wait, confirm the API token has zone edit rights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser still shows the old expiry after renewal&lt;/td&gt;
&lt;td&gt;Service not reloaded, or CDN caching the old cert&lt;/td&gt;
&lt;td&gt;Reload nginx/Apache; purge or re-upload at the CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;/code&gt; after install&lt;/td&gt;
&lt;td&gt;Intermediate chain missing&lt;/td&gt;
&lt;td&gt;Install the full chain file the CA provides, not only the leaf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate renewed but mail clients complain&lt;/td&gt;
&lt;td&gt;Mail server uses its own copy&lt;/td&gt;
&lt;td&gt;Deploy the same files to Postfix/Dovecot/Exchange and restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAA lookup failure&lt;/td&gt;
&lt;td&gt;CAA record lists other CAs only&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;0 issue "letsencrypt.org"&lt;/code&gt; (or your CA) to the CAA record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Set a renewal policy you can actually keep
&lt;/h2&gt;

&lt;p&gt;With 200-day certificates now and 47-day certificates by 2029, a renewal policy should be boring and automatic. Renew at roughly two thirds of the certificate's lifetime, exactly the behaviour Let's Encrypt recommends for clients that lack ARI. Run a monitoring check that alerts at 14 and 7 days before expiry, independent of the renewal tool, because the failure you need to catch is the automation silently stopping. Keep an inventory of every host name that terminates TLS, including internal ones. And test the dry run after every server migration, PHP upgrade or firewall change, since those are the moments the ACME challenge stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/ssl-certificate-expired" rel="noopener noreferrer"&gt;What happens when an SSL certificate expires&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/ssl-tls-certificate-security" rel="noopener noreferrer"&gt;SSL/TLS certificate security: the complete guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/your-connection-is-not-private" rel="noopener noreferrer"&gt;Fixing "Your connection is not private"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/what-is-hsts" rel="noopener noreferrer"&gt;What is HSTS and why it matters after a renewal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How early can I renew an SSL certificate?&lt;/strong&gt;&lt;br&gt;
Let's Encrypt allows renewal at any time, and Certbot does it automatically 30 days before expiry. Commercial CAs typically open renewal 30 to 90 days early and add the unused days to the new certificate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does renewing an SSL certificate cause downtime?&lt;/strong&gt;&lt;br&gt;
No, if you reload rather than restart the web server. nginx and Apache both load the new certificate on a reload without dropping existing connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a new private key to renew?&lt;/strong&gt;&lt;br&gt;
Not strictly, but generating a new key is best practice. Certbot creates a new key on every renewal by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I renew a certificate after it has expired?&lt;/strong&gt;&lt;br&gt;
Yes. Renewal after expiry works the same way; the only difference is that visitors see warnings until the new certificate is installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many times a year will I have to renew from 2029?&lt;/strong&gt;&lt;br&gt;
With the 47-day maximum, about eight times per certificate per year, which is why manual renewal is being phased out in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your certificate before it becomes an incident
&lt;/h2&gt;

&lt;p&gt;Run the &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;free FortifyNet scan&lt;/a&gt; on your domain. In about 60 seconds it reports the certificate expiry date, chain validity, protocol versions, security headers, DNS and email authentication, so a failed renewal shows up before your visitors find it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/how-to-renew-ssl-certificate" rel="noopener noreferrer"&gt;fortifynet.com/blog/how-to-renew-ssl-certificate&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssl</category>
      <category>tls</category>
      <category>certificates</category>
      <category>certbot</category>
    </item>
    <item>
      <title>Website Security Audit: Complete 2026 Guide | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Wed, 02 Sep 2026 04:47:19 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/website-security-audit-complete-2026-guide-fortifynet-43fe</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/website-security-audit-complete-2026-guide-fortifynet-43fe</guid>
      <description>&lt;p&gt;A website security audit is a structured review of everything an attacker can see and reach from outside your organisation: your TLS certificate and cipher configuration, your DNS records, your email authentication (SPF, DKIM and DMARC), your HTTP security headers, the software versions you leak in responses, and any company credentials already circulating on the dark web. A good audit ends with a ranked list of findings, each with an owner and a concrete fix. The externally visible layer takes about sixty seconds to scan automatically. The full exercise, done properly, takes an afternoon.&lt;/p&gt;

&lt;p&gt;This guide covers what an audit actually tests, the four levels of depth available to you, an eight step process you can run this week, and how often to repeat 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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1400%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1400%26q%3D80" alt="A team reviewing security findings and metrics printed on paper around a table" width="1400" height="934"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the 2026 threat picture changed the maths
&lt;/h2&gt;

&lt;p&gt;Verizon's 2026 Data Breach Investigations Report, published on 19 May 2026 and built on more than 31,000 security incidents and over 22,000 confirmed breaches across 145 countries, found that exploitation of vulnerabilities was the initial access vector in 31% of breaches. That is the first time in the report's nineteen editions that it has overtaken stolen credentials, which fell to 13%.&lt;/p&gt;

&lt;p&gt;Two supporting figures from the same report explain why infrequent auditing is now expensive. Only 26% of the critical vulnerabilities listed in CISA's Known Exploited Vulnerabilities catalogue were fully remediated during 2025, down from 38% the year before. And the median time to fully remediate a vulnerability rose to 43 days. Attackers are moving faster while defenders are moving slower, so the gap you leave between audits is the gap they work in.&lt;/p&gt;

&lt;p&gt;The certificate side changed too. Under CA/Browser Forum ballot SC-081v3, approved in April 2025 by a vote of 29 to 0, the maximum lifetime of a publicly trusted TLS certificate dropped from 398 days to 200 days on 15 March 2026. It falls to 100 days on 15 March 2027 and to 47 days on 15 March 2029. Domain validation reuse periods shrink on the same dates. Any renewal process that still relies on a person remembering a calendar entry will fail within this cycle, which makes certificate expiry a permanent audit item rather than an annual one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a website security audit actually checks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What the audit looks for&lt;/th&gt;
&lt;th&gt;Usual owner&lt;/th&gt;
&lt;th&gt;Sensible cadence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TLS and certificates&lt;/td&gt;
&lt;td&gt;Expiry date, chain completeness, hostname match, protocol versions, weak ciphers&lt;/td&gt;
&lt;td&gt;Ops or hosting provider&lt;/td&gt;
&lt;td&gt;Continuous monitoring, review monthly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;Dangling CNAME records, missing DNSSEC, open zone transfers, stale subdomains&lt;/td&gt;
&lt;td&gt;DNS or platform team&lt;/td&gt;
&lt;td&gt;Quarterly, plus after every migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email authentication&lt;/td&gt;
&lt;td&gt;SPF record validity and lookup budget, DKIM selectors, DMARC policy and reporting&lt;/td&gt;
&lt;td&gt;IT or email admin&lt;/td&gt;
&lt;td&gt;Quarterly, plus after any provider change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP security headers&lt;/td&gt;
&lt;td&gt;HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy&lt;/td&gt;
&lt;td&gt;Web or platform team&lt;/td&gt;
&lt;td&gt;After every deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application surface&lt;/td&gt;
&lt;td&gt;Outdated CMS, plugins and libraries, exposed admin paths, verbose error pages&lt;/td&gt;
&lt;td&gt;Development team&lt;/td&gt;
&lt;td&gt;Monthly, automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data exposure&lt;/td&gt;
&lt;td&gt;Company email addresses and credentials in known breach corpora&lt;/td&gt;
&lt;td&gt;Security or IT lead&lt;/td&gt;
&lt;td&gt;Continuous monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance and availability&lt;/td&gt;
&lt;td&gt;Core Web Vitals, uptime, response times under load&lt;/td&gt;
&lt;td&gt;Web team&lt;/td&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first four rows are the ones an external scanner can assess without any access to your systems, which is why they are the fastest place to start and usually where the first real findings appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What audits find missing most often
&lt;/h2&gt;

&lt;p&gt;The HTTP Archive's Web Almanac 2025, which analyses millions of real websites, measured how many sites actually set each protective HTTP header. The answer is sobering: the single most widely adopted security header is present on roughly half of sites, and Content Security Policy on barely a fifth.&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%2Fquickchart.io%2Fchart%3Fv%3D4%26w%3D760%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522X-Content-Type-Options%2522%252C%2522Strict-Transport-Security%2522%252C%2522X-Frame-Options%2522%252C%2522Content-Security-Policy%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522%2525%2520of%2520mobile%2520sites%2522%252C%2522data%2522%253A%255B50%252C36%252C35%252C21.9%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Security%2520controls%2520found%2520on%2520real%2520websites%2520%25282025%2529%2522%252C%2522font%2522%253A%257B%2522size%2522%253A16%257D%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522color%2522%253A%2522%2523111827%2522%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A60%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522%2525%2520of%2520sites%2522%257D%257D%252C%2522x%2522%253A%257B%2522ticks%2522%253A%257B%2522font%2522%253A%257B%2522size%2522%253A10%257D%257D%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fv%3D4%26w%3D760%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522X-Content-Type-Options%2522%252C%2522Strict-Transport-Security%2522%252C%2522X-Frame-Options%2522%252C%2522Content-Security-Policy%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522%2525%2520of%2520mobile%2520sites%2522%252C%2522data%2522%253A%255B50%252C36%252C35%252C21.9%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522Security%2520controls%2520found%2520on%2520real%2520websites%2520%25282025%2529%2522%252C%2522font%2522%253A%257B%2522size%2522%253A16%257D%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522datalabels%2522%253A%257B%2522display%2522%253Atrue%252C%2522anchor%2522%253A%2522end%2522%252C%2522align%2522%253A%2522end%2522%252C%2522color%2522%253A%2522%2523111827%2522%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A60%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522%2525%2520of%2520sites%2522%257D%257D%252C%2522x%2522%253A%257B%2522ticks%2522%253A%257B%2522font%2522%253A%257B%2522size%2522%253A10%257D%257D%257D%257D%257D%257D" alt="Bar chart of security header adoption across real websites in 2025, showing X-Content-Type-Options near 50 percent, Strict-Transport-Security at 36 percent, X-Frame-Options near 35 percent and Content-Security-Policy at 21.9 percent" width="1520" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Adoption of key HTTP security headers on mobile pages. Source: HTTP Archive Web Almanac 2025, Security chapter. Figures for X-Content-Type-Options and X-Frame-Options are approximate as published.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Strict-Transport-Security reached 36% of pages in 2025, up six percentage points on 2024 and the strongest riser of the year. Content Security Policy grew from 18.5% to 21.9%. Newer controls such as Permissions-Policy and Cross-Origin-Opener-Policy remain below 10%. Even among sites that do deploy CSP, 92% still allow &lt;code&gt;unsafe-inline&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt; and 77% allow &lt;code&gt;unsafe-eval&lt;/code&gt;, which removes most of the protection the header is meant to provide. Adopting a header is not the same as configuring it well, and a real audit checks the value, not just the presence.&lt;/p&gt;

&lt;p&gt;Transport encryption is in far better shape. TLS 1.3 now carries roughly 76% of page loads, TLS 1.2 is down to 13 to 15%, and HTTPS adoption exceeds 95% in every country the Almanac measures except Japan. If your audit still finds TLS 1.0 or 1.1 enabled, you are in a small and conspicuous minority.&lt;/p&gt;

&lt;p&gt;Email authentication remains the widest gap of all. DMARCguard's State of Email Authentication 2026, a scan of 5,499,028 Tranco-ranked domains completed on 27 February 2026, found SPF on 56.0% of domains, DMARC on 30.4% and DKIM on just 22.7%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four levels of audit, and when each is worth it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Audit type&lt;/th&gt;
&lt;th&gt;Typical duration&lt;/th&gt;
&lt;th&gt;What it finds&lt;/th&gt;
&lt;th&gt;What it misses&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automated external scan&lt;/td&gt;
&lt;td&gt;Under 2 minutes&lt;/td&gt;
&lt;td&gt;Certificate, DNS, email auth, header and exposure issues visible from outside&lt;/td&gt;
&lt;td&gt;Business logic flaws, authenticated areas&lt;/td&gt;
&lt;td&gt;Continuous baseline, every site, every month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authenticated vulnerability assessment&lt;/td&gt;
&lt;td&gt;1 to 5 days&lt;/td&gt;
&lt;td&gt;Known CVEs in your stack, misconfigured services, weak access controls&lt;/td&gt;
&lt;td&gt;Novel logic abuse, chained exploits&lt;/td&gt;
&lt;td&gt;Regulated or transactional sites, annually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Penetration test&lt;/td&gt;
&lt;td&gt;1 to 3 weeks&lt;/td&gt;
&lt;td&gt;Chained and creative attacks, privilege escalation, real exploitability&lt;/td&gt;
&lt;td&gt;Anything outside the agreed scope or time box&lt;/td&gt;
&lt;td&gt;Before a major launch, or annually for high-risk systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual code and architecture review&lt;/td&gt;
&lt;td&gt;1 to 4 weeks&lt;/td&gt;
&lt;td&gt;Design flaws, insecure defaults, supply chain risk in dependencies&lt;/td&gt;
&lt;td&gt;Runtime and infrastructure issues&lt;/td&gt;
&lt;td&gt;New products, or after an incident&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most organisations should treat the first row as always on and choose one of the other three per year based on risk. Running a penetration test on a site that still fails an automated scan wastes the test, because the tester spends the engagement reporting findings a free tool would have caught in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run a website security audit in eight steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory what you own.&lt;/strong&gt; List every domain, subdomain and third-party service that renders under your brand. Audits fail most often because the vulnerable host was one nobody remembered owning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run an external scan first.&lt;/strong&gt; Get the outside-in picture before you spend anyone's time. This tells you which of the seven layers above are already failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify certificates and renewal automation.&lt;/strong&gt; Check expiry, chain and hostname coverage, then check that renewal is automated. Under the 200 day limit now in force, manual renewal is a scheduled outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check DNS for orphans.&lt;/strong&gt; Every CNAME pointing at a service you no longer use is a subdomain takeover waiting to happen. Remove stale records rather than leaving them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate email authentication end to end.&lt;/strong&gt; Confirm your SPF record stays within its ten DNS lookup budget, that DKIM signs all sending sources, and that DMARC has a policy stronger than &lt;code&gt;p=none&lt;/code&gt; with a working reporting address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review header values, not just header presence.&lt;/strong&gt; A CSP full of &lt;code&gt;unsafe-inline&lt;/code&gt; and an HSTS &lt;code&gt;max-age&lt;/code&gt; of 300 seconds both pass a naive checklist and protect nobody.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch by exploitability, not by CVSS score alone.&lt;/strong&gt; Anything on the CISA Known Exploited Vulnerabilities catalogue that touches your stack goes first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the findings down with an owner and a date.&lt;/strong&gt; An audit that ends in a conversation instead of a ticket has not happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Mapping your findings to OWASP Top 10:2025
&lt;/h2&gt;

&lt;p&gt;OWASP published the 2025 edition of its Top 10 in January 2026, after announcing it at Global AppSec in Washington DC in November 2025. Two categories are new, and both are relevant to what an audit should now cover.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Change from 2021&lt;/th&gt;
&lt;th&gt;Visible in an external audit?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A01&lt;/td&gt;
&lt;td&gt;Broken Access Control&lt;/td&gt;
&lt;td&gt;Stable at the top for four editions, now absorbs SSRF&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A02&lt;/td&gt;
&lt;td&gt;Security Misconfiguration&lt;/td&gt;
&lt;td&gt;Up from A05&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A03&lt;/td&gt;
&lt;td&gt;Software Supply Chain Failures&lt;/td&gt;
&lt;td&gt;New, expands the old A06&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A04&lt;/td&gt;
&lt;td&gt;Cryptographic Failures&lt;/td&gt;
&lt;td&gt;Down from A02&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A05&lt;/td&gt;
&lt;td&gt;Injection&lt;/td&gt;
&lt;td&gt;Down from A03&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A06&lt;/td&gt;
&lt;td&gt;Insecure Design&lt;/td&gt;
&lt;td&gt;Down from A04&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A07&lt;/td&gt;
&lt;td&gt;Authentication Failures&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A08&lt;/td&gt;
&lt;td&gt;Software and Data Integrity Failures&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A09&lt;/td&gt;
&lt;td&gt;Security Logging and Alerting Failures&lt;/td&gt;
&lt;td&gt;Renamed from Monitoring&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A10&lt;/td&gt;
&lt;td&gt;Mishandling of Exceptional Conditions&lt;/td&gt;
&lt;td&gt;New, replaces SSRF as a standalone entry&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The promotion of Security Misconfiguration to second place is the single most useful signal here for anyone planning an audit. It is the category most fully addressed by external scanning, and the category most often left to whoever set the server up years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often should you audit?
&lt;/h2&gt;

&lt;p&gt;Continuously for anything automatable, quarterly for anything requiring judgement, and immediately after any of these events: a platform or hosting migration, a change of email provider, a new subdomain going live, a dependency major version bump, or a staff departure with administrative access. The Verizon figure that third parties now feature in 48% of breaches, a 60% increase year on year, means a supplier change deserves the same treatment as a change of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long does a website security audit take?&lt;/strong&gt;&lt;br&gt;
An automated external scan of certificates, DNS, email authentication, headers and exposure completes in about a minute. A thorough internal review of the findings, with fixes assigned, typically takes half a day for a small site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a penetration test, or is a scan enough?&lt;/strong&gt;&lt;br&gt;
A scan is the right and sufficient starting point for every site, and the only sensible thing to run continuously. A penetration test adds value once your automated findings are clean and you have authenticated functionality, payments or personal data worth attacking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an audit be done without access to my server?&lt;/strong&gt;&lt;br&gt;
Most of the highest-value checks can. TLS configuration, certificate validity, DNS hygiene, SPF, DKIM, DMARC, security headers and dark web exposure are all assessable from the public internet. Code review and authenticated testing are the parts that need access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the first thing to fix after an audit?&lt;/strong&gt;&lt;br&gt;
Anything on the CISA Known Exploited Vulnerabilities catalogue, then an expiring or misconfigured certificate, then a DMARC policy of &lt;code&gt;p=none&lt;/code&gt;. These three account for a disproportionate share of real-world compromise relative to the effort of fixing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the sixty second version
&lt;/h2&gt;

&lt;p&gt;You cannot fix what you have not measured, and the outside-in layer of a website security audit is free to measure. FortifyNet's scanner checks your SSL and TLS configuration, DNS records, SPF, DKIM and DMARC, HTTP security headers, dark web exposure and performance in about sixty seconds, and returns a prioritised list rather than a wall of raw output. Run it on your domain, fix what it finds, then decide whether you need the deeper levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related guides
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/website-security-checklist" rel="noopener noreferrer"&gt;Website security checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/website-vulnerability-scanner" rel="noopener noreferrer"&gt;Website vulnerability scanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/http-security-headers-guide" rel="noopener noreferrer"&gt;HTTP security headers guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/ssl-tls-certificate-security" rel="noopener noreferrer"&gt;SSL and TLS certificate security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/website-security-audit" rel="noopener noreferrer"&gt;fortifynet.com/blog/website-security-audit&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>websitesecurityaudit</category>
      <category>securityaudit</category>
      <category>vulnerabilitymanagement</category>
      <category>owasp</category>
    </item>
    <item>
      <title>SSL Certificate Expired: What Happens and How to Fix It</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:46:08 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/ssl-certificate-expired-what-happens-and-how-to-fix-it-20h0</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/ssl-certificate-expired-what-happens-and-how-to-fix-it-20h0</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1501139083538-0139583c060f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1501139083538-0139583c060f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="An hourglass with sand running out, symbolizing an SSL certificate reaching its expiry date" width="1600" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An expired SSL certificate means the certificate that proves your website's identity has passed its expiry date, so browsers stop trusting it on the spot. Visitors see a full-screen warning such as NET::ERR_CERT_DATE_INVALID instead of your content, API clients and mobile apps fail outright, and traffic collapses until the certificate is replaced. The fix is to renew or reissue the certificate, install the new full chain on every server that terminates TLS, and reload those services; with an automated ACME setup that takes minutes. This guide shows you how to check your expiry date, fix an expired certificate step by step, and make sure it never happens again.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What visitors see when your SSL certificate expires
&lt;/h2&gt;

&lt;p&gt;Browsers do not politely mention the problem in a corner. They replace your entire page with a warning screen, and the visitor has to dig through an "Advanced" menu to proceed at all. The wording differs by browser:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Warning screen&lt;/th&gt;
&lt;th&gt;Error code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome&lt;/td&gt;
&lt;td&gt;"Your connection is not private"&lt;/td&gt;
&lt;td&gt;NET::ERR_CERT_DATE_INVALID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge&lt;/td&gt;
&lt;td&gt;"Your connection isn't private"&lt;/td&gt;
&lt;td&gt;NET::ERR_CERT_DATE_INVALID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;"Warning: Potential Security Risk Ahead"&lt;/td&gt;
&lt;td&gt;SEC_ERROR_EXPIRED_CERTIFICATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari&lt;/td&gt;
&lt;td&gt;"This Connection Is Not Private"&lt;/td&gt;
&lt;td&gt;Shown without a code on the first screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These warnings are common enough to be measured at scale. In the largest published study of browser certificate warnings, Google and Carnegie Mellon researchers analysed more than 300 million Chrome warning reports and attributed 4.23% of warnings on Windows to certificate date errors (Acer et al., ACM CCS 2017). Expired certificates are the routine failure mode of HTTPS, and &lt;a href="https://fortifynet.com/blog/your-connection-is-not-private" rel="noopener noreferrer"&gt;the warning screen itself&lt;/a&gt; is often the first sign an owner gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than your homepage breaks
&lt;/h2&gt;

&lt;p&gt;The browser warning is only the visible part. The quiet failures usually cost more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;APIs and webhooks fail hard.&lt;/strong&gt; Machine clients do not have an "Advanced" button. Payment callbacks, mobile apps, partner integrations and monitoring agents simply refuse the connection the second the certificate expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS removes the escape hatch.&lt;/strong&gt; If your site sends the &lt;a href="https://fortifynet.com/blog/what-is-hsts" rel="noopener noreferrer"&gt;HSTS header&lt;/a&gt;, browsers hide the "proceed anyway" link entirely. Visitors cannot click through even if they want to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email and internal tools stop.&lt;/strong&gt; TLS on SMTP, IMAP and admin panels fails just as hard, and those certificates are the ones nobody watches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust erodes fastest.&lt;/strong&gt; A business with an expired certificate on its own site is a contradiction visitors remember. Search engines also see the warning state, and conversion-focused pages lose their traffic for as long as the warning stands.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three famous outages caused by one expired certificate
&lt;/h2&gt;

&lt;p&gt;Certificate expiry is not a small-company problem. Some of the most expensive outages of the past decade were caused by exactly this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;What expired&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6 Dec 2018&lt;/td&gt;
&lt;td&gt;Ericsson (O2, SoftBank)&lt;/td&gt;
&lt;td&gt;Certificate in core mobile network software&lt;/td&gt;
&lt;td&gt;About 32 million O2 subscribers in the UK lost data for most of the day; tens of millions of SoftBank customers in Japan were cut off for hours (&lt;a href="https://www.theregister.com/2018/12/06/ericsson_o2_telefonica_uk_outage/" rel="noopener noreferrer"&gt;The Register&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 Feb 2020&lt;/td&gt;
&lt;td&gt;Microsoft Teams&lt;/td&gt;
&lt;td&gt;Authentication certificate&lt;/td&gt;
&lt;td&gt;Roughly three hours of global downtime for a service with 20 million daily users (&lt;a href="https://techcrunch.com/2020/02/03/microsoft-teams-has-been-down-this-morning/" rel="noopener noreferrer"&gt;TechCrunch&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19 Aug 2020&lt;/td&gt;
&lt;td&gt;Spotify&lt;/td&gt;
&lt;td&gt;Wildcard TLS certificate (*.wg.spotify.com)&lt;/td&gt;
&lt;td&gt;Around one hour of global outage across players and APIs (&lt;a href="https://www.datacenterdynamics.com/en/news/spotify-sees-hour-long-global-outage-forgot-renew-certificates/" rel="noopener noreferrer"&gt;Data Center Dynamics&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The lesson is uncomfortable: these companies employ world-class engineers, and the certificate still expired. Renewal is a process problem, not a skill problem, and a calendar reminder in someone's inbox is not a process.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check when your certificate expires
&lt;/h2&gt;

&lt;p&gt;Three ways, from quickest to most thorough:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. In the browser.&lt;/strong&gt; Click the padlock or tune icon in the address bar, open "Connection is secure", then "Certificate is valid". The details panel shows the exact validity period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. From the command line.&lt;/strong&gt; Query the live certificate with OpenSSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl s_client -connect example.com:443 -servername example.com 2&amp;gt;/dev/null | openssl x509 -noout -dates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output prints &lt;code&gt;notBefore&lt;/code&gt; and &lt;code&gt;notAfter&lt;/code&gt;; the second line is your deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check the whole chain, not just your own certificate.&lt;/strong&gt; Intermediate and root certificates expire too. When the AddTrust External CA Root expired on 30 May 2020, services with perfectly valid leaf certificates failed worldwide, including Roku devices and several payment platforms. An external scan that validates the full chain catches what a glance at your own certificate misses.&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%2Fimages.unsplash.com%2Fphoto-1495364141860-b0d03eccd065%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1495364141860-b0d03eccd065%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="A classic alarm clock on a desk, a reminder that certificate renewal is a deadline that does not move" width="1600" height="1060"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix an expired SSL certificate, step by step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: confirm expiry is really the cause.&lt;/strong&gt; Run the OpenSSL command above and read &lt;code&gt;notAfter&lt;/code&gt;. If the certificate is actually valid, the visitor's device clock is wrong, and nothing on your server needs fixing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: renew or reissue.&lt;/strong&gt; With Let's Encrypt or any other ACME client the renewal is one command (&lt;code&gt;sudo certbot renew&lt;/code&gt;), and a dry run (&lt;code&gt;sudo certbot renew --dry-run&lt;/code&gt;) tells you in advance whether validation will succeed. With a commercial CA, generate a fresh CSR in your control panel and complete validation there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: install the full chain everywhere TLS terminates.&lt;/strong&gt; The new certificate has to reach every endpoint that speaks TLS for the domain: origin servers, load balancers and the CDN edge. Deploy &lt;code&gt;fullchain.pem&lt;/code&gt; (leaf plus intermediates), not the leaf alone, or you trade a date error for &lt;a href="https://fortifynet.com/blog/err-cert-authority-invalid" rel="noopener noreferrer"&gt;a chain error&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: reload the service.&lt;/strong&gt; A renewed file on disk changes nothing until the server re-reads it. Run &lt;code&gt;sudo systemctl reload nginx&lt;/code&gt; (or the equivalent for Apache, HAProxy or your platform). This is the single most common failure in "automated" setups: the renewal cron works perfectly, the reload hook was never configured, and the server keeps serving the old certificate from memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: verify from the outside.&lt;/strong&gt; Check from a phone on mobile data, a fresh browser profile, or an external scanner. There is no propagation delay with certificates: the moment the server is reloaded, every new connection receives the new certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renewal is about to get relentless
&lt;/h2&gt;

&lt;p&gt;If renewal feels manageable today, the schedule is changing under your feet. Under CA/Browser Forum ballot SC-081v3, adopted in April 2025 by a unanimous 29-0 vote, the maximum lifetime of a public TLS certificate dropped from 398 to 200 days on 15 March 2026, falls to 100 days on 15 March 2027, and lands at 47 days on 15 March 2029 (&lt;a href="https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/" rel="noopener noreferrer"&gt;CA/Browser Forum&lt;/a&gt;). Let's Encrypt has gone further already: it &lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45" rel="noopener noreferrer"&gt;announced in December 2025&lt;/a&gt; that its default lifetime will drop from 90 to 45 days, and its six-day short-lived certificates became &lt;a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability" rel="noopener noreferrer"&gt;generally available on 15 January 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D700%26h%3D380%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2520%2522bar%2522%252C%2520%2522data%2522%253A%2520%257B%2522labels%2522%253A%2520%255B%2522Max%2520allowed%2520today%2522%252C%2520%2522Let%2527s%2520Encrypt%2520default%2522%252C%2520%2522Cap%2520from%2520Mar%25202029%2522%252C%2520%2522Let%2527s%2520Encrypt%2520planned%2520default%2522%252C%2520%2522Let%2527s%2520Encrypt%2520short-lived%2522%255D%252C%2520%2522datasets%2522%253A%2520%255B%257B%2522label%2522%253A%2520%2522Days%2522%252C%2520%2522data%2522%253A%2520%255B200%252C%252090%252C%252047%252C%252045%252C%25206%255D%252C%2520%2522backgroundColor%2522%253A%2520%2522%25232563eb%2522%257D%255D%257D%252C%2520%2522options%2522%253A%2520%257B%2522plugins%2522%253A%2520%257B%2522title%2522%253A%2520%257B%2522display%2522%253A%2520true%252C%2520%2522text%2522%253A%2520%2522TLS%2520certificate%2520lifetimes%2520are%2520shrinking%2520%2528days%2529%2522%257D%252C%2520%2522legend%2522%253A%2520%257B%2522display%2522%253A%2520false%257D%252C%2520%2522datalabels%2522%253A%2520%257B%2522display%2522%253A%2520true%252C%2520%2522anchor%2522%253A%2520%2522end%2522%252C%2520%2522align%2522%253A%2520%2522top%2522%252C%2520%2522color%2522%253A%2520%2522%2523111827%2522%257D%257D%252C%2520%2522scales%2522%253A%2520%257B%2522y%2522%253A%2520%257B%2522title%2522%253A%2520%257B%2522display%2522%253A%2520true%252C%2520%2522text%2522%253A%2520%2522Days%2522%257D%252C%2520%2522beginAtZero%2522%253A%2520true%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fw%3D700%26h%3D380%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2520%2522bar%2522%252C%2520%2522data%2522%253A%2520%257B%2522labels%2522%253A%2520%255B%2522Max%2520allowed%2520today%2522%252C%2520%2522Let%2527s%2520Encrypt%2520default%2522%252C%2520%2522Cap%2520from%2520Mar%25202029%2522%252C%2520%2522Let%2527s%2520Encrypt%2520planned%2520default%2522%252C%2520%2522Let%2527s%2520Encrypt%2520short-lived%2522%255D%252C%2520%2522datasets%2522%253A%2520%255B%257B%2522label%2522%253A%2520%2522Days%2522%252C%2520%2522data%2522%253A%2520%255B200%252C%252090%252C%252047%252C%252045%252C%25206%255D%252C%2520%2522backgroundColor%2522%253A%2520%2522%25232563eb%2522%257D%255D%257D%252C%2520%2522options%2522%253A%2520%257B%2522plugins%2522%253A%2520%257B%2522title%2522%253A%2520%257B%2522display%2522%253A%2520true%252C%2520%2522text%2522%253A%2520%2522TLS%2520certificate%2520lifetimes%2520are%2520shrinking%2520%2528days%2529%2522%257D%252C%2520%2522legend%2522%253A%2520%257B%2522display%2522%253A%2520false%257D%252C%2520%2522datalabels%2522%253A%2520%257B%2522display%2522%253A%2520true%252C%2520%2522anchor%2522%253A%2520%2522end%2522%252C%2520%2522align%2522%253A%2520%2522top%2522%252C%2520%2522color%2522%253A%2520%2522%2523111827%2522%257D%257D%252C%2520%2522scales%2522%253A%2520%257B%2522y%2522%253A%2520%257B%2522title%2522%253A%2520%257B%2522display%2522%253A%2520true%252C%2520%2522text%2522%253A%2520%2522Days%2522%257D%252C%2520%2522beginAtZero%2522%253A%2520true%257D%257D%257D%257D" alt="Bar chart showing TLS certificate lifetimes in 2026: 200 days maximum allowed today, 90 days Lets Encrypt default, 47 days cap from March 2029, 45 days planned Lets Encrypt default, 6 days short-lived certificates" width="1400" height="760"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Certificate lifetimes available and permitted in 2026. Sources: CA/Browser Forum ballot SC-081v3; Let's Encrypt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The direction is one-way: renewals that used to happen once a year will soon happen every six to seven weeks, and for short-lived certificates every few days. Manual renewal does not survive that schedule. Automation stops being a nice-to-have and becomes the only way to keep a certificate valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to never let a certificate expire again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automate issuance and renewal with ACME&lt;/strong&gt;, and configure the deploy hook that reloads your web server after each renewal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor expiry from outside.&lt;/strong&gt; An external check catches every certificate a renewal script silently skipped. Alert at 30, 14 and 7 days before expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory every certificate&lt;/strong&gt;, not just the main site: API subdomains, mail servers, staging hosts and internal panels expire on their own schedules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat renewal as a deployment.&lt;/strong&gt; It changes what production serves, so it deserves the same verification step as a code release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/ssl-tls-certificate-security" rel="noopener noreferrer"&gt;SSL/TLS certificate security: the complete guide to an A+ rating&lt;/a&gt; covers protocols, chains and configuration beyond expiry.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/your-connection-is-not-private" rel="noopener noreferrer"&gt;"Your connection is not private": what it means and how to fix it&lt;/a&gt; explains the warning screen your visitors are seeing right now.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/err-cert-authority-invalid" rel="noopener noreferrer"&gt;ERR_CERT_AUTHORITY_INVALID: what causes it and how to fix it&lt;/a&gt; handles the chain errors that often appear right after a rushed renewal.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fortifynet.com/blog/what-is-hsts" rel="noopener noreferrer"&gt;What is HSTS?&lt;/a&gt; explains why some visitors cannot bypass certificate warnings at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can visitors still open my site while the certificate is expired?&lt;/strong&gt;&lt;br&gt;
In most browsers they can click through an "Advanced" menu, but the majority will not, and if your site sends HSTS the browser removes that option entirely. API clients and apps refuse the connection outright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does an expired SSL certificate hurt SEO?&lt;/strong&gt;&lt;br&gt;
Indirectly, yes. Crawlers see the same invalid certificate, visitors bounce off the warning page, and HTTPS is a confirmed ranking signal. The longer the warning stands, the more traffic and trust you lose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does the fix take?&lt;/strong&gt;&lt;br&gt;
With ACME automation in place, minutes: renew, reload, verify. There is no propagation period, because the new certificate is served to every new connection immediately after the reload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my certificate expire even though auto-renewal was on?&lt;/strong&gt;&lt;br&gt;
The usual causes: the renewal ran but no reload hook was configured, so the old certificate stayed in memory; validation silently failed after a DNS or firewall change; or the renewal covered one hostname but not the certificate that actually expired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do expired certificates on internal tools matter?&lt;/strong&gt;&lt;br&gt;
Yes. Internal and API certificates fail the same way public ones do, and they are watched less. Many of the incidents in this article started with a certificate nobody had inventoried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your certificates before your visitors do
&lt;/h2&gt;

&lt;p&gt;An expiry date never negotiates. &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;Run a free FortifyNet scan&lt;/a&gt; to see your certificate's exact expiry date, the full chain, protocol configuration, security headers, DNS and email authentication in about 60 seconds. No signup required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/" rel="noopener noreferrer"&gt;CA/Browser Forum, Ballot SC-081v3&lt;/a&gt;; &lt;a href="https://letsencrypt.org/docs/cert-lifetimes/" rel="noopener noreferrer"&gt;Let's Encrypt, Certificate Lifetimes&lt;/a&gt;; &lt;a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability" rel="noopener noreferrer"&gt;Let's Encrypt, 6-day certificates GA&lt;/a&gt;; &lt;a href="https://www.theregister.com/2018/12/06/ericsson_o2_telefonica_uk_outage/" rel="noopener noreferrer"&gt;The Register on the Ericsson outage&lt;/a&gt;; &lt;a href="https://techcrunch.com/2020/02/03/microsoft-teams-has-been-down-this-morning/" rel="noopener noreferrer"&gt;TechCrunch on the Teams outage&lt;/a&gt;; &lt;a href="https://www.datacenterdynamics.com/en/news/spotify-sees-hour-long-global-outage-forgot-renew-certificates/" rel="noopener noreferrer"&gt;Data Center Dynamics on the Spotify outage&lt;/a&gt;; Acer et al., "Where the Wild Warnings Are", ACM CCS 2017.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/ssl-certificate-expired" rel="noopener noreferrer"&gt;fortifynet.com/blog/ssl-certificate-expired&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssl</category>
      <category>certificates</category>
      <category>tls</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>SPF, DKIM &amp; DMARC Rollout Guide: Zero to p=reject Step by Step</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Mon, 31 Aug 2026 04:46:55 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/spf-dkim-dmarc-rollout-guide-zero-to-preject-step-by-step-2mpj</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/spf-dkim-dmarc-rollout-guide-zero-to-preject-step-by-step-2mpj</guid>
      <description>&lt;p&gt;Email remains the single most abused entry point into organizations. According to the &lt;a href="https://www.verizon.com/business/resources/reports/dbir/" rel="noopener noreferrer"&gt;Verizon Data Breach Investigations Report&lt;/a&gt;, phishing and pretexting are involved in the majority of social-engineering breaches, and most of those start with a spoofed sender address. The three DNS-based authentication standards - SPF, DKIM and DMARC - exist to make spoofing your domain technically detectable and, at full enforcement, rejectable.&lt;/p&gt;

&lt;p&gt;This guide is written for IT managers who need to take a real organization - with newsletters, a CRM, a helpdesk, invoicing software and a marketing platform all sending mail "from" the company domain - from zero email authentication to an enforced &lt;strong&gt;DMARC policy of p=reject&lt;/strong&gt;, without losing a single legitimate message on the way. Expect the full rollout to take 6–10 weeks of calendar time, with only a few hours of hands-on work per week.&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%2Fimages.unsplash.com%2Fphoto-1558494949-ef010cbdcc31%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1558494949-ef010cbdcc31%3Fw%3D1200%26q%3D80" alt="Server room with network cables"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the three protocols fit together
&lt;/h2&gt;

&lt;p&gt;Before touching DNS, make sure the mental model is right, because most failed rollouts come from misunderstanding &lt;em&gt;alignment&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; (RFC 7208) publishes, in DNS, the list of IP addresses allowed to send mail using your domain in the SMTP envelope (MAIL FROM).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; (RFC 6376) cryptographically signs each message with a private key; the public key is published in DNS. Receivers verify the signature to confirm the message was not altered and was authorized by the signing domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; (RFC 7489) ties both together. It tells receivers: "if a message claims to be from my domain in the visible From: header, it must pass SPF &lt;strong&gt;or&lt;/strong&gt; DKIM &lt;em&gt;in alignment&lt;/em&gt; with that domain - and here is what to do if it does not, and here is where to send reports."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The verification flow at the receiving mail server looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            Incoming message claiming From: billing@example.com
                                 |
              +------------------+------------------+
              |                                     |
        SPF check                              DKIM check
   Is sending IP listed in               Does the d= signature
   example.com's SPF record              verify against the DNS
   for the envelope sender?              public key?
              |                                     |
              +------------------+------------------+
                                 |
                          DMARC alignment
        Does the domain that passed (envelope domain for SPF,
        d= domain for DKIM) match the visible From: domain?
                                 |
              +------------------+------------------+
              |                                     |
         PASS: deliver                    FAIL: apply policy
                                          p=none  -&amp;gt; deliver, report
                                          p=quarantine -&amp;gt; spam folder
                                          p=reject -&amp;gt; refuse message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crucial detail: a message can pass SPF and DKIM &lt;em&gt;for some domain&lt;/em&gt; and still fail DMARC, because DMARC requires the passing domain to &lt;strong&gt;align&lt;/strong&gt; with the From: header domain your users actually see. This is exactly what stops a phisher who signs mail with their own domain's DKIM key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 0 - Inventory every legitimate sender (week 1)
&lt;/h2&gt;

&lt;p&gt;The number one cause of lost mail during DMARC rollouts is a forgotten sending service. Before publishing anything, build a sender inventory. Interview the marketing, sales, finance and HR teams, and check DNS for existing include: mechanisms.&lt;/p&gt;

&lt;p&gt;Typical inventory for a 50-person company:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sending service&lt;/th&gt;
&lt;th&gt;What it sends&lt;/th&gt;
&lt;th&gt;Envelope domain&lt;/th&gt;
&lt;th&gt;DKIM capable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Workspace / M365&lt;/td&gt;
&lt;td&gt;Day-to-day corporate mail&lt;/td&gt;
&lt;td&gt;example.com&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailchimp / Brevo&lt;/td&gt;
&lt;td&gt;Newsletters&lt;/td&gt;
&lt;td&gt;mail.example.com&lt;/td&gt;
&lt;td&gt;Yes (CNAME)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HubSpot / Pipedrive&lt;/td&gt;
&lt;td&gt;Sales sequences&lt;/td&gt;
&lt;td&gt;example.com&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zendesk / Freshdesk&lt;/td&gt;
&lt;td&gt;Support replies&lt;/td&gt;
&lt;td&gt;example.com&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fortnox / QuickBooks&lt;/td&gt;
&lt;td&gt;Invoices&lt;/td&gt;
&lt;td&gt;often their own domain&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Website contact form&lt;/td&gt;
&lt;td&gt;Notifications&lt;/td&gt;
&lt;td&gt;web server IP&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring/CI (cron, Jenkins)&lt;/td&gt;
&lt;td&gt;Alerts&lt;/td&gt;
&lt;td&gt;server hostname&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules of thumb from this phase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Anything that cannot do DKIM should be migrated to send through an authenticated relay (e.g. your Workspace SMTP relay or a transactional provider such as Postmark or SES) rather than directly from a web server.&lt;/li&gt;
&lt;li&gt;Do not guess. The DMARC monitoring phase below will catch what interviews miss - that is what it is for.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Phase 1 - Publish a correct SPF record (week 1–2)
&lt;/h2&gt;

&lt;p&gt;SPF is one TXT record on the root of your domain. Collect the include mechanisms from each provider's documentation and merge them into a single record - &lt;strong&gt;a domain must have exactly one SPF record&lt;/strong&gt;; two records cause a permanent error (permerror) and effectively disable SPF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com.  IN TXT  "v=spf1 include:_spf.google.com include:servers.mcsv.net include:_spf.hubspot.com ip4:203.0.113.25 ~all"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End with ~all (softfail), not -all, during rollout.&lt;/strong&gt; DMARC does not need -all to work, and -all combined with an incomplete inventory bounces legitimate mail immediately. You can tighten to -all after monitoring confirms the inventory is complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay under 10 DNS lookups.&lt;/strong&gt; Every include, a, mx and redirect mechanism costs a lookup; exceeding ten is a permerror per RFC 7208 §4.6.4. Count the nested lookups inside each include (Google's include alone costs three). If you are near the limit, use SPF flattening cautiously or move bulk senders to a subdomain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use ip4:/ip6: for servers you control&lt;/strong&gt; instead of a: mechanisms - they cost no lookups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validate with a checker (FortifyNet's &lt;a href="https://dev.to/tools/mx-lookup"&gt;MX &amp;amp; email security tool&lt;/a&gt; shows SPF, DKIM and DMARC in one pass) and with dig:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 2 - Enable DKIM everywhere (week 2–3)
&lt;/h2&gt;

&lt;p&gt;DKIM setup is per-provider, but always follows the same shape: the provider generates a key pair, you publish the public key at selector._domainkey.example.com, and the provider starts signing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Workspace:&lt;/strong&gt; Admin console → Apps → Gmail → Authenticate email. Generate a &lt;strong&gt;2048-bit&lt;/strong&gt; key (1024-bit keys are considered weak; NIST and M3AAWG both recommend 2048). Publish the TXT record, wait for DNS propagation, click "Start authentication".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft 365:&lt;/strong&gt; Defender portal → Email authentication settings → DKIM. Microsoft uses two CNAME records (selector1, selector2) that enable automatic key rotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailchimp, HubSpot, Zendesk etc.:&lt;/strong&gt; each has an "authenticate your domain" wizard that gives you 2–4 CNAME records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify each selector after publishing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT google._domainkey.example.com
dig +short CNAME selector1._domainkey.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send a test message to a Gmail address, open "Show original", and confirm you see &lt;strong&gt;DKIM: PASS with domain example.com&lt;/strong&gt; - the "with domain" part is the alignment that DMARC will check.&lt;/p&gt;

&lt;p&gt;Plan for &lt;strong&gt;key rotation every 6–12 months&lt;/strong&gt;. Providers with CNAME-based setups rotate for you; for manually published keys, put a recurring calendar item on the security calendar now, because nobody remembers later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3 - DMARC in monitoring mode (week 3–4)
&lt;/h2&gt;

&lt;p&gt;Publish a DMARC record at _dmarc.example.com with policy &lt;strong&gt;p=none&lt;/strong&gt;. This changes nothing about delivery - it only turns on reporting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_dmarc.example.com.  IN TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; fo=1; adkim=r; aspf=r"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each tag does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Take no action on failures yet - just report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rua&lt;/td&gt;
&lt;td&gt;mailto:…&lt;/td&gt;
&lt;td&gt;Where daily aggregate (XML) reports are sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fo&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Generate a failure report if &lt;em&gt;any&lt;/em&gt; mechanism fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;adkim / aspf&lt;/td&gt;
&lt;td&gt;r&lt;/td&gt;
&lt;td&gt;Relaxed alignment: subdomains of example.com count as aligned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sp&lt;/td&gt;
&lt;td&gt;(unset)&lt;/td&gt;
&lt;td&gt;Subdomain policy - inherit p for now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a dedicated mailbox or, far better, a DMARC report processor - raw reports are gzipped XML and arrive from every large receiver daily. Free tiers of common analyzers are enough for a single domain.&lt;/li&gt;
&lt;li&gt;If the report address is on a different domain than the one being monitored, the report domain must publish an external destination verification record (RFC 7489 §7.1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let monitoring run at least two full weeks&lt;/strong&gt;, ideally four, so monthly jobs (invoice runs, board reports) appear in the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phase 4 - Read the reports and fix what fails (week 4–6)
&lt;/h2&gt;

&lt;p&gt;Aggregate reports answer three questions for every sending IP: how much mail, did SPF align, did DKIM align. Triage into three buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Aligned pass (SPF or DKIM):&lt;/strong&gt; healthy. The bulk of your Workspace/M365 mail should be here from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your service, failing alignment:&lt;/strong&gt; e.g. the CRM passes DKIM but signs with d=crm-provider.com. Fix by completing that provider's domain authentication wizard so it signs with your domain. This bucket is your Phase 4 work queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown IPs failing everything:&lt;/strong&gt; this is spoofing (or forwarding). You do not fix this - enforcement will neutralize it. Its presence is the business case for this whole project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One legitimate source will keep failing no matter what you do: &lt;strong&gt;mailing lists and some forwarders&lt;/strong&gt; break SPF (new sending IP) and sometimes DKIM (footer added). ARC (RFC 8617) helps at large receivers, but the honest answer is that a small residual failure rate (well under 1%) is normal and acceptable at enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 5 - Ramp to enforcement (week 6–10)
&lt;/h2&gt;

&lt;p&gt;Never jump from p=none straight to p=reject. Use the pct tag to ramp:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Record change&lt;/th&gt;
&lt;th&gt;Hold time&lt;/th&gt;
&lt;th&gt;Rollback trigger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;p=quarantine; pct=10&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;any report of legitimate mail in spam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;p=quarantine; pct=50&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;p=quarantine (pct=100)&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;p=reject&lt;/td&gt;
&lt;td&gt;permanent&lt;/td&gt;
&lt;td&gt;aligned-pass rate drops below ~98%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At each step, watch two signals: the aggregate reports (aligned-pass percentage for known senders should stay flat) and the helpdesk (tickets about "customer says my mail bounced"). Rollback is a single DNS change back to the previous step and takes effect within the record's TTL - set the TTL to 3600 seconds during the ramp so mistakes are cheap.&lt;/p&gt;

&lt;p&gt;When you reach p=reject, finish the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add an explicit subdomain policy: &lt;strong&gt;sp=reject&lt;/strong&gt;, unless a subdomain intentionally sends with looser rules.&lt;/li&gt;
&lt;li&gt;Protect &lt;strong&gt;parked domains&lt;/strong&gt; you own but never send from: give each v=spf1 -all, an empty DKIM policy, and v=DMARC1; p=reject. Parked domains are the spoofer's favourite target.&lt;/li&gt;
&lt;li&gt;Consider &lt;strong&gt;BIMI&lt;/strong&gt; (brand logo in inboxes) - it requires enforced DMARC, so you have just unlocked it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;permerror in reports&lt;/td&gt;
&lt;td&gt;two SPF records, or &amp;gt;10 lookups&lt;/td&gt;
&lt;td&gt;merge records; reduce includes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM fail only at one receiver&lt;/td&gt;
&lt;td&gt;DNS propagation or truncated TXT&lt;/td&gt;
&lt;td&gt;re-check record; split 2048-bit key correctly across strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newsletter fails alignment&lt;/td&gt;
&lt;td&gt;ESP signs with own domain&lt;/td&gt;
&lt;td&gt;complete ESP domain authentication (CNAMEs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Own web server mail rejected&lt;/td&gt;
&lt;td&gt;form mail sent directly from web host&lt;/td&gt;
&lt;td&gt;route through authenticated SMTP relay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forwarded mail quarantined&lt;/td&gt;
&lt;td&gt;forwarding breaks SPF&lt;/td&gt;
&lt;td&gt;expected; rely on DKIM surviving, accept residual&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The rollout 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;Week&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Deliverable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Sender inventory interviews&lt;/td&gt;
&lt;td&gt;signed-off sender table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1–2&lt;/td&gt;
&lt;td&gt;SPF published and validated&lt;/td&gt;
&lt;td&gt;one record, under 10 lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;td&gt;DKIM per provider, 2048-bit&lt;/td&gt;
&lt;td&gt;PASS with aligned domain at Gmail test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3–4&lt;/td&gt;
&lt;td&gt;DMARC p=none + report processor&lt;/td&gt;
&lt;td&gt;reports flowing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4–6&lt;/td&gt;
&lt;td&gt;Report triage, fix alignment gaps&lt;/td&gt;
&lt;td&gt;bucket 2 empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6–10&lt;/td&gt;
&lt;td&gt;Ramp: q10 → q50 → q100 → reject&lt;/td&gt;
&lt;td&gt;p=reject live, sp=reject set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Print this table into the project ticket; each row is a natural weekly status update for management, and the deliverable column keeps the project honest about what "done" means at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond DMARC - MTA-STS and TLS-RPT
&lt;/h2&gt;

&lt;p&gt;DMARC authenticates &lt;em&gt;who&lt;/em&gt; sent a message; it says nothing about whether the message travelled encrypted. SMTP still falls back to plaintext when TLS negotiation fails, and an active attacker on the network path can force exactly that failure. Two companion standards close the gap, and since you are already in the DNS console this is the right week to deploy them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MTA-STS&lt;/strong&gt; (RFC 8461) lets your domain declare "only deliver mail to me over authenticated TLS". It consists of a DNS record and a small policy file served over HTTPS:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`&lt;br&gt;
_mta-sts.example.com.  IN TXT  "v=STSv1; id=20260828T000000"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mta-sts.example.com/.well-known/mta-sts.txt:" rel="noopener noreferrer"&gt;https://mta-sts.example.com/.well-known/mta-sts.txt:&lt;/a&gt;&lt;br&gt;
  version: STSv1&lt;br&gt;
  mode: testing&lt;br&gt;
  mx: mail.example.com&lt;br&gt;
  max_age: 86400&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Run mode: testing for two weeks, then switch to mode: enforce and raise max_age to 604800. The mx lines must match your real MX hostnames exactly - a typo here silently drops inbound mail in enforce mode, which is why the testing stage exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS-RPT&lt;/strong&gt; (RFC 8460) is the reporting channel for MTA-STS, exactly as rua reporting is for DMARC:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
_smtp._tls.example.com.  IN TXT  "v=TLSRPTv1; rua=mailto:tls-reports@example.com"&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Senders that fail to deliver over TLS tell you why, daily, in machine-readable JSON. In practice the reports are quiet - which is precisely the confirmation you want before enforcing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading an aggregate report by hand
&lt;/h2&gt;

&lt;p&gt;Even with an analyzer, read one raw report once so the data stops being a black box. Inside the gzipped XML each record block contains three things that matter:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;record&amp;gt;&lt;br&gt;
  &amp;lt;row&amp;gt;&lt;br&gt;
    &amp;lt;source_ip&amp;gt;198.51.100.7&amp;lt;/source_ip&amp;gt;&lt;br&gt;
    &amp;lt;count&amp;gt;412&amp;lt;/count&amp;gt;&lt;br&gt;
    &amp;lt;policy_evaluated&amp;gt;&lt;br&gt;
      &amp;lt;disposition&amp;gt;none&amp;lt;/disposition&amp;gt;&lt;br&gt;
      &amp;lt;dkim&amp;gt;fail&amp;lt;/dkim&amp;gt;&lt;br&gt;
      &amp;lt;spf&amp;gt;pass&amp;lt;/spf&amp;gt;&lt;br&gt;
    &amp;lt;/policy_evaluated&amp;gt;&lt;br&gt;
  &amp;lt;/row&amp;gt;&lt;br&gt;
  &amp;lt;identifiers&amp;gt;&lt;br&gt;
    &amp;lt;header_from&amp;gt;example.com&amp;lt;/header_from&amp;gt;&lt;br&gt;
  &amp;lt;/identifiers&amp;gt;&lt;br&gt;
&amp;lt;/record&amp;gt;&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Read it as a sentence: "198.51.100.7 sent 412 messages claiming to be example.com; SPF aligned, DKIM did not; under the current policy nothing was done." Look the IP up (reverse DNS plus the sender's published ranges) and you know whether this row is bucket 1, 2 or 3 from Phase 4. Ten minutes with one report teaches more than any dashboard tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance - keeping it enforced
&lt;/h2&gt;

&lt;p&gt;Email authentication is not a project with an end date; it is a small standing capability. Bake these into normal operations before closing the ticket:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change control on DNS:&lt;/strong&gt; every new SaaS tool that "just needs to send email as us" goes through you, gets DKIM set up properly, and is added to the inventory table. The alternative is that marketing signs up for a tool in November and its mail silently lands in spam under your p=reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly report review:&lt;/strong&gt; fifteen minutes per quarter scanning the analyzer for new failing sources. Calendar it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring the records themselves:&lt;/strong&gt; SPF, DKIM and DMARC records can be broken by an innocent DNS migration. External monitoring that alerts on any change to these records - FortifyNet's domain monitoring does this continuously - turns a silent regression into a same-day fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offboarding:&lt;/strong&gt; when a sending service is cancelled, remove its include and DKIM records the same week. Stale includes are both a lookup-budget leak and a supply-chain risk: if the provider's IP space is re-assigned, whoever inherits it can send authenticated mail as you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Sender inventory reviewed by marketing, finance, support&lt;/li&gt;
&lt;li&gt;[ ] Exactly one SPF record, under 10 lookups, ends ~all (then -all)&lt;/li&gt;
&lt;li&gt;[ ] DKIM 2048-bit, verified per selector, rotation scheduled&lt;/li&gt;
&lt;li&gt;[ ] DMARC p=none with rua reporting, 2–4 weeks of data reviewed&lt;/li&gt;
&lt;li&gt;[ ] All legitimate senders aligned (bucket 2 empty)&lt;/li&gt;
&lt;li&gt;[ ] Ramp: quarantine 10% → 50% → 100% → reject, one week+ per step&lt;/li&gt;
&lt;li&gt;[ ] sp=reject set; parked domains locked down&lt;/li&gt;
&lt;li&gt;[ ] Run a full &lt;a href="https://dev.to/Onboarding"&gt;FortifyNet scan&lt;/a&gt; to confirm the records grade cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;RFC 7208 - Sender Policy Framework (SPF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6376" rel="noopener noreferrer"&gt;RFC 6376 - DomainKeys Identified Mail (DKIM)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;RFC 7489 - Domain-based Message Authentication, Reporting &amp;amp; Conformance (DMARC)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.m3aawg.org/published-documents" rel="noopener noreferrer"&gt;M3AAWG Email Authentication Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.google.com/a/answer/2466580" rel="noopener noreferrer"&gt;Google Workspace - Set up DMARC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/defender-office-365/email-authentication-dmarc-configure" rel="noopener noreferrer"&gt;Microsoft 365 - Use DMARC to validate email&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.verizon.com/business/resources/reports/dbir/" rel="noopener noreferrer"&gt;Verizon Data Breach Investigations Report&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/guide-email-authentication-rollout" rel="noopener noreferrer"&gt;fortifynet.com/blog/guide-email-authentication-rollout&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deepguide</category>
      <category>email</category>
      <category>dmarc</category>
      <category>spf</category>
    </item>
    <item>
      <title>HTTP Security Headers Hardening Guide: HSTS, CSP &amp; More Step by Step</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Sun, 30 Aug 2026 04:45:49 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/http-security-headers-hardening-guide-hsts-csp-more-step-by-step-1m22</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/http-security-headers-hardening-guide-hsts-csp-more-step-by-step-1m22</guid>
      <description>&lt;p&gt;HTTP security headers are the cheapest security control you will ever deploy: a handful of response headers, set once at the web server or CDN, that instruct every visiting browser to refuse whole classes of attack - clickjacking, protocol downgrade, MIME sniffing, cross-site data leaks and large parts of cross-site scripting. Yet the majority of websites still ship without them, or with values copied from a blog post that silently break functionality.&lt;/p&gt;

&lt;p&gt;This guide walks an IT manager through a complete, staged hardening of a production website's headers: what each header does, the exact values to deploy, how to roll out Content-Security-Policy without breaking the site, web-server configuration for nginx and Apache, and how to verify the result. Budget one working day of effort spread over two to three weeks of calendar time (the CSP report-only period accounts for most 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%2Fimages.unsplash.com%2Fphoto-1555949963-aa79dcee981c%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1555949963-aa79dcee981c%3Fw%3D1200%26q%3D80" alt="Code on a screen in a dark room"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How headers protect users
&lt;/h2&gt;

&lt;p&gt;Every header in this guide is an instruction from your server to the visitor's browser. The browser enforces it locally - no appliance, agent or code change required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Your server                          Visitor's browser
   -----------                          -----------------
   HTTP/1.1 200 OK
   Strict-Transport-Security  ------&amp;gt;   "Never load this site over HTTP again"
   Content-Security-Policy    ------&amp;gt;   "Only run scripts from these origins"
   X-Frame-Options            ------&amp;gt;   "Refuse to render this site in an iframe"
   X-Content-Type-Options     ------&amp;gt;   "Never guess file types"
   Referrer-Policy            ------&amp;gt;   "Don't leak full URLs to other sites"
   Permissions-Policy         ------&amp;gt;   "This site may not use camera/mic/location"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because enforcement happens in the browser, headers protect your &lt;em&gt;users&lt;/em&gt; even when the attack does not touch your server at all - for example a phishing page framing your login form, or an injected ad script exfiltrating form data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Baseline: measure before you change anything
&lt;/h2&gt;

&lt;p&gt;Run your site through a header scanner - FortifyNet's free &lt;a href="https://dev.to/tools/security-headers"&gt;security headers checker&lt;/a&gt; grades each header and shows a one-line fix, and &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt; is a good second opinion. Save the result. A typical unhardened site scores an F with only Server and X-Powered-By present - both of which &lt;em&gt;leak&lt;/em&gt; information rather than protect anything.&lt;/p&gt;

&lt;p&gt;Also inventory what the site actually loads. Open the browser dev tools Network tab on your five most important pages and note every third-party origin: analytics, fonts, tag managers, chat widgets, payment iframes, video embeds. This list becomes your CSP allowlist in Step 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 - Deploy the five safe headers immediately
&lt;/h2&gt;

&lt;p&gt;Five headers are effectively risk-free for almost every site and should go out in the first change window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strict-Transport-Security (HSTS)
&lt;/h3&gt;

&lt;p&gt;Forces HTTPS for every future visit, defeating SSL-stripping attacks on public Wi-Fi.&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;Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with max-age=31536000 (one year) only if all subdomains already serve HTTPS; otherwise begin with max-age=86400 and includeSubDomains omitted, then expand. Once stable, consider &lt;a href="https://hstspreload.org" rel="noopener noreferrer"&gt;HSTS preload&lt;/a&gt; - being hard-coded into browsers removes even the first-visit window, but is effectively irreversible, so treat preload as a one-way door.&lt;/p&gt;

&lt;h3&gt;
  
  
  X-Content-Type-Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stops browsers from "sniffing" file types, which prevents a user-uploaded file served as text from being executed as script. There is no legitimate reason to omit this.&lt;/p&gt;

&lt;h3&gt;
  
  
  X-Frame-Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blocks clickjacking - an attacker overlaying your real, framed site with invisible buttons. Use SAMEORIGIN instead if your own site legitimately frames itself (some admin panels and preview features do). Modern CSP frame-ancestors supersedes this header, but keep both for older browsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Referrer-Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Referrer-Policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensures links to external sites reveal only your origin (&lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt;) rather than the full URL - which may contain search terms, account paths or reset tokens. This value is also the modern browser default, but setting it explicitly guarantees it everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions-Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Declares which powerful browser features the site (and any injected script) may use. Deny everything you do not use; a compromised third-party script then cannot silently request the camera. Add features back (e.g. payment=(self "&lt;a href="https://js.stripe.com%22)" rel="noopener noreferrer"&gt;https://js.stripe.com")&lt;/a&gt;) only where genuinely needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 - Remove headers that leak
&lt;/h2&gt;

&lt;p&gt;While editing configuration, strip the chatty defaults: &lt;strong&gt;Server&lt;/strong&gt; version strings and &lt;strong&gt;X-Powered-By&lt;/strong&gt;. They provide zero function and hand attackers your exact software versions for CVE matching. In nginx: server_tokens off;. In Apache: ServerTokens Prod and ServerSignature Off. For PHP: expose_php = Off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - Content-Security-Policy, the staged way
&lt;/h2&gt;

&lt;p&gt;CSP is the most powerful header - and the only one that can break your site if deployed carelessly. Never copy a CSP from another site. Follow this three-stage process instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage A: Report-Only (1–2 weeks)
&lt;/h3&gt;

&lt;p&gt;Deploy the policy in &lt;strong&gt;Content-Security-Policy-Report-Only&lt;/strong&gt; mode. Browsers evaluate it and report violations, but block nothing:&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;Content-Security-Policy-Report-Only: default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://js.stripe.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
  frame-src https://js.stripe.com https://www.youtube-nocookie.com;
  connect-src 'self' https://www.google-analytics.com;
  frame-ancestors 'none';
  report-uri /csp-report
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the allowlist from your Step 1 inventory. Point report-uri at a small endpoint (or a hosted service) and let real traffic exercise every page, including the checkout and the rarely-visited account pages, for at least a week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage B: Fix the violations
&lt;/h3&gt;

&lt;p&gt;Review the reports and dev-tools console. The usual findings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Violation&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inline script blocked&lt;/td&gt;
&lt;td&gt;onclick= handlers, inline script tags&lt;/td&gt;
&lt;td&gt;move to external files, or add per-response nonces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eval blocked&lt;/td&gt;
&lt;td&gt;old libraries, some tag managers&lt;/td&gt;
&lt;td&gt;upgrade library; avoid 'unsafe-eval'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unknown origin&lt;/td&gt;
&lt;td&gt;a script loading a script&lt;/td&gt;
&lt;td&gt;add the origin, or drop the widget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;data: font&lt;/td&gt;
&lt;td&gt;icon fonts embedded in CSS&lt;/td&gt;
&lt;td&gt;allow font-src data: or switch to SVG icons&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gold standard for scripts is &lt;strong&gt;nonces&lt;/strong&gt;: the server generates a random value per response, adds it to the header (script-src 'nonce-R4nd0m') and to each legitimate script tag. Injected scripts lack the nonce and die. 'strict-dynamic' (CSP Level 3) then lets a nonce-approved script load its own dependencies - this is Google's recommended pattern and dramatically shrinks the allowlist.&lt;/p&gt;

&lt;p&gt;Accept 'unsafe-inline' for &lt;strong&gt;styles&lt;/strong&gt; if removing it is disproportionate work - the practical risk of inline CSS is far lower than inline JS. Do not accept 'unsafe-inline' for scripts; it disables the header's main value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage C: Enforce
&lt;/h3&gt;

&lt;p&gt;Rename the header to &lt;strong&gt;Content-Security-Policy&lt;/strong&gt;, keep the reporting endpoint active permanently, and watch the first 48 hours. Any regression is one config line away from rollback to Report-Only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 - Web-server configuration
&lt;/h2&gt;

&lt;p&gt;Set headers in one central place - the web server or CDN - not per-application, so every response (including error pages and static assets) is covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nginx&lt;/strong&gt; (inside the server block; always is required so headers also attach to 4xx/5xx responses):&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;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Permissions-Policy&lt;/span&gt; &lt;span class="s"&gt;"camera=(),&lt;/span&gt; &lt;span class="s"&gt;microphone=(),&lt;/span&gt; &lt;span class="s"&gt;geolocation=()"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Content-Security-Policy&lt;/span&gt; &lt;span class="s"&gt;"default-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;..."&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beware the classic nginx trap: add_header directives are &lt;strong&gt;not inherited&lt;/strong&gt; if a location block declares its own add_header. Keep them all at server level, or repeat the full set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache&lt;/strong&gt; (in the vhost or .htaccess, with mod_headers enabled):&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;ServerTokens&lt;/span&gt; Prod
&lt;span class="nc"&gt;ServerSignature&lt;/span&gt; &lt;span class="ss"&gt;Off&lt;/span&gt;
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Strict-Transport-Security "max-age=31536000; includeSubDomains"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Content-Type-Options "nosniff"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Frame-Options "DENY"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Referrer-Policy "strict-origin-when-cross-origin"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Permissions-Policy "camera=(), microphone=(), geolocation=()"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Content-Security-Policy "default-src 'self'; ..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On CDNs (Cloudflare, CloudFront, Vercel, Netlify) use their response-header transforms so the edge applies the same set to cached responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 - Verify and keep verifying
&lt;/h2&gt;

&lt;p&gt;After each change: hard-refresh with dev tools open and read the actual response headers; then re-run the &lt;a href="https://dev.to/tools/security-headers"&gt;headers checker&lt;/a&gt; and compare against the Step 1 baseline. A well-hardened site reaches grade A.&lt;/p&gt;

&lt;p&gt;Headers rot. A redesign adds a widget that CSP blocks; a migration to a new load balancer silently drops the whole set. Put verification on a schedule - FortifyNet's continuous monitoring re-checks headers and alerts on any change, which is exactly the "config drift" failure mode that manual processes miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cookie hardening - the forgotten response headers
&lt;/h2&gt;

&lt;p&gt;Session cookies are set by response headers too, and a hardened header set is incomplete without them. Three attributes on every Set-Cookie:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt; - the cookie is never sent over plain HTTP. With HSTS in place this closes the loop entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HttpOnly&lt;/strong&gt; - JavaScript cannot read the cookie, so even a successful XSS cannot exfiltrate the session token directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SameSite=Lax&lt;/strong&gt; (or Strict) - the browser withholds the cookie on cross-site requests, neutralizing most CSRF attacks at zero code cost. Use SameSite=None; Secure only for cookies that genuinely must travel cross-site (embedded widgets, SSO flows).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider cookie &lt;strong&gt;prefixes&lt;/strong&gt; for defense in depth: a cookie named __Host-session is only accepted by browsers if it is Secure, has no Domain attribute and Path=/ - making it impossible for a compromised subdomain to overwrite it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-origin isolation - COOP, CORP and CORS sanity
&lt;/h2&gt;

&lt;p&gt;Three newer headers control how your site interacts with other origins at the browser level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Recommended value&lt;/th&gt;
&lt;th&gt;What it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Origin-Opener-Policy&lt;/td&gt;
&lt;td&gt;same-origin&lt;/td&gt;
&lt;td&gt;other sites holding a scriptable reference to your window (tabnabbing, XS-Leaks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Origin-Resource-Policy&lt;/td&gt;
&lt;td&gt;same-origin (or same-site)&lt;/td&gt;
&lt;td&gt;other origins embedding your resources (data theft via inclusion)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Origin-Embedder-Policy&lt;/td&gt;
&lt;td&gt;require-corp (advanced)&lt;/td&gt;
&lt;td&gt;loading cross-origin resources that haven't opted in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;COOP: same-origin and CORP: same-origin are safe for most sites and worth deploying in the same change as Step 2. COEP is stricter - it breaks any cross-origin image or script that does not send CORP headers itself - so deploy it only if you need cross-origin isolation (e.g. for SharedArrayBuffer) and test in Report-Only first (Cross-Origin-Embedder-Policy-Report-Only exists for exactly this).&lt;/p&gt;

&lt;p&gt;While here, sanity-check &lt;strong&gt;CORS&lt;/strong&gt;: Access-Control-Allow-Origin: * on an API that uses cookies or returns user data is a real vulnerability, not a convenience. Allow specific origins, and never reflect the request's Origin header back unvalidated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching sensitive responses
&lt;/h2&gt;

&lt;p&gt;Header hardening includes making sure private data is not stored where you cannot delete it. For any authenticated or personal-data response:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Cache-Control: no-store&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;no-store beats the older Pragma and Expires combinations and prevents both browser and intermediary caching. The classic failure: a logout button that works, but the back button still shows the account page from browser cache - no-store on authenticated pages fixes it. Keep long-lived caching (Cache-Control: public, max-age=31536000, immutable) for fingerprinted static assets; the two policies coexist per-route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes for SPAs and frameworks
&lt;/h2&gt;

&lt;p&gt;Single-page applications change the CSP calculus slightly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The HTML shell is served once, so &lt;strong&gt;nonces must be generated per response&lt;/strong&gt; at the edge or server - a static site generator that bakes one nonce into the build has zero security value. If the site is fully static, use hashes (sha256-…) of the specific inline scripts instead of nonces.&lt;/li&gt;
&lt;li&gt;Hydration frameworks (Next.js, Nuxt) inline state as script tags; both support nonce propagation from middleware - use the framework's documented CSP integration rather than fighting it.&lt;/li&gt;
&lt;li&gt;connect-src is the SPA's most active directive: every API origin, websocket endpoint and telemetry sink must be listed. Missing entries fail &lt;em&gt;silently&lt;/em&gt; for users (requests blocked, features dead) - another reason the Report-Only stage is non-negotiable.&lt;/li&gt;
&lt;li&gt;For APIs consumed by browsers, apply the same base headers to API responses: nosniff, no-store where relevant, and a restrictive CSP (default-src 'none') on any endpoint that could ever be opened directly in a browser tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verifying from the command line
&lt;/h2&gt;

&lt;p&gt;Scanners are convenient, but you should also be able to verify headers directly - especially when debugging why a header is present on one route but missing on another (the nginx inheritance trap, a CDN transform applied to only one path pattern, or an application framework overriding the server). Use curl with the head-request flag (uppercase I), or the Network panel in the browser's developer tools, and read the raw response headers for three URLs: the homepage, an authenticated-area route, and a page that does not exist. A healthy response looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
HTTP/2 200&lt;br&gt;
content-security-policy: default-src 'self'; ...&lt;br&gt;
permissions-policy: camera=(), microphone=(), geolocation=()&lt;br&gt;
referrer-policy: strict-origin-when-cross-origin&lt;br&gt;
strict-transport-security: max-age=31536000; includeSubDomains&lt;br&gt;
x-content-type-options: nosniff&lt;br&gt;
x-frame-options: DENY&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Check all three cases: the homepage, an authenticated-area route, and a 404. Repeat the check over both HTTP and HTTPS, and on the www and bare-domain variants if both are served - redirects frequently come from a different configuration block that carries none of the hardening, and an SSL-stripping attack begins at exactly that unprotected first hop. The error page is the other classic gap - frameworks often serve error responses through a different code path that skips middleware, and an attacker probing your site sees mostly error pages. If the 404 lacks headers that the homepage has, the set is not applied centrally enough; move it to the server or edge layer.&lt;/p&gt;

&lt;p&gt;Two more habits worth automating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI check:&lt;/strong&gt; a five-line script in the deployment pipeline that curls the staging URL and fails the build if an expected header is missing. This catches the "new load balancer dropped everything" regression before production does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change alerting in production:&lt;/strong&gt; external monitoring that re-reads the live header set on a schedule and alerts on any difference - this is part of FortifyNet's continuous monitoring, and it is how you learn about the regression that CI could not see, such as a CDN configuration edit made directly in the provider's dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common myths and mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"We have a WAF, headers are redundant."&lt;/strong&gt; A WAF inspects traffic to your server; headers control what the &lt;em&gt;browser&lt;/em&gt; does with your pages, including attacks that never touch your infrastructure. They are complementary layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"X-XSS-Protection should be enabled."&lt;/strong&gt; No - the header is deprecated; the auditor that flags its absence is out of date. Modern browsers removed the XSS auditor it controlled, and in old browsers it introduced vulnerabilities. Set it to 0 or omit it entirely; CSP is its replacement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"CSP broke the site once, so we removed it."&lt;/strong&gt; The failure was skipping Report-Only, not CSP itself. Re-run the staged process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Grade A means secure."&lt;/strong&gt; Headers are one layer. An A-grade site with an unpatched CMS is still one plugin CVE from compromise - which is why the header scan is one test among several in a full &lt;a href="https://dev.to/Onboarding"&gt;FortifyNet audit&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rollout checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Baseline scan saved; third-party origin inventory complete&lt;/li&gt;
&lt;li&gt;[ ] HSTS, nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy deployed&lt;/li&gt;
&lt;li&gt;[ ] Server/X-Powered-By version leakage removed&lt;/li&gt;
&lt;li&gt;[ ] CSP running in Report-Only with reporting endpoint&lt;/li&gt;
&lt;li&gt;[ ] Violations triaged; nonces used for scripts; no 'unsafe-inline' for script-src&lt;/li&gt;
&lt;li&gt;[ ] CSP enforced; frame-ancestors set; reporting kept on&lt;/li&gt;
&lt;li&gt;[ ] Headers set centrally (server/CDN), always/Header always used&lt;/li&gt;
&lt;li&gt;[ ] Re-scan shows grade A; continuous monitoring enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" rel="noopener noreferrer"&gt;MDN - HTTP response headers reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-secure-headers/" rel="noopener noreferrer"&gt;OWASP Secure Headers Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/CSP3/" rel="noopener noreferrer"&gt;W3C - Content Security Policy Level 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/articles/strict-csp" rel="noopener noreferrer"&gt;Google web.dev - Mitigate XSS with a strict CSP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6797" rel="noopener noreferrer"&gt;RFC 6797 - HTTP Strict Transport Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hstspreload.org" rel="noopener noreferrer"&gt;hstspreload.org - HSTS preload submission&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/guide-security-headers-hardening" rel="noopener noreferrer"&gt;fortifynet.com/blog/guide-security-headers-hardening&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deepguide</category>
      <category>headers</category>
      <category>csp</category>
      <category>hsts</category>
    </item>
    <item>
      <title>Website Incident Response Playbook: NIST-Based Plan for Small IT Teams</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Sat, 29 Aug 2026 04:45:29 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/website-incident-response-playbook-nist-based-plan-for-small-it-teams-52ch</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/website-incident-response-playbook-nist-based-plan-for-small-it-teams-52ch</guid>
      <description>&lt;p&gt;When a company website is defaced, starts serving malware, or quietly leaks its customer database, the difference between a bad day and an existential event is rarely technical skill - it is whether anyone wrote down, &lt;em&gt;before the incident&lt;/em&gt;, who decides what, in which order, with which accesses. This guide gives IT managers at small and mid-sized organizations a complete, practical incident response plan for website and web-application incidents, built on the structure of &lt;a href="https://csrc.nist.gov/pubs/sp/800/61/r3/final" rel="noopener noreferrer"&gt;NIST SP 800-61&lt;/a&gt; and adapted to teams of one to five people rather than a 24/7 SOC.&lt;/p&gt;

&lt;p&gt;Work through it once in calm conditions (roughly one working day), and you will have a printed playbook, a contact card, and tested backups - the three artifacts that consistently separate quick recoveries from week-long outages.&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%2Fimages.unsplash.com%2Fphoto-1504384308090-c894fdcc538d%3Fw%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1504384308090-c894fdcc538d%3Fw%3D1200%26q%3D80" alt="Emergency operations desk with monitors"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident lifecycle
&lt;/h2&gt;

&lt;p&gt;Every credible framework uses some version of the same loop. Print this; it is the spine of the whole plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   +--------------+     +-----------------+     +--------------+
   | 1. PREPARE   | --&amp;gt; | 2. DETECT &amp;amp;     | --&amp;gt; | 3. CONTAIN   |
   |  plan, roles |     |    ANALYZE      |     |  stop spread |
   |  backups     |     |  triage, scope  |     |  preserve    |
   +--------------+     +-----------------+     |  evidence    |
          ^                                     +------+-------+
          |                                            |
   +------+-------+     +-----------------+     +------v-------+
   | 6. LEARN     | &amp;lt;-- | 5. RECOVER      | &amp;lt;-- | 4. ERADICATE |
   |  post-mortem |     |  restore,       |     |  remove      |
   |  fix causes  |     |  monitor,       |     |  cause,      |
   +--------------+     |  notify         |     |  patch       |
                        +-----------------+     +--------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 1 - Preparation (do this now, not during the fire)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Define severity levels and who decides
&lt;/h3&gt;

&lt;p&gt;Ambiguity about "how bad is this" wastes the first critical hour. Adopt three levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Who is woken up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEV-1&lt;/td&gt;
&lt;td&gt;Confirmed compromise or data breach&lt;/td&gt;
&lt;td&gt;defacement, malware served, database exfiltrated, ransomware&lt;/td&gt;
&lt;td&gt;IT lead + CEO immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEV-2&lt;/td&gt;
&lt;td&gt;Strong indicators, no confirmed damage&lt;/td&gt;
&lt;td&gt;credential stuffing spike, vulnerable component actively probed, admin login from unknown country&lt;/td&gt;
&lt;td&gt;IT lead, business hours+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEV-3&lt;/td&gt;
&lt;td&gt;Suspicious but likely benign&lt;/td&gt;
&lt;td&gt;single phishing report, scanner noise, one-off WAF block&lt;/td&gt;
&lt;td&gt;logged, reviewed weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Name an &lt;strong&gt;Incident Commander&lt;/strong&gt; (IC) - the person who coordinates, decides and communicates - and one deputy for vacations. In a small company the IC is usually the IT manager; the point is that it is &lt;em&gt;one named person&lt;/em&gt;, not "the team".&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Build the contact card
&lt;/h3&gt;

&lt;p&gt;During an incident you will not have time to search for logins. Prepare a one-page card (printed and in a shared password manager) with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hosting provider: support phone/portal, account number, who holds admin access&lt;/li&gt;
&lt;li&gt;Domain registrar and DNS provider: same&lt;/li&gt;
&lt;li&gt;CMS/platform admin accounts and where their credentials live&lt;/li&gt;
&lt;li&gt;CDN/WAF provider (Cloudflare etc.) access&lt;/li&gt;
&lt;li&gt;Cyber-insurance policy number and 24h hotline, if insured&lt;/li&gt;
&lt;li&gt;External IR/forensics firm you would call for a SEV-1 (pick one &lt;em&gt;now&lt;/em&gt;; retainers are cheap compared to emergency onboarding)&lt;/li&gt;
&lt;li&gt;Data protection authority contact (for Sweden: &lt;a href="https://www.imy.se" rel="noopener noreferrer"&gt;IMY&lt;/a&gt;; EU-wide list at the &lt;a href="https://www.edpb.europa.eu/about-edpb/about-edpb/members_en" rel="noopener noreferrer"&gt;EDPB&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Internal: CEO, communications owner, legal counsel&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1.3 Backups you have actually restored
&lt;/h3&gt;

&lt;p&gt;A backup that has never been test-restored is a hope, not a control. Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3-2-1 rule:&lt;/strong&gt; three copies, two media, one off-site - and the off-site copy must be &lt;em&gt;offline or immutable&lt;/em&gt; (object-lock / snapshot), because modern ransomware deletes reachable backups first.&lt;/li&gt;
&lt;li&gt;Backup scope: web root, database, server configuration (nginx/Apache, cron), TLS certificates and - often forgotten - the &lt;strong&gt;environment variables/secrets&lt;/strong&gt; the app needs to boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly restore drill:&lt;/strong&gt; restore to a scratch VM, boot the site, log in. Time it. That number is your realistic Recovery Time Objective, not the one in the policy document.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1.4 Detection you will actually notice
&lt;/h3&gt;

&lt;p&gt;Small teams do not watch dashboards; alerts must come to them. Minimum set: uptime monitoring with alerting, file-integrity or defacement detection on the web root, admin-login notifications from the CMS, and external monitoring of your domain's security posture and certificate/DNS changes - FortifyNet's &lt;a href="https://dev.to/Onboarding"&gt;continuous monitoring&lt;/a&gt; covers the external half and emails you on change, which is precisely how defacements and expired certificates are usually caught late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2 - Detection &amp;amp; analysis (the first hour)
&lt;/h2&gt;

&lt;p&gt;When an alert or report arrives, the IC runs triage - in writing, in a dedicated channel (create #incident-YYYYMMDD), because the timeline you write now is the post-mortem and possibly the legal record later.&lt;/p&gt;

&lt;p&gt;Triage questions, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is it real?&lt;/strong&gt; Reproduce the symptom from an outside network (mobile hotspot) - CDN caches and split DNS often fool internal checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the scope?&lt;/strong&gt; One page or entire site? Web server only, or database too? Are customer credentials or personal data plausibly involved? (This answer starts the GDPR clock - see Phase 5.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it ongoing?&lt;/strong&gt; Live traffic from the attacker, or the residue of a past event?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assign severity&lt;/strong&gt; from the 1.1 table and notify accordingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Evidence rule from the first minute: &lt;strong&gt;copy logs off the affected server immediately&lt;/strong&gt; (access logs, error logs, auth logs, CMS audit trail). Attackers commonly delete logs; hosting providers rotate them within days. Do not power off a server you may need forensics from - isolate it instead (see below); powering off destroys memory-resident evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3 - Containment
&lt;/h2&gt;

&lt;p&gt;Containment stops the bleeding without destroying evidence. Choose the &lt;em&gt;least destructive action that stops ongoing harm&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site serving malware/defacement:&lt;/strong&gt; put the site into maintenance mode at the CDN/load-balancer level, or geo-block/challenge all traffic. Users see a clean holding page; the compromised origin stays intact for analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compromised admin account:&lt;/strong&gt; disable that account and rotate &lt;em&gt;all&lt;/em&gt; admin credentials and API keys - assume the attacker harvested the credential store. Enforce MFA in the same change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-level compromise:&lt;/strong&gt; isolate at the network level (security group/firewall: block all inbound except your management IP). Take a disk snapshot &lt;em&gt;before&lt;/em&gt; changing anything if your host supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database exfiltration:&lt;/strong&gt; rotate database credentials, revoke external access, and preserve query logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two containment mistakes to brief the team on in advance: (1) restoring from backup &lt;em&gt;before&lt;/em&gt; understanding the entry point - you restore the vulnerability with the data and get re-compromised within hours; (2) communicating on potentially compromised infrastructure - if the mail server may be affected, coordinate on phones/Signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4 - Eradication
&lt;/h2&gt;

&lt;p&gt;Find and remove the root cause, not just the symptom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Establish the &lt;strong&gt;entry point&lt;/strong&gt; from the evidence: unpatched CMS/plugin (the majority of small-business web incidents), stolen credentials, exposed admin panel, vulnerable upload form, or a hosting-level breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild rather than clean.&lt;/strong&gt; For any server-level compromise, deploy a fresh server from a known-good image and migrate verified data into it. Cleaning a rooted box is unreliable; webshells hide in innocuous files and cron entries.&lt;/li&gt;
&lt;li&gt;Patch the entry point everywhere it exists (staging too), update all components, and re-scan.&lt;/li&gt;
&lt;li&gt;Rotate every secret the compromised system could read: DB passwords, API keys, OAuth secrets, TLS private keys if key material could have been read.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Phase 5 - Recovery and notification
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Technical recovery
&lt;/h3&gt;

&lt;p&gt;Restore service progressively: bring the rebuilt site up behind the CDN, run an external scan to confirm nothing malicious is served, watch error rates and logs at elevated attention for 72 hours, and keep the tightened firewall rules. If the site was flagged by Google Safe Browsing, request review via Search Console - delisting typically takes hours to a couple of days after cleanup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Legal notification - the 72-hour rule
&lt;/h3&gt;

&lt;p&gt;Under &lt;strong&gt;GDPR Article 33&lt;/strong&gt;, a personal-data breach must be reported to the supervisory authority &lt;strong&gt;within 72 hours of becoming aware of it&lt;/strong&gt;, unless the breach is unlikely to risk individuals' rights. Article 34 additionally requires notifying the affected individuals when the risk is high (e.g. leaked passwords or payment data). Practical points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The clock starts at &lt;em&gt;awareness&lt;/em&gt;, not at full understanding. A partial report ("we have confirmed unauthorized access, scope under investigation") within 72 hours is compliant; silence is not.&lt;/li&gt;
&lt;li&gt;Document the assessment even when you decide &lt;em&gt;not&lt;/em&gt; to notify - the reasoning is itself required.&lt;/li&gt;
&lt;li&gt;Organizations under &lt;strong&gt;NIS2&lt;/strong&gt; obligations have a faster track: early warning to the CSIRT within 24 hours, incident notification within 72 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Communication
&lt;/h3&gt;

&lt;p&gt;One voice: the IC or the designated communications owner. Internally, short factual updates at a fixed cadence (even "no change"). Externally, honesty beats spin - state what happened, what data was involved, what users should do (password reset), and what you have changed. Companies are rarely punished by customers for having an incident; they are punished for hiding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 6 - Post-incident review
&lt;/h2&gt;

&lt;p&gt;Within two weeks, run a &lt;strong&gt;blameless post-mortem&lt;/strong&gt; (one hour, IC facilitates): timeline, root cause, what worked, what was slow, and 3–5 concrete actions with owners and dates - e.g. "enable auto-updates for plugins", "move backups to immutable storage", "add file-integrity monitoring". Feed the actions into the normal work backlog; an unassigned lesson is a lesson not learned.&lt;/p&gt;

&lt;p&gt;Finally, schedule a &lt;strong&gt;tabletop exercise&lt;/strong&gt; twice a year: pick a scenario (defacement discovered by a customer on Friday 17:30 is a classic), walk the team through this playbook verbally, and note every point where someone says "hmm, I'm not sure who…". Those pauses are the plan's bugs - fix them on paper before an attacker finds them in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example - the Friday-evening defacement
&lt;/h2&gt;

&lt;p&gt;Theory sticks better with a timeline. Here is how the playbook runs for the most common SMB scenario: a customer emails at 17:42 on a Friday saying your homepage "looks strange".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;17:45 - Detect.&lt;/strong&gt; The on-call IT manager (IC by default) opens the site from a phone on mobile data: the homepage shows an attacker's banner. Screenshot taken. #incident-20260828 channel created; SEV-1 declared (confirmed compromise); CEO texted per the severity table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17:55 - Evidence.&lt;/strong&gt; Before touching anything: access and error logs from the past 14 days copied off the server to local storage; CMS audit log exported; disk snapshot triggered at the hosting provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18:05 - Contain.&lt;/strong&gt; Site switched to maintenance mode at the CDN. Customers now see a neutral "back shortly" page. All CMS admin passwords rotated from a known-clean laptop; MFA enforced; the one unfamiliar admin account found in the user list is disabled, not deleted (it is evidence).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18:40 - Analyze.&lt;/strong&gt; The CMS audit log shows the rogue admin was created Tuesday 03:14 via a vulnerable gallery plugin (version three releases behind). Database tables for customers appear untouched per query logs - noted, with reasoning, in the channel for the GDPR assessment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19:30 - Eradicate.&lt;/strong&gt; Fresh server deployed from the host's stock image; latest clean backup (Monday) restored to it; CMS core and all plugins updated; the gallery plugin deleted outright; secrets rotated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21:10 - Recover.&lt;/strong&gt; DNS/CDN pointed at the new origin. External scan confirms clean content and headers. Maintenance mode lifted. Elevated log-watching agreed for the weekend, split between IC and deputy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monday - Notify &amp;amp; learn.&lt;/strong&gt; GDPR assessment concluded: no personal data accessed, documented, no Article 33 report required. Post-mortem Wednesday produces three actions: auto-updates on, plugin inventory quarterly, file-integrity monitoring installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total customer-visible downtime: about three hours, on a Friday night, with two people. That outcome is bought almost entirely in Phase 1 - the snapshot capability, the CDN maintenance switch, the tested backup and the severity table were all decided &lt;em&gt;before&lt;/em&gt; 17:42.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with external parties
&lt;/h2&gt;

&lt;p&gt;Three relationships determine how much help you get under pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosting provider.&lt;/strong&gt; Know &lt;em&gt;in advance&lt;/em&gt; what they can do in an emergency: snapshots on demand? network isolation? log retention period? Their emergency response is often faster through the paid support tier - decide before the incident whether that tier is worth it (it usually is).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forensics / IR firm.&lt;/strong&gt; For any SEV-1 involving suspected data theft, ransomware or legal exposure, bring professionals in early - evidence handling mistakes in the first hours are irreversible. A named firm on a light retainer answers in hours; a cold call during a national ransomware wave may answer in weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cyber insurance.&lt;/strong&gt; Read the policy &lt;em&gt;now&lt;/em&gt;: most require notification within a fixed window and some require using the insurer's approved IR panel - engaging your own firm first can void coverage. Put both facts on the contact card.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If extortion enters the picture
&lt;/h2&gt;

&lt;p&gt;If the incident comes with a ransom demand - for decryption, or for not publishing stolen data - slow down before anyone replies. Do not communicate with the attacker from personal or corporate accounts; involve legal counsel and, for significant demands, the police (in Sweden via polisen.se; many EU countries have national cybercrime reporting routes) before any contact. Paying is legal in most jurisdictions but funds the next attack, offers no guarantee, and may be sanctioned if the group is on a sanctions list - a check your legal counsel or insurer must make. Whatever is decided, decide it once, at management level, documented; extortion pressure is designed to force fast individual decisions, and the playbook's job is to prevent exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the capability
&lt;/h2&gt;

&lt;p&gt;Two numbers, reviewed twice a year, tell you if the capability is improving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MTTD (mean time to detect):&lt;/strong&gt; time from compromise to awareness. The defacement example above had an MTTD of three days - the attacker got in Tuesday, you learned Friday. File-integrity monitoring would have cut it to minutes; that is the argument for the post-mortem action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MTTR (mean time to recover):&lt;/strong&gt; time from detection to restored service. Driven almost entirely by preparation quality: tested backups, pre-agreed containment switches, and a named decision-maker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tabletop exercises produce these numbers safely: time the walkthrough, note where the team stalls, and treat every "I'd have to look that up" as a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customer notification template
&lt;/h2&gt;

&lt;p&gt;Have a draft ready so legal review is the only delay. Keep it factual, short and free of adjectives:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`&lt;br&gt;
Subject: Security incident affecting [service] - what happened and what we're doing&lt;/p&gt;

&lt;p&gt;On [date] we detected unauthorized access to [system]. We took the&lt;br&gt;
affected service offline within [X minutes/hours], removed the cause,&lt;br&gt;
and restored service on [date/time].&lt;/p&gt;

&lt;p&gt;What was affected: [specific data categories, or "no customer data,&lt;br&gt;
based on our log analysis"].&lt;/p&gt;

&lt;p&gt;What you should do: [reset your password / no action is required].&lt;/p&gt;

&lt;p&gt;We have reported the incident to [authority] and engaged [external&lt;br&gt;
experts] to verify our findings. We will update this page as the&lt;br&gt;
investigation concludes: [status page URL].&lt;/p&gt;

&lt;p&gt;Contact: &lt;a href="mailto:security@yourcompany.com"&gt;security@yourcompany.com&lt;/a&gt;&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-page checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Severity table adopted; Incident Commander + deputy named&lt;/li&gt;
&lt;li&gt;[ ] Contact card printed and stored in the password manager&lt;/li&gt;
&lt;li&gt;[ ] 3-2-1 backups with an offline/immutable copy; restore drill done this quarter&lt;/li&gt;
&lt;li&gt;[ ] Uptime, defacement and external posture monitoring alerting to phones&lt;/li&gt;
&lt;li&gt;[ ] Evidence rule known: copy logs first, isolate - don't power off&lt;/li&gt;
&lt;li&gt;[ ] Containment options pre-agreed with hosting/CDN provider&lt;/li&gt;
&lt;li&gt;[ ] Rebuild-not-clean policy for server compromise&lt;/li&gt;
&lt;li&gt;[ ] GDPR 72h / NIS2 24h notification duties mapped to your organization&lt;/li&gt;
&lt;li&gt;[ ] Blameless post-mortem template ready; tabletop exercise scheduled&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://csrc.nist.gov/pubs/sp/800/61/r3/final" rel="noopener noreferrer"&gt;NIST SP 800-61 - Incident Response Recommendations and Considerations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.enisa.europa.eu/topics/incident-response" rel="noopener noreferrer"&gt;ENISA - Incident response and cyber crisis material&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gdpr-info.eu/art-33-gdpr/" rel="noopener noreferrer"&gt;GDPR Article 33 - Notification of a personal data breach&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gdpr-info.eu/art-34-gdpr/" rel="noopener noreferrer"&gt;GDPR Article 34 - Communication to the data subject&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eur-lex.europa.eu/eli/dir/2022/2555/oj" rel="noopener noreferrer"&gt;NIS2 Directive (EU) 2022/2555&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.imy.se/en/organisations/data-protection/report-a-personal-databreach/" rel="noopener noreferrer"&gt;IMY - Report a personal data breach (Sweden)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.google.com/webmasters/answer/168328" rel="noopener noreferrer"&gt;Google Search Console - Request a security review&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/guide-website-incident-response" rel="noopener noreferrer"&gt;fortifynet.com/blog/guide-website-incident-response&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deepguide</category>
      <category>incidentresponse</category>
      <category>gdpr</category>
      <category>nis2</category>
    </item>
    <item>
      <title>DMARC Monitoring: How to Read Your Reports | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Thu, 27 Aug 2026 04:45:30 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/dmarc-monitoring-how-to-read-your-reports-fortifynet-4jm</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/dmarc-monitoring-how-to-read-your-reports-fortifynet-4jm</guid>
      <description>&lt;p&gt;DMARC monitoring means collecting and reading the aggregate reports that mail receivers send back to your domain every day, so you can see exactly who is sending email as you before you switch your policy to quarantine or reject. You turn it on by adding a &lt;code&gt;rua=mailto:&lt;/code&gt; tag to your DMARC record. Within 24 to 48 hours, providers such as Google, Microsoft 365 and Yahoo start mailing you compressed XML files summarising every message that claimed your domain. Those files are the only evidence you get, and without them, moving to enforcement is guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DMARC monitoring actually is
&lt;/h2&gt;

&lt;p&gt;DMARC (Domain-based Message Authentication, Reporting and Conformance) does two jobs. The policy half tells receivers what to do with mail that fails authentication. The reporting half tells you what receivers are seeing. Monitoring is that second half, and it is the part most domain owners skip.&lt;/p&gt;

&lt;p&gt;The mechanism is straightforward. When a receiving provider evaluates mail claiming your From domain, it counts the results. At the end of each UTC day it packages those counts into an XML file and emails it to the address in your &lt;code&gt;rua=&lt;/code&gt; tag. &lt;a href="https://www.rfc-editor.org/info/rfc9990/" rel="noopener noreferrer"&gt;RFC 9990&lt;/a&gt; section 3.1.1.4 specifies that the reporting period covers a single UTC day beginning at 0000 UTC, which is why your first report arrives 24 to 48 hours after you publish the tag rather than immediately.&lt;/p&gt;

&lt;p&gt;Reports are gzip compressed and small. Measured per-provider file sizes ran between 3.6 KB and 4.8 KB (Google 3,962 bytes, Yahoo 4,626, Fastmail 4,839) in the dataset published by Ashiq et al. at USENIX Security 2023. Storage is never the constraint. Volume is: a busy domain can receive hundreds of files a day, which is why almost nobody reads them by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than half of DMARC domains fly blind
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D760%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522p%253Dnone%2520%2528monitoring%2520only%2529%2522%252C%2522p%253Dquarantine%2522%252C%2522p%253Dreject%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522%2525%2520of%2520DMARC%2520domains%2522%252C%2522data%2522%253A%255B57.9%252C22.4%252C19.6%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%2523059669%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522DMARC%2520policy%2520distribution%252C%25201%252C670%252C975%2520domains%2520%2528Feb%25202026%2529%2522%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522%2525%2520of%2520DMARC%2520domains%2522%257D%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fw%3D760%26h%3D420%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522p%253Dnone%2520%2528monitoring%2520only%2529%2522%252C%2522p%253Dquarantine%2522%252C%2522p%253Dreject%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522%2525%2520of%2520DMARC%2520domains%2522%252C%2522data%2522%253A%255B57.9%252C22.4%252C19.6%255D%252C%2522backgroundColor%2522%253A%255B%2522%2523dc2626%2522%252C%2522%2523f59e0b%2522%252C%2522%2523059669%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522DMARC%2520policy%2520distribution%252C%25201%252C670%252C975%2520domains%2520%2528Feb%25202026%2529%2522%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522%2525%2520of%2520DMARC%2520domains%2522%257D%257D%257D%257D%257D" alt="Bar chart of DMARC policy distribution across 1,670,975 domains in February 2026: 57.9 percent at p=none, 22.4 percent at quarantine, 19.6 percent at reject"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DMARC policy distribution across the 1,670,975 domains that publish a record. Source: DMARCguard, State of Email Authentication 2026, scan of 5,499,028 Tranco domains, 27 February 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That same scan found only 894,057 of those 1,670,975 domains (53.5%) publish a &lt;code&gt;rua=&lt;/code&gt; tag. The other 776,918 have a policy and no telemetry at all.&lt;/p&gt;

&lt;p&gt;The consequence shows up in the policy split above. 57.9% of DMARC domains, 967,474 of them, sit at &lt;code&gt;p=none&lt;/code&gt;. Monitoring-only mode is meant to be a staging area, not a destination, and the reason so many domains never leave it is simple: without reports there is nothing to base the decision on.&lt;/p&gt;

&lt;p&gt;The stakes are not theoretical. The FBI's Internet Crime Complaint Center recorded $2.77 billion in Business Email Compromise losses across 21,442 complaints in its 2024 annual report. On the other side of the ledger, after Google and Yahoo enforced bulk-sender authentication requirements in February 2024, Gmail reported 265 billion fewer unauthenticated messages that year (Neil Kumaran, Group Product Manager, Gmail Security and Trust, October 2024).&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning reporting on
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;rua=&lt;/code&gt; tag to the TXT record at &lt;code&gt;_dmarc.yourdomain.com&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; fo=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check what is currently published with a DNS lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;dig +short TXT _dmarc.yourdomain.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules worth knowing before you edit DNS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One record only.&lt;/strong&gt; RFC 9989 requires exactly one DMARC TXT record per domain. Publish two and receivers apply no policy at all, silently disabling DMARC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;rua=&lt;/code&gt; means no reports.&lt;/strong&gt; Receivers must not generate aggregate feedback without the tag. There is no opt-in anywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage is partial by design.&lt;/strong&gt; Microsoft's own documentation, updated 14 May 2026, states that not all receivers send reports and that coverage is typically 70 to 90 percent of total mail volume. Apple iCloud and Proton are not documented as report senders at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sending reports to a different domain
&lt;/h3&gt;

&lt;p&gt;If your &lt;code&gt;rua=&lt;/code&gt; address sits on a different organisational domain than the policy, that receiving domain has to authorise it. RFC 9990 section 4 requires a TXT record at &lt;code&gt;&amp;lt;your-domain&amp;gt;._report._dmarc.&amp;lt;reporting-domain&amp;gt;&lt;/code&gt; containing &lt;code&gt;v=DMARC1;&lt;/code&gt;. Without it, receivers silently stop sending. This is the single most common reason a correctly written &lt;code&gt;rua=&lt;/code&gt; tag produces no reports at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregate reports versus failure reports
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Aggregate reports (RUA)&lt;/th&gt;
&lt;th&gt;Failure reports (RUF)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Specification&lt;/td&gt;
&lt;td&gt;RFC 9990&lt;/td&gt;
&lt;td&gt;RFC 9991&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadence&lt;/td&gt;
&lt;td&gt;Once per UTC day, per provider&lt;/td&gt;
&lt;td&gt;Near real time, per message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content&lt;/td&gt;
&lt;td&gt;Counts per source IP with SPF and DKIM alignment plus the applied disposition&lt;/td&gt;
&lt;td&gt;Details of one failing message, potentially including headers and body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;Aggregated counts, no message content&lt;/td&gt;
&lt;td&gt;RFC 9991 states reports may contain personally identifiable information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider support&lt;/td&gt;
&lt;td&gt;Broad: Google, Microsoft 365, Yahoo, Mail.ru, Fastmail, Zoho&lt;/td&gt;
&lt;td&gt;Rare. Microsoft 365 does not send them even with a valid &lt;code&gt;ruf=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best used for&lt;/td&gt;
&lt;td&gt;Building the sender inventory and deciding when to enforce&lt;/td&gt;
&lt;td&gt;Diagnosing one specific failure you can reproduce&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with RUA. Almost everything you need to reach &lt;code&gt;p=reject&lt;/code&gt; safely is in the aggregate data, and it carries none of the privacy baggage of failure reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading a report: the four outcomes that matter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" alt="Analyst reviewing email authentication data on a laptop screen alongside printed charts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;&amp;lt;record&amp;gt;&lt;/code&gt; block in the XML pairs a source IP with a message count, the raw SPF and DKIM results, and a &lt;code&gt;policy_evaluated&lt;/code&gt; verdict. What matters is alignment, not raw authentication. SPF or DKIM has to pass &lt;strong&gt;and&lt;/strong&gt; match your From domain for DMARC to pass.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SPF aligned&lt;/th&gt;
&lt;th&gt;DKIM aligned&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Healthy. Your own infrastructure, correctly configured&lt;/td&gt;
&lt;td&gt;Nothing. This is the target state for every legitimate sender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;The sender is authorised but is not signing, or is signing with its own domain&lt;/td&gt;
&lt;td&gt;Enable DKIM at that service and align the &lt;code&gt;d=&lt;/code&gt; domain to yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Almost always forwarding or a mailing list. DMARC still passes&lt;/td&gt;
&lt;td&gt;Safe. Do not add the forwarder to your SPF record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Either a legitimate sender you forgot about, or spoofing&lt;/td&gt;
&lt;td&gt;Identify the IP. Authorise it, or leave it to be blocked at enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row is the one people get wrong most often. Forwarding breaks SPF by design, because the envelope sender changes at the forwarding hop. That is precisely why DKIM matters as a fallback, and why the 43.0% of DMARC domains that rely on SPF alignment alone (DMARCguard, February 2026) are more fragile than they look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in 2026: RFC 9989, 9990 and 9991
&lt;/h2&gt;

&lt;p&gt;In May 2026 the IETF published RFC 9989, RFC 9990 and RFC 9991. Together they obsolete RFC 7489 and move DMARC onto the Standards Track as a Proposed Standard for the first time since 2015, with reporting split into its own documents.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;RFC 7489 (2015)&lt;/th&gt;
&lt;th&gt;RFC 9989 / 9990 (2026)&lt;/th&gt;
&lt;th&gt;What you should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pct&lt;/code&gt; tag&lt;/td&gt;
&lt;td&gt;Supported, for partial rollout&lt;/td&gt;
&lt;td&gt;Removed&lt;/td&gt;
&lt;td&gt;Stop relying on it. Use the new &lt;code&gt;t=y&lt;/code&gt; testing mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rf&lt;/code&gt; and &lt;code&gt;ri&lt;/code&gt; tags&lt;/td&gt;
&lt;td&gt;Defined&lt;/td&gt;
&lt;td&gt;Removed&lt;/td&gt;
&lt;td&gt;Delete them at your next DNS edit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organisational domain discovery&lt;/td&gt;
&lt;td&gt;Public Suffix List&lt;/td&gt;
&lt;td&gt;DNS tree walk&lt;/td&gt;
&lt;td&gt;Nothing. This is a receiver-side change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report XML namespace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://dmarc.org/dmarc-xml/0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;urn:ietf:params:xml:ns:dmarc-2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any parser you run must accept both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM &lt;code&gt;selector&lt;/code&gt; in reports&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Fix generators or parsers that drop the field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;disposition&lt;/code&gt; values&lt;/td&gt;
&lt;td&gt;none, quarantine, reject&lt;/td&gt;
&lt;td&gt;Adds &lt;code&gt;pass&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Handle the new value instead of rejecting the file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown elements&lt;/td&gt;
&lt;td&gt;Undefined&lt;/td&gt;
&lt;td&gt;Ignore and continue (RFC 9990 section 5)&lt;/td&gt;
&lt;td&gt;Never reject a report you do not fully understand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your existing record keeps working. The &lt;code&gt;v=&lt;/code&gt;, &lt;code&gt;p=&lt;/code&gt;, &lt;code&gt;sp=&lt;/code&gt;, &lt;code&gt;rua=&lt;/code&gt;, &lt;code&gt;ruf=&lt;/code&gt;, &lt;code&gt;adkim=&lt;/code&gt;, &lt;code&gt;aspf=&lt;/code&gt; and &lt;code&gt;fo=&lt;/code&gt; tags all retain their meaning. In practice every major provider still emits legacy-shaped XML today, so expect a mixed window lasting years and parse tolerantly rather than strictly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe four-stage rollout to p=reject
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 1 to 2.&lt;/strong&gt; Publish &lt;code&gt;p=none&lt;/code&gt; with &lt;code&gt;rua=&lt;/code&gt;. Change nothing else. Collect reports and build a list of every IP and service sending as you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 3 to 6.&lt;/strong&gt; Fix alignment. Add missing senders to SPF, enable DKIM everywhere, and confirm each third-party service signs with your domain rather than its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 7 to 8.&lt;/strong&gt; Move to &lt;code&gt;p=quarantine&lt;/code&gt;, optionally starting on a subdomain via &lt;code&gt;sp=&lt;/code&gt;. Watch the reports for a full week for any legitimate sender you missed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 9 onward.&lt;/strong&gt; Move to &lt;code&gt;p=reject&lt;/code&gt;, and keep &lt;code&gt;rua=&lt;/code&gt; in place permanently. Monitoring is not scaffolding you take down after the build.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rushing stage one is the classic failure. A quarterly invoice run or an annual campaign will not appear in two weeks of data, and it will bounce on the day you enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How often are DMARC reports sent?&lt;/strong&gt;&lt;br&gt;
Once per UTC day per reporting provider, covering a window that begins at 0000 UTC (RFC 9990 section 3.1.1.4). Your first report typically arrives 24 to 48 hours after you publish &lt;code&gt;rua=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why am I not receiving any reports?&lt;/strong&gt;&lt;br&gt;
Most often the &lt;code&gt;rua=&lt;/code&gt; address is on a different domain and the required authorisation record at &lt;code&gt;&amp;lt;your-domain&amp;gt;._report._dmarc.&amp;lt;reporting-domain&amp;gt;&lt;/code&gt; is missing. Other causes: a syntax error in the DMARC record, two DMARC records published at once, or simply no mail claiming your domain during the window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the DMARC report in my inbox legitimate?&lt;/strong&gt;&lt;br&gt;
Almost certainly. Genuine reports use the subject pattern &lt;code&gt;Report Domain: &amp;lt;domain&amp;gt; Submitter: &amp;lt;reporter&amp;gt; Report-ID: &amp;lt;id&amp;gt;&lt;/code&gt; and attach compressed XML with a bang-separated filename. Real reports contain no links to click, no payment demands and no credential requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I still need monitoring after reaching p=reject?&lt;/strong&gt;&lt;br&gt;
Yes. New services get added, DNS gets edited, DKIM keys get rotated. Reports are how you find out something broke before your customers do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I just delete the reports?&lt;/strong&gt;&lt;br&gt;
You can remove &lt;code&gt;rua=&lt;/code&gt; to stop them, but that trades a cluttered inbox for zero visibility into who is spoofing you. Pointing &lt;code&gt;rua=&lt;/code&gt; at an analyser is the better answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your setup in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Before you touch DNS, it helps to know exactly where you stand. FortifyNet's free scan reads your DMARC, SPF and DKIM records and tells you whether reporting is actually configured, whether your policy is at enforcement, and what is missing. It takes about a minute and needs no signup. &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;Run a free scan at FortifyNet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related guides:&lt;/strong&gt; &lt;a href="https://fortifynet.com/blog/how-to-set-up-dmarc" rel="noopener noreferrer"&gt;How to Set Up DMARC, Step by Step&lt;/a&gt; · &lt;a href="https://fortifynet.com/blog/what-is-dkim" rel="noopener noreferrer"&gt;What Is DKIM?&lt;/a&gt; · &lt;a href="https://fortifynet.com/blog/spf-record-checker" rel="noopener noreferrer"&gt;SPF Record Checker&lt;/a&gt; · &lt;a href="https://fortifynet.com/blog/dns-security-configuration" rel="noopener noreferrer"&gt;DNS Security Configuration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/dmarc-monitoring" rel="noopener noreferrer"&gt;fortifynet.com/blog/dmarc-monitoring&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dmarc</category>
      <category>emailauthentication</category>
      <category>dmarcreports</category>
      <category>rua</category>
    </item>
    <item>
      <title>SPF Record Syntax: Mechanisms &amp; Limits | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Sun, 23 Aug 2026 04:45:29 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/spf-record-syntax-mechanisms-limits-fortifynet-54n3</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/spf-record-syntax-mechanisms-limits-fortifynet-54n3</guid>
      <description>&lt;p&gt;An SPF record is a single DNS TXT record, published at your domain's apex, that begins with &lt;code&gt;v=spf1&lt;/code&gt;, lists the mail servers authorized to send on your behalf, and ends with an &lt;code&gt;all&lt;/code&gt; mechanism telling receivers what to do with everything else. &lt;code&gt;v=spf1 include:_spf.google.com ~all&lt;/code&gt; is a complete, valid record. Every other piece of SPF record syntax is a variation on those three parts - plus a set of hard limits that silently break records which otherwise look correct.&lt;/p&gt;

&lt;p&gt;Those limits matter more than most people expect. In a scan of 5,499,028 domains from the Tranco list carried out on 27 February 2026, DMARCguard found that 56.0% publish an SPF record - ahead of DMARC (30.4%) and DKIM (22.7%) - but that 4.8% of those SPF records, 148,655 domains, exceed the 10-lookup ceiling set by &lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;RFC 7208&lt;/a&gt; and therefore fail evaluation outright.&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%2Fimages.unsplash.com%2Fphoto-1554224155-6726b3ff858f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1400%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1554224155-6726b3ff858f%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1400%26q%3D80" alt="A terminal window showing DNS TXT record output, illustrating how an SPF record is published and read" width="1400" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three parts of every SPF record
&lt;/h2&gt;

&lt;p&gt;Every valid record breaks down the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1  ip4:203.0.113.10  include:_spf.google.com  -all
  |            |                    |               |
version    mechanism            mechanism      catch-all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The version tag.&lt;/strong&gt; &lt;code&gt;v=spf1&lt;/code&gt; must be the first term and must match exactly. Anything else means the record is not an SPF record at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mechanisms.&lt;/strong&gt; Evaluated strictly left to right. The first mechanism that matches the connecting IP decides the result and evaluation stops immediately - order is not cosmetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The catch-all.&lt;/strong&gt; An &lt;code&gt;all&lt;/code&gt; mechanism as the final term. Anything published after &lt;code&gt;all&lt;/code&gt; is ignored.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One thing worth being precise about: SPF checks the envelope sender used in the SMTP &lt;code&gt;MAIL FROM&lt;/code&gt; command (the Return-Path) and the HELO identity. It does &lt;strong&gt;not&lt;/strong&gt; check the &lt;code&gt;From:&lt;/code&gt; header your recipient actually sees. Closing that gap is the job of DMARC, which is why the two are always deployed together. If you are still mapping out the wider setup, our &lt;a href="https://fortifynet.com/blog/dns-security-configuration" rel="noopener noreferrer"&gt;DNS security configuration guide&lt;/a&gt; covers how SPF, DKIM, DMARC and DNSSEC fit together.&lt;/p&gt;

&lt;p&gt;SPF once had a dedicated DNS record type (type 99). RFC 7208 §3.1 retired it in April 2014 - publish SPF as a TXT record only.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF mechanisms: the full reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Matches when the sending IP…&lt;/th&gt;
&lt;th&gt;DNS lookups&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;always matches&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Must be the last term&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip4:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;falls inside the given IPv4 address or CIDR range&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ip4:203.0.113.0/24&lt;/code&gt; - costs nothing against the lookup limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip6:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;falls inside the given IPv6 address or prefix&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip6:2001:db8::/32&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;matches an A or AAAA record of the domain&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Bare &lt;code&gt;a&lt;/code&gt; means the current domain; &lt;code&gt;a:mail.example.com&lt;/code&gt; names another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;matches an address record of one of the domain's MX hosts&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Each MX host must not resolve to more than 10 address records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;passes the included domain's own SPF evaluation&lt;/td&gt;
&lt;td&gt;1, plus whatever the included record itself spends&lt;/td&gt;
&lt;td&gt;The most-used mechanism and the usual cause of lookup blowouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the expanded domain name has any A record&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Used with macros for per-sender rules; rare in ordinary records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reverse DNS for the IP resolves back into the domain&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;RFC 7208 §5.5 states plainly: "This mechanism SHOULD NOT be published." Remove it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A subtlety that trips people up: &lt;code&gt;include:&lt;/code&gt; is not a textual paste. It runs a full, separate SPF evaluation against the included domain and matches only if that evaluation returns &lt;strong&gt;pass&lt;/strong&gt;. A &lt;code&gt;-all&lt;/code&gt; inside an included record does not reject your mail - it simply makes the &lt;code&gt;include:&lt;/code&gt; not match, and evaluation continues to your next mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qualifiers: four characters that decide the verdict
&lt;/h2&gt;

&lt;p&gt;Any mechanism can carry a qualifier prefix. RFC 7208 §4.6.2 defines exactly four, and the default is &lt;code&gt;+&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Qualifier&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;What it asks the receiver to do&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;+&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;td&gt;Treat the sender as authorized&lt;/td&gt;
&lt;td&gt;Implicit - &lt;code&gt;+mx&lt;/code&gt; and &lt;code&gt;mx&lt;/code&gt; are identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;softfail&lt;/td&gt;
&lt;td&gt;Accept, but mark as suspicious&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~all&lt;/code&gt; during rollout, or where forwarding is common&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;td&gt;Treat as unauthorized; reject or bin&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-all&lt;/code&gt; once every legitimate sender is listed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;neutral&lt;/td&gt;
&lt;td&gt;Make no statement either way&lt;/td&gt;
&lt;td&gt;Practically the same as publishing nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Never publish &lt;code&gt;+all&lt;/code&gt;. It authorizes the entire internet to send as your domain and is strictly worse than having no SPF record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modifiers: redirect and exp
&lt;/h2&gt;

&lt;p&gt;Modifiers are name/value pairs rather than mechanisms, and they may appear only once each.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;redirect=example.com&lt;/code&gt; replaces your record's result wholesale with the target domain's SPF result. It costs one DNS lookup and is ignored entirely if an &lt;code&gt;all&lt;/code&gt; mechanism is present, so the two should not appear together.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp=explain.example.com&lt;/code&gt; points at a TXT record supplying a human-readable explanation string returned on a fail. It costs no lookup at evaluation time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The limits that break otherwise-valid records
&lt;/h2&gt;

&lt;p&gt;This is where most real-world SPF failures live. All of the following come straight from RFC 7208 §3.4 and §4.6.4:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10 DNS-querying terms.&lt;/strong&gt; The &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt; and &lt;code&gt;exists&lt;/code&gt; mechanisms and the &lt;code&gt;redirect&lt;/code&gt; modifier each consume one. Exceeding 10 &lt;strong&gt;MUST&lt;/strong&gt; return &lt;code&gt;permerror&lt;/code&gt; - a hard failure, not a warning. &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;ip4&lt;/code&gt;, &lt;code&gt;ip6&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt; are free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 void lookups.&lt;/strong&gt; A query returning NXDOMAIN or an empty answer is a "void lookup". Implementations should cap these at two; exceeding the cap also produces &lt;code&gt;permerror&lt;/code&gt;. Stale &lt;code&gt;include:&lt;/code&gt; entries for services you no longer use are the usual culprit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 address records per &lt;code&gt;mx&lt;/code&gt; or &lt;code&gt;ptr&lt;/code&gt;.&lt;/strong&gt; Beyond the overall budget, each individual MX host is capped at 10 A/AAAA records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One record per domain.&lt;/strong&gt; If a lookup returns more than one &lt;code&gt;v=spf1&lt;/code&gt; record, the result is &lt;code&gt;permerror&lt;/code&gt;. Merge them into a single record; never publish two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;255 characters per string, 450 octets recommended overall.&lt;/strong&gt; A TXT record can hold multiple quoted strings, concatenated without spaces. RFC 7208 §3.4 recommends keeping the whole DNS answer under 450 octets so it fits in a UDP packet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 20-second evaluation budget.&lt;/strong&gt; Receivers should allow at least 20 seconds and return &lt;code&gt;temperror&lt;/code&gt; past that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where your ten lookups actually go
&lt;/h2&gt;

&lt;p&gt;Each third-party sender you add spends at least one lookup, and some spend more because their own records nest further includes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sender&lt;/th&gt;
&lt;th&gt;Typical &lt;code&gt;include:&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Lookups consumed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_spf.google.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft 365&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spf.protection.outlook.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailchimp&lt;/td&gt;
&lt;td&gt;&lt;code&gt;servers.mcsv.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sendgrid.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;&lt;code&gt;amazonses.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zendesk&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mail.zendesk.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salesforce&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exacttarget.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six or seven services and you are at the ceiling. The fixes, in order of preference: remove includes for services you have retired, move senders onto dedicated subdomains with their own SPF records, and only then consider flattening includes into literal &lt;code&gt;ip4:&lt;/code&gt; ranges - flattening removes lookups but means you must track your providers' IP changes yourself.&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%2Fquickchart.io%2Fchart%3Fw%3D720%26h%3D400%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522.de%2522%252C%2522.uk%2522%252C%2522.org%2522%252C%2522.com%2522%252C%2522.io%2522%252C%2522.net%2522%252C%2522.ru%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522%2525%2520of%2520domains%2520publishing%2520SPF%2522%252C%2522data%2522%253A%255B76.8%252C62.8%252C59.4%252C58.7%252C58.0%252C55.4%252C47.8%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522SPF%2520adoption%2520by%2520TLD%2520%2528February%25202026%2529%2522%252C%2522font%2522%253A%257B%2522size%2522%253A16%257D%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A100%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522%2525%2520of%2520domains%2520publishing%2520SPF%2522%257D%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fw%3D720%26h%3D400%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522.de%2522%252C%2522.uk%2522%252C%2522.org%2522%252C%2522.com%2522%252C%2522.io%2522%252C%2522.net%2522%252C%2522.ru%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522label%2522%253A%2522%2525%2520of%2520domains%2520publishing%2520SPF%2522%252C%2522data%2522%253A%255B76.8%252C62.8%252C59.4%252C58.7%252C58.0%252C55.4%252C47.8%255D%252C%2522backgroundColor%2522%253A%2522%25232563eb%2522%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522SPF%2520adoption%2520by%2520TLD%2520%2528February%25202026%2529%2522%252C%2522font%2522%253A%257B%2522size%2522%253A16%257D%257D%252C%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A100%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522%2525%2520of%2520domains%2520publishing%2520SPF%2522%257D%257D%257D%257D%257D" alt="SPF adoption by TLD, February 2026" width="1440" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Share of domains publishing an SPF record, by top-level domain. Source: DMARCguard, "State of Email Authentication 2026", scan of 5,499,028 Tranco domains, 27 February 2026.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Five syntax mistakes that cause PermError
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Two &lt;code&gt;v=spf1&lt;/code&gt; records on the same name.&lt;/strong&gt; Common after a migration. Merge, do not stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A qualifier on &lt;code&gt;all&lt;/code&gt; that contradicts intent&lt;/strong&gt; - &lt;code&gt;?all&lt;/code&gt; looks cautious but tells receivers nothing, so spoofed mail sails through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terms after &lt;code&gt;all&lt;/code&gt;.&lt;/strong&gt; Everything to the right of &lt;code&gt;all&lt;/code&gt; is dead text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leftover &lt;code&gt;ptr&lt;/code&gt;.&lt;/strong&gt; Deprecated since 2014, slow, and it burns a lookup for no benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Includes for retired vendors.&lt;/strong&gt; They cost a lookup each and can turn into void lookups when the vendor removes the record.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to check your own record
&lt;/h2&gt;

&lt;p&gt;Read your record straight from DNS with a single query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dig +short TXT example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, the equivalent is &lt;code&gt;nslookup -type=TXT example.com&lt;/code&gt;. Look for exactly one string starting with &lt;code&gt;v=spf1&lt;/code&gt;, then count the lookup-consuming terms by hand - and remember to count the ones hidden inside each &lt;code&gt;include:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since February 2024, Google and Yahoo have required bulk senders (roughly 5,000 messages a day or more) to authenticate with both SPF and DKIM and to publish a DMARC record of at least &lt;code&gt;p=none&lt;/code&gt;. A &lt;code&gt;permerror&lt;/code&gt; from a broken SPF record puts that compliance at risk, so it is worth verifying rather than assuming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run a free 60-second FortifyNet scan&lt;/strong&gt; and we will check your SPF record alongside DKIM, DMARC, your TLS configuration, security headers and dark-web exposure - no signup needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can a domain have two SPF records?&lt;/strong&gt;&lt;br&gt;
No. If a lookup returns more than one &lt;code&gt;v=spf1&lt;/code&gt; record, RFC 7208 §4.5 requires receivers to return &lt;code&gt;permerror&lt;/code&gt;, which fails SPF entirely. Combine the mechanisms into one record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use &lt;code&gt;-all&lt;/code&gt; or &lt;code&gt;~all&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Start with &lt;code&gt;~all&lt;/code&gt; while you confirm every legitimate sender is listed, then move to &lt;code&gt;-all&lt;/code&gt;. &lt;code&gt;-all&lt;/code&gt; is the stronger anti-spoofing signal, but only once your inventory of senders is genuinely complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do &lt;code&gt;ip4:&lt;/code&gt; entries count toward the 10-lookup limit?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;ip4:&lt;/code&gt;, &lt;code&gt;ip6:&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt; cause no DNS queries at evaluation time and are exempt. Only &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt; and &lt;code&gt;redirect&lt;/code&gt; count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does SPF on its own stop spoofing?&lt;/strong&gt;&lt;br&gt;
No. SPF validates the envelope sender, not the &lt;code&gt;From:&lt;/code&gt; header a recipient reads. An attacker can pass SPF on a domain they control while displaying yours. DMARC ties the two identities together, which is what actually closes the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long can an SPF record be?&lt;/strong&gt;&lt;br&gt;
Each individual string in a TXT record is capped at 255 characters, though multiple strings are concatenated. RFC 7208 recommends keeping the whole DNS answer under 450 octets so it survives in a UDP packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/spf-record-checker" rel="noopener noreferrer"&gt;SPF record checker: how to test, read and fix your SPF record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/how-to-set-up-dmarc" rel="noopener noreferrer"&gt;How to set up DMARC, step by step&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/what-is-dkim" rel="noopener noreferrer"&gt;What is DKIM? DomainKeys Identified Mail explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;RFC 7208, Sender Policy Framework (SPF) version 1&lt;/a&gt;, IETF, April 2014 (§3.1, §3.4, §4.5, §4.6.2, §4.6.4, §5.5, §6.1, §6.2) · DMARCguard, &lt;a href="https://dmarcguard.io/research/email-authentication/" rel="noopener noreferrer"&gt;"State of Email Authentication 2026"&lt;/a&gt;, scan of 5,499,028 Tranco domains, 27 February 2026 · Google and Yahoo bulk sender requirements, effective February 2024.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/spf-record-syntax" rel="noopener noreferrer"&gt;fortifynet.com/blog/spf-record-syntax&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spf</category>
      <category>spfrecord</category>
      <category>emailauthentication</category>
      <category>dns</category>
    </item>
    <item>
      <title>ERR_CERT_COMMON_NAME_INVALID: Causes and Fixes | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Sat, 22 Aug 2026 04:45:29 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/errcertcommonnameinvalid-causes-and-fixes-fortifynet-4o06</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/errcertcommonnameinvalid-causes-and-fixes-fortifynet-4o06</guid>
      <description>&lt;h1&gt;
  
  
  ERR_CERT_COMMON_NAME_INVALID: What It Means and How to Fix It
&lt;/h1&gt;

&lt;p&gt;ERR_CERT_COMMON_NAME_INVALID means the website presented a real, unexpired certificate, but that certificate was issued for a different name than the one in your address bar. Chrome compares the domain you requested against the hostnames listed in the certificate's Subject Alternative Name (SAN) field. If none of them match, Chrome blocks the page with this error. The fix depends on who you are: as a visitor you can usually resolve or safely judge it in minutes, while as a site owner you need to reissue or reconfigure a certificate. This guide covers both, with data on how common each cause really is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the error actually means
&lt;/h2&gt;

&lt;p&gt;Every public HTTPS certificate carries a list of hostnames it is valid for, stored in the SAN extension. Browsers historically also accepted the certificate's Common Name (CN) field, which is where this error got its name. That fallback is long gone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RFC 2818 deprecated CN matching for HTTPS back in the year 2000.&lt;/li&gt;
&lt;li&gt;Chrome 58 removed CN matching entirely in April 2017 and has required SAN entries ever since (source: Chrome for Developers, "Deprecations and Removals in Chrome 58"). At the time, only about 0.1% of all certificate validations still relied on the CN fallback (Chromium security-dev, 2017).&lt;/li&gt;
&lt;li&gt;RFC 9525 (November 2023), which replaced RFC 6125, made it formal across the industry: clients must check the SAN extension only and must not match domain names against the CN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So despite its name, this error really means "SAN mismatch": nothing in the certificate's name list covers the host you asked for.&lt;/p&gt;

&lt;p&gt;It is also remarkably common. In Google's study of over 300 million real-world Chrome certificate warnings (Acer et al., "Where the Wild Warnings Are", ACM CCS 2017), name mismatch was the single largest server-side cause of certificate warnings on desktop: 11.7% of all warning reports on Windows and 11.6% on Mac, ahead of untrusted authorities and expired certificates.&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%2Fquickchart.io%2Fchart%3Fw%3D700%26h%3D420%26bkg%3Dwhite%26c%3D%257Btype%253A%2527bar%2527%252Cdata%253A%257Blabels%253A%255B%2527Name%2520mismatch%2527%252C%2527Untrusted%2520authority%2527%252C%2527Expired%2520or%2520bad%2520date%2527%252C%2527Missing%2520intermediates%2527%255D%252Cdatasets%253A%255B%257Bdata%253A%255B11.7%252C6.11%252C4.23%252C1.26%255D%252CbackgroundColor%253A%255B%2527%2523dc2626%2527%252C%2527%2523f97316%2527%252C%2527%25230ea5e9%2527%252C%2527%252364748b%2527%255D%257D%255D%257D%252Coptions%253A%257Bplugins%253A%257Blegend%253A%257Bdisplay%253Afalse%257D%252Ctitle%253A%257Bdisplay%253Atrue%252Ctext%253A%2527Chrome%2520certificate%2520warnings%2520by%2520server-side%2520cause%2520%2528%2525%2520of%2520all%2520reports%252C%2520Windows%2529%2527%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fw%3D700%26h%3D420%26bkg%3Dwhite%26c%3D%257Btype%253A%2527bar%2527%252Cdata%253A%257Blabels%253A%255B%2527Name%2520mismatch%2527%252C%2527Untrusted%2520authority%2527%252C%2527Expired%2520or%2520bad%2520date%2527%252C%2527Missing%2520intermediates%2527%255D%252Cdatasets%253A%255B%257Bdata%253A%255B11.7%252C6.11%252C4.23%252C1.26%255D%252CbackgroundColor%253A%255B%2527%2523dc2626%2527%252C%2527%2523f97316%2527%252C%2527%25230ea5e9%2527%252C%2527%252364748b%2527%255D%257D%255D%257D%252Coptions%253A%257Bplugins%253A%257Blegend%253A%257Bdisplay%253Afalse%257D%252Ctitle%253A%257Bdisplay%253Atrue%252Ctext%253A%2527Chrome%2520certificate%2520warnings%2520by%2520server-side%2520cause%2520%2528%2525%2520of%2520all%2520reports%252C%2520Windows%2529%2527%257D%257D%257D%257D" alt="Bar chart showing server-side causes of Chrome certificate warnings on Windows: name mismatch 11.7 percent, untrusted authority 6.11 percent, expired or bad date 4.23 percent, missing intermediates 1.26 percent" width="1400" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Server-side causes of Chrome certificate warnings as a share of all warning reports on Windows. Data: Acer et al., "Where the Wild Warnings Are", ACM CCS 2017.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7 common causes, and who can fix them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;What is happening&lt;/th&gt;
&lt;th&gt;Who fixes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;www mismatch&lt;/td&gt;
&lt;td&gt;The certificate covers &lt;code&gt;www.example.com&lt;/code&gt; but not &lt;code&gt;example.com&lt;/code&gt;, or the reverse&lt;/td&gt;
&lt;td&gt;Site owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Subdomain outside wildcard scope&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;*.example.com&lt;/code&gt; does not cover &lt;code&gt;app.eu.example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Site owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Wrong or default server certificate&lt;/td&gt;
&lt;td&gt;Shared hosting or a misconfigured server (SNI) answers with another site's certificate&lt;/td&gt;
&lt;td&gt;Site owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Stale DNS or a moved domain&lt;/td&gt;
&lt;td&gt;The domain still points at a server that no longer hosts it, so a stranger's certificate answers&lt;/td&gt;
&lt;td&gt;Site owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;CN-only certificate&lt;/td&gt;
&lt;td&gt;A legacy or internal certificate has the right CN but no SAN entries; Chrome has rejected these since version 58 (April 2017)&lt;/td&gt;
&lt;td&gt;Site owner / IT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Captive portal&lt;/td&gt;
&lt;td&gt;Hotel or airport Wi-Fi intercepts your first request to show its login page&lt;/td&gt;
&lt;td&gt;Visitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;HTTPS inspection&lt;/td&gt;
&lt;td&gt;Antivirus or a corporate proxy re-signs traffic with its own certificate&lt;/td&gt;
&lt;td&gt;Visitor / IT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Within name mismatches, subdomain mistakes stand out. In the same Google dataset, 13.2% of server name-mismatch errors were requests for subdomains outside a wildcard's scope, and 3.7% were plain www mismatches (Acer et al., CCS 2017). A further 3.8% of name mismatch errors matched known captive portal patterns, which means the network, not the website, caused them.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are a visitor: 5 quick checks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Try the other form of the address.&lt;/strong&gt; If &lt;code&gt;example.com&lt;/code&gt; fails, try &lt;code&gt;www.example.com&lt;/code&gt;, or the reverse. If one works, the site has a www mismatch and you can tell the owner exactly that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at who the certificate was issued to.&lt;/strong&gt; Click the padlock or "Not secure" chip, open certificate details, and read the SAN list. A certificate for a hosting company's placeholder domain or a completely unrelated site explains the block instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On public Wi-Fi, trigger the login page first.&lt;/strong&gt; Captive portals intercept your first HTTPS request and answer with their own certificate. Open any plain HTTP page (for example a router status page or &lt;code&gt;neverssl.com&lt;/code&gt;), complete the portal login, then retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with HTTPS inspection off.&lt;/strong&gt; Some antivirus suites and corporate proxies re-sign TLS traffic. Temporarily disable HTTPS scanning or try another network; if the error disappears, the interception software is the cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not click through on sites that matter.&lt;/strong&gt; For banking, email, or anything with a login, treat a name mismatch as a hard stop. The generic version of this warning is covered in our guide to the &lt;a href="https://fortifynet.com/blog/your-connection-is-not-private" rel="noopener noreferrer"&gt;"Your connection is not private" error&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If you own the site: diagnose, then fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1200%26q%3D80" alt="Laptop screen showing website monitoring graphs and analytics" width="1200" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by reading exactly which names your certificate covers. From any terminal with OpenSSL installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &amp;lt;/dev/null 2&amp;gt;/dev/null | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-ext&lt;/span&gt; subjectAltName
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it twice, once with your apex domain and once with &lt;code&gt;www&lt;/code&gt;, and compare the SAN output against every hostname your users actually visit. Then fix the cause you find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cover the apex and www together.&lt;/strong&gt; Issue one certificate that lists both names. Let's Encrypt includes up to 100 hostnames per certificate at no cost (Let's Encrypt rate limit documentation), and every commercial CA sells multi-domain SAN certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind wildcard scope.&lt;/strong&gt; A wildcard matches exactly one label, per RFC 9525:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name requested&lt;/th&gt;
&lt;th&gt;Covered by &lt;code&gt;*.example.com&lt;/code&gt;?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, the apex needs its own SAN entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eu.app.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, wildcards match one label only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app.example.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No, different domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fix SNI and the default virtual host.&lt;/strong&gt; If your server hosts several sites, make sure each hostname is tied to its own certificate and that the default (fallback) virtual host does not answer with the wrong one for clients or bots that omit SNI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check both ends of your CDN.&lt;/strong&gt; With a CDN or reverse proxy in front, the edge certificate must cover your public hostname and the origin certificate must cover the origin hostname the CDN connects to. A mismatch on either leg triggers errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not rely on redirects.&lt;/strong&gt; TLS is negotiated before any HTTP redirect is sent. Even if &lt;code&gt;example.com&lt;/code&gt; immediately redirects to &lt;code&gt;www.example.com&lt;/code&gt;, the certificate served on &lt;code&gt;example.com&lt;/code&gt; must still cover that name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more reason to automate: under CA/Browser Forum ballot SC-081v3 (adopted April 2025), the maximum public certificate lifetime dropped from 398 to 200 days on March 15, 2026, falls to 100 days in March 2027, and reaches 47 days in March 2029. Renewals are becoming far more frequent, and every renewal is a fresh chance for a SAN list to silently lose a name. Use ACME automation and monitor your coverage instead of trusting memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to click "Proceed anyway"?&lt;/strong&gt;&lt;br&gt;
Only when you positively know why the mismatch exists, for example your own test server reached by IP address. On any site where you would type a password or payment details, do not proceed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this error mean the site was hacked?&lt;/strong&gt;&lt;br&gt;
Almost never. It usually means a configuration mistake: a renamed domain, a forgotten www entry, or a wildcard that does not stretch as far as the owner thought. Attacks exist, which is exactly why browsers refuse to guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is it called "common name" if browsers ignore the CN field?&lt;/strong&gt;&lt;br&gt;
The name is historical. Browsers matched the CN field until RFC 2818 (2000) deprecated it and Chrome 58 (April 2017) removed it. Today only the SAN list counts, per RFC 9525 (November 2023), but the error identifier was never renamed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a separate certificate for every subdomain?&lt;/strong&gt;&lt;br&gt;
No. One certificate can list many SAN entries, and a wildcard covers all direct subdomains at one level. You only need extra planning for deeper levels like &lt;code&gt;a.b.example.com&lt;/code&gt;, which a single-level wildcard does not cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your certificate coverage in 60 seconds
&lt;/h2&gt;

&lt;p&gt;A name mismatch is invisible until someone hits the one hostname you forgot. FortifyNet's free scan reads your live certificate and reports which names it covers, alongside your TLS configuration, security headers, DNS, email authentication, and dark-web exposure. &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;Run the free scan&lt;/a&gt; on your domain and check apex, www, and subdomains in one pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/your-connection-is-not-private" rel="noopener noreferrer"&gt;"Your connection is not private": what it means and how to fix it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/err-cert-authority-invalid" rel="noopener noreferrer"&gt;ERR_CERT_AUTHORITY_INVALID: what causes it and how to fix it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/err-ssl-protocol-error" rel="noopener noreferrer"&gt;ERR_SSL_PROTOCOL_ERROR: what it means and how to fix it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/err-cert-common-name-invalid" rel="noopener noreferrer"&gt;fortifynet.com/blog/err-cert-common-name-invalid&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssl</category>
      <category>certificates</category>
      <category>browsererrors</category>
      <category>chrome</category>
    </item>
    <item>
      <title>X-Frame-Options: How It Works and How to Set It | FortifyNet</title>
      <dc:creator>Håkan Fägnell</dc:creator>
      <pubDate>Wed, 19 Aug 2026 04:45:29 +0000</pubDate>
      <link>https://dev.to/hkan_fgnell_322b4aba3d6/x-frame-options-how-it-works-and-how-to-set-it-fortifynet-4oao</link>
      <guid>https://dev.to/hkan_fgnell_322b4aba3d6/x-frame-options-how-it-works-and-how-to-set-it-fortifynet-4oao</guid>
      <description>&lt;h1&gt;
  
  
  X-Frame-Options: What It Does and How to Configure It Right
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;X-Frame-Options is an HTTP response header that tells the browser whether your page may be rendered inside a frame or iframe on another site.&lt;/strong&gt; It has exactly two valid values: &lt;code&gt;DENY&lt;/code&gt; (no framing at all) and &lt;code&gt;SAMEORIGIN&lt;/code&gt; (framing allowed only from your own origin). Its purpose is to stop clickjacking, an attack where your site is loaded invisibly inside an attacker's page and your visitors are tricked into clicking things they cannot see. The modern replacement is the Content-Security-Policy directive &lt;code&gt;frame-ancestors&lt;/code&gt;, and the 2026 best practice is to send both headers. This guide covers the correct setup for every major server, plus the mistakes that silently disable the protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the X-Frame-Options header does
&lt;/h2&gt;

&lt;p&gt;When a browser loads a page that another document has embedded through &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;frame&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;embed&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt;, it checks the response headers of the embedded page before rendering it. If the response carries &lt;code&gt;X-Frame-Options: DENY&lt;/code&gt;, the browser refuses to draw the page inside the embedding document. With &lt;code&gt;SAMEORIGIN&lt;/code&gt;, it renders the page only if the embedding chain comes from the same origin (same scheme, host and port). Modern browsers evaluate the entire chain of ancestor frames, not just the top window.&lt;/p&gt;

&lt;p&gt;The header was formalized in &lt;a href="https://www.rfc-editor.org/rfc/rfc7034" rel="noopener noreferrer"&gt;RFC 7034&lt;/a&gt; in October 2013, after browsers had already shipped it. It remains one of the most widely deployed security headers: the &lt;a href="https://almanac.httparchive.org/en/2025/security" rel="noopener noreferrer"&gt;HTTP Archive Web Almanac 2025 security chapter&lt;/a&gt; measures X-Frame-Options on roughly 35% of mobile sites, making it a top three security header behind X-Content-Type-Options (close to 50%).&lt;/p&gt;

&lt;h2&gt;
  
  
  Clickjacking in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Clickjacking was named by security researchers Robert Hansen and Jeremiah Grossman in 2008. The attack is simple: an attacker's page loads your site in an iframe, makes the iframe fully transparent with CSS, and positions it over harmless looking buttons. The visitor believes they are clicking "Play video", but the click actually lands on your site's "Delete account", "Confirm payment" or "Authorize app" button, using the victim's logged-in session. &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt; documents variants ranging from Facebook "likejacking" to hijacked one-click purchases.&lt;/p&gt;

&lt;p&gt;Framing control is the defense. If the browser refuses to render your page inside an attacker's frame, the overlay trick collapses.&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%2Fimages.unsplash.com%2Fphoto-1555066931-4365d14bab8c%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" 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%2Fimages.unsplash.com%2Fphoto-1555066931-4365d14bab8c%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" alt="Developer reviewing web server configuration code on a laptop screen" width="1600" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The two valid values, and the ones to avoid
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Status in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DENY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No site may frame the page, including your own&lt;/td&gt;
&lt;td&gt;Valid, supported by all browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SAMEORIGIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only pages from the same origin may frame it&lt;/td&gt;
&lt;td&gt;Valid, supported by all browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALLOW-FROM uri&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Was meant to allow one named origin&lt;/td&gt;
&lt;td&gt;Obsolete: Firefox removed it in version 70 (October 2019), Chrome and Safari never supported it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALLOWALL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Never part of any spec&lt;/td&gt;
&lt;td&gt;Invalid: browsers ignore the header entirely, leaving no protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dangerous part is how browsers handle invalid values: they ignore the whole header. A site sending &lt;code&gt;ALLOW-FROM&lt;/code&gt; or &lt;code&gt;ALLOWALL&lt;/code&gt; believes it is protected while browsers treat it as if no header were set. As the Web Almanac 2025 puts it, these values "may have been set by developers expecting protections to be active due to them setting the header."&lt;/p&gt;

&lt;h2&gt;
  
  
  What real-world data shows
&lt;/h2&gt;

&lt;p&gt;Across millions of sites measured by HTTP Archive in 2025, the header's values break down like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fquickchart.io%2Fchart%3Fw%3D700%26h%3D380%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522SAMEORIGIN%2522%252C%2522DENY%2522%252C%2522ALLOWALL%2520%2528invalid%2529%2522%252C%2522Other%252Finvalid%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522data%2522%253A%255B72.1%252C24.6%252C0.7%252C2.5%255D%252C%2522backgroundColor%2522%253A%255B%2522%25232563eb%2522%252C%2522%252316a34a%2522%252C%2522%2523dc2626%2522%252C%2522%25239ca3af%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522X-Frame-Options%2520values%2520across%2520the%2520web%252C%25202025%2520%2528%2525%2520of%2520XFO%2520headers%252C%2520mobile%2529%2522%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A80%257D%257D%257D%257D" 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%2Fquickchart.io%2Fchart%3Fw%3D700%26h%3D380%26bkg%3Dwhite%26c%3D%257B%2522type%2522%253A%2522bar%2522%252C%2522data%2522%253A%257B%2522labels%2522%253A%255B%2522SAMEORIGIN%2522%252C%2522DENY%2522%252C%2522ALLOWALL%2520%2528invalid%2529%2522%252C%2522Other%252Finvalid%2522%255D%252C%2522datasets%2522%253A%255B%257B%2522data%2522%253A%255B72.1%252C24.6%252C0.7%252C2.5%255D%252C%2522backgroundColor%2522%253A%255B%2522%25232563eb%2522%252C%2522%252316a34a%2522%252C%2522%2523dc2626%2522%252C%2522%25239ca3af%2522%255D%257D%255D%257D%252C%2522options%2522%253A%257B%2522plugins%2522%253A%257B%2522legend%2522%253A%257B%2522display%2522%253Afalse%257D%252C%2522title%2522%253A%257B%2522display%2522%253Atrue%252C%2522text%2522%253A%2522X-Frame-Options%2520values%2520across%2520the%2520web%252C%25202025%2520%2528%2525%2520of%2520XFO%2520headers%252C%2520mobile%2529%2522%257D%257D%252C%2522scales%2522%253A%257B%2522y%2522%253A%257B%2522beginAtZero%2522%253Atrue%252C%2522max%2522%253A80%257D%257D%257D%257D" alt="Bar chart showing the distribution of X-Frame-Options values on mobile sites in 2025: SAMEORIGIN 72.1 percent, DENY 24.6 percent, ALLOWALL 0.7 percent, other or invalid values 2.5 percent" width="1400" height="760"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: HTTP Archive Web Almanac 2025, security chapter, figure 9.30 (mobile dataset).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;About 72.1% of sites that send the header choose &lt;code&gt;SAMEORIGIN&lt;/code&gt; and 24.6% choose &lt;code&gt;DENY&lt;/code&gt;. Roughly 3% send values that do nothing, which on the scale of the web is hundreds of thousands of sites with imaginary protection. Checking what your server actually sends takes less than a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  X-Frame-Options vs CSP frame-ancestors
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/TR/CSP2/" rel="noopener noreferrer"&gt;W3C Content Security Policy Level 2 specification&lt;/a&gt; (a W3C Recommendation since December 2016) introduced the &lt;code&gt;frame-ancestors&lt;/code&gt; directive and formally obsoleted X-Frame-Options. Where XFO is a blunt on/off switch, &lt;code&gt;frame-ancestors&lt;/code&gt; is a full allowlist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;X-Frame-Options&lt;/th&gt;
&lt;th&gt;CSP frame-ancestors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Block all framing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DENY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frame-ancestors 'none'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allow own origin only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SAMEORIGIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frame-ancestors 'self'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allow named partner origins&lt;/td&gt;
&lt;td&gt;Not possible (ALLOW-FROM is dead)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frame-ancestors 'self' https://partner.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wildcard subdomains&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frame-ancestors https://*.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spec status&lt;/td&gt;
&lt;td&gt;Informational RFC 7034 (2013), obsoleted&lt;/td&gt;
&lt;td&gt;W3C CSP Level 2 (2016), current standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When both headers are present&lt;/td&gt;
&lt;td&gt;Ignored by CSP2-capable browsers&lt;/td&gt;
&lt;td&gt;Takes precedence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details worth knowing. First, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors" rel="noopener noreferrer"&gt;MDN&lt;/a&gt; notes that &lt;code&gt;frame-ancestors&lt;/code&gt; does not inherit from &lt;code&gt;default-src&lt;/code&gt;: a policy of &lt;code&gt;default-src 'none'&lt;/code&gt; still allows anyone to frame the page, so you must declare the directive explicitly. Second, all current versions of Chrome, Edge, Firefox and Safari support &lt;code&gt;frame-ancestors&lt;/code&gt;, so the only reason to keep sending X-Frame-Options is defense in depth for very old clients. That costs one line, so keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to configure it on your server
&lt;/h2&gt;

&lt;p&gt;Send the header on every HTML response, exactly once. The examples below set &lt;code&gt;SAMEORIGIN&lt;/code&gt; plus the equivalent CSP directive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nginx&lt;/strong&gt; (inside &lt;code&gt;server&lt;/code&gt; or &lt;code&gt;location&lt;/code&gt;):&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;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Content-Security-Policy&lt;/span&gt; &lt;span class="s"&gt;"frame-ancestors&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Apache 2.4&lt;/strong&gt; (httpd.conf or .htaccess, mod_headers enabled):&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;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Frame-Options "SAMEORIGIN"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Content-Security-Policy "frame-ancestors 'self';"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IIS&lt;/strong&gt; (web.config):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;system.webServer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;httpProtocol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;customHeaders&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Frame-Options"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"frame-ancestors 'self';"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/customHeaders&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/httpProtocol&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/system.webServer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Node.js with Express and Helmet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;helmet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;helmet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;helmet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frameguard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sameorigin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;helmet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentSecurityPolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;directives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;frameAncestors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'self'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind a CDN such as Cloudflare you can also inject both headers with a response header transform rule, which is useful when you cannot touch the origin server. If a partner site legitimately needs to embed you, keep XFO at &lt;code&gt;SAMEORIGIN&lt;/code&gt; and express the allowlist in CSP: &lt;code&gt;frame-ancestors 'self' https://partner.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that silently disable the protection
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using ALLOW-FROM in 2026.&lt;/strong&gt; Every current browser ignores it, and with it the whole header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting the header in a meta tag.&lt;/strong&gt; &lt;code&gt;X-Frame-Options&lt;/code&gt; only works as an HTTP header, and &lt;code&gt;frame-ancestors&lt;/code&gt; is explicitly forbidden inside &lt;code&gt;&amp;lt;meta http-equiv="Content-Security-Policy"&amp;gt;&lt;/code&gt;. Both must come from the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending the header twice.&lt;/strong&gt; Duplicate or comma-joined values such as &lt;code&gt;SAMEORIGIN, SAMEORIGIN&lt;/code&gt; (0.28% of sites in the Almanac 2025 data) can be rejected as invalid. Set it in one place only, either the app or the web server, not both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using DENY on pages you embed yourself.&lt;/strong&gt; If your own checkout, widget or preview runs in an iframe, &lt;code&gt;DENY&lt;/code&gt; breaks it. Use &lt;code&gt;SAMEORIGIN&lt;/code&gt; or an explicit &lt;code&gt;frame-ancestors&lt;/code&gt; allowlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming default-src covers framing.&lt;/strong&gt; It does not. Declare &lt;code&gt;frame-ancestors&lt;/code&gt; explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to test your configuration
&lt;/h2&gt;

&lt;p&gt;Open your browser's developer tools, load your page, and inspect the response headers on the main document: you should see exactly one &lt;code&gt;X-Frame-Options&lt;/code&gt; and a &lt;code&gt;Content-Security-Policy&lt;/code&gt; containing &lt;code&gt;frame-ancestors&lt;/code&gt;. For the full picture, the free &lt;a href="https://fortifynet.com" rel="noopener noreferrer"&gt;FortifyNet scan&lt;/a&gt; checks your framing protection together with the rest of your security header set, SSL/TLS, DNS and email authentication in about 60 seconds, and tells you exactly which header to add where. No signup needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/http-security-headers-guide" rel="noopener noreferrer"&gt;HTTP Security Headers: The Complete Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/what-is-hsts" rel="noopener noreferrer"&gt;What Is HSTS? HTTP Strict Transport Security Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fortifynet.com/blog/website-security-checklist" rel="noopener noreferrer"&gt;Website Security Checklist: 12 Essential Steps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is X-Frame-Options deprecated?&lt;/strong&gt;&lt;br&gt;
Formally yes: W3C CSP Level 2 obsoleted it in favor of &lt;code&gt;frame-ancestors&lt;/code&gt; back in 2016. In practice every browser still honors it, and OWASP recommends sending both headers for defense in depth. The one thing you must not do is rely on &lt;code&gt;ALLOW-FROM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use DENY or SAMEORIGIN?&lt;/strong&gt;&lt;br&gt;
Use &lt;code&gt;DENY&lt;/code&gt; if nothing on your site is ever shown in a frame, which is the strongest setting. Use &lt;code&gt;SAMEORIGIN&lt;/code&gt; if your own pages embed each other, for example dashboards, previews or internal widgets. In the 2025 Web Almanac data, 72.1% of sites choose SAMEORIGIN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I allow one specific partner site to embed my pages?&lt;/strong&gt;&lt;br&gt;
X-Frame-Options cannot do this anymore. Use &lt;code&gt;Content-Security-Policy: frame-ancestors 'self' https://partner.example.com&lt;/code&gt; and list each allowed origin explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if I send both X-Frame-Options and frame-ancestors?&lt;/strong&gt;&lt;br&gt;
Browsers that support CSP Level 2, which is all modern ones, enforce &lt;code&gt;frame-ancestors&lt;/code&gt; and ignore X-Frame-Options. Older clients fall back to X-Frame-Options. That fallback chain is exactly why sending both is the recommended setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I set X-Frame-Options with a meta tag?&lt;/strong&gt;&lt;br&gt;
No. Browsers only honor it as a real HTTP response header. If you cannot change server configuration, set it through your hosting panel, CDN rules or application middleware.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://fortifynet.com/blog/x-frame-options" rel="noopener noreferrer"&gt;fortifynet.com/blog/x-frame-options&lt;/a&gt;. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>securityheaders</category>
      <category>clickjacking</category>
      <category>xframeoptions</category>
      <category>csp</category>
    </item>
  </channel>
</rss>
