<?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: Auric</title>
    <description>The latest articles on DEV Community by Auric (@auric).</description>
    <link>https://dev.to/auric</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%2F4025281%2F01e8e3f9-2b24-4cc0-a4bc-8afda0f3cd07.png</url>
      <title>DEV Community: Auric</title>
      <link>https://dev.to/auric</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/auric"/>
    <language>en</language>
    <item>
      <title>How I made my Astro site secure by default (strict CSP, self-hosted fonts, zero trackers)</title>
      <dc:creator>Auric</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:32:29 +0000</pubDate>
      <link>https://dev.to/auric/how-i-made-my-astro-site-secure-by-default-strict-csp-self-hosted-fonts-zero-trackers-1ceh</link>
      <guid>https://dev.to/auric/how-i-made-my-astro-site-secure-by-default-strict-csp-self-hosted-fonts-zero-trackers-1ceh</guid>
      <description>&lt;p&gt;Most portfolio and blog templates quietly work against your visitors. They pull fonts from Google's CDN (leaking every visitor's IP to a third party), ship analytics that set cookies before anyone consents, and sprinkle inline scripts around — which forces a loose Content-Security-Policy, if there's one at all.&lt;/p&gt;

&lt;p&gt;I wanted the opposite: a site that's &lt;strong&gt;fast, private, and secure by default&lt;/strong&gt; — green on &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt; before I touch a single setting. Here's exactly how, with Astro, and how you can do the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Astro makes this easy
&lt;/h2&gt;

&lt;p&gt;Astro ships &lt;strong&gt;zero JavaScript by default&lt;/strong&gt;. Components render to static HTML at build time; you opt into client-side JS only where you actually need it. That's already half the battle: less JS means a smaller attack surface and a CSP that can stay strict.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A strict Content-Security-Policy
&lt;/h2&gt;

&lt;p&gt;The single biggest win is a CSP that only allows resources from your own origin. The header that matters most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Content&lt;/span&gt;-&lt;span class="n"&gt;Security&lt;/span&gt;-&lt;span class="n"&gt;Policy&lt;/span&gt;:
  &lt;span class="n"&gt;default&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
  &lt;span class="n"&gt;script&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
  &lt;span class="n"&gt;style&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="s1"&gt;'unsafe-inline'&lt;/span&gt;;
  &lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;:;
  &lt;span class="n"&gt;font&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
  &lt;span class="n"&gt;connect&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
  &lt;span class="n"&gt;frame&lt;/span&gt;-&lt;span class="n"&gt;ancestors&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;;
  &lt;span class="n"&gt;base&lt;/span&gt;-&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;;
  &lt;span class="n"&gt;form&lt;/span&gt;-&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical part is &lt;strong&gt;&lt;code&gt;script-src 'self'&lt;/code&gt; with no &lt;code&gt;'unsafe-inline'&lt;/code&gt;&lt;/strong&gt;. That single omission neutralizes the entire class of injected-inline-script XSS — an attacker who manages to inject &lt;code&gt;&amp;lt;script&amp;gt;…&amp;lt;/script&amp;gt;&lt;/code&gt; still can't get it to run. The rule you have to respect in return: &lt;strong&gt;no inline scripts and no inline event handlers anywhere.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Astro makes that painless. &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags inside &lt;code&gt;.astro&lt;/code&gt; components are processed at build time and emitted as &lt;strong&gt;external, hashed files served from your own origin&lt;/strong&gt;, so they satisfy &lt;code&gt;script-src 'self'&lt;/code&gt; automatically. You just avoid &lt;code&gt;onclick="…"&lt;/code&gt;-style handlers and inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks — use &lt;code&gt;addEventListener&lt;/code&gt; in a real script file instead. Writing a theme toggle or view-transition logic? Put it in a &lt;code&gt;.js&lt;/code&gt;/&lt;code&gt;.ts&lt;/code&gt; file, not an inline blob.&lt;/p&gt;

&lt;p&gt;A note on &lt;code&gt;style-src&lt;/code&gt;: I keep &lt;code&gt;'unsafe-inline'&lt;/code&gt; there because Astro emits some scoped &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; inline, and styles can't execute code — the risk is minimal compared to scripts. If you want it fully strict, you can hash or externalize your styles, but the security payoff is small. Spend your strictness budget on &lt;code&gt;script-src&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Self-hosted fonts (kill the Google Fonts request)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@import url('https://fonts.googleapis.com/…')&lt;/code&gt; is the most common privacy leak on the web: every visitor's browser hits Google, handing over their IP and a referer — plus you pay a DNS lookup and a connection to a third party on the critical path.&lt;/p&gt;

&lt;p&gt;Self-hosting fixes privacy and performance in one move:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grab the &lt;code&gt;woff2&lt;/code&gt; files (from the font's repo, or a helper like &lt;code&gt;google-webfonts-helper&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Drop them in &lt;code&gt;public/fonts/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Declare them locally:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Inter'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/fonts/inter-variable.woff2')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&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;ol&gt;
&lt;li&gt;Preload the ones above the fold:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/inter-variable.woff2"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;font-src 'self'&lt;/code&gt; holds, there are &lt;strong&gt;zero third-party requests&lt;/strong&gt;, and text paints sooner. Same change, two wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Security headers
&lt;/h2&gt;

&lt;p&gt;CSP is one header; a few more close the remaining gaps. On Cloudflare Pages or Netlify you set these in a &lt;code&gt;_headers&lt;/code&gt; file; elsewhere, in your server or edge config:&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=63072000; includeSubDomains; preload
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), camera=(), microphone=(), browsing-topics=()
  X-Frame-Options: DENY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each buys you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HSTS&lt;/strong&gt; forces HTTPS and removes the downgrade window. Test it before adding &lt;code&gt;preload&lt;/code&gt; — that step is hard to undo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Content-Type-Options: nosniff&lt;/strong&gt; stops the browser from MIME-guessing a response into something executable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referrer-Policy&lt;/strong&gt; trims the referer you leak to other sites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions-Policy&lt;/strong&gt; disables APIs you don't use and opts out of interest-based ad cohorts (&lt;code&gt;browsing-topics=()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Frame-Options / &lt;code&gt;frame-ancestors 'none'&lt;/code&gt;&lt;/strong&gt; stop clickjacking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Zero trackers by default
&lt;/h2&gt;

&lt;p&gt;The default build ships &lt;strong&gt;no analytics at all&lt;/strong&gt; — no Google Analytics, no cookies, no consent banner to bolt on. If you want numbers later, add a &lt;strong&gt;cookieless, privacy-respecting&lt;/strong&gt; analytics and open exactly one &lt;code&gt;connect-src&lt;/code&gt; entry for it. The point is that privacy is the &lt;em&gt;default&lt;/em&gt;, not something you retrofit and apologize for with a cookie wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Put together, this gets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Green (A+) on securityheaders.com&lt;/strong&gt; with no manual tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No third-party requests&lt;/strong&gt; on a cold load — nothing to block, nothing to leak.&lt;/li&gt;
&lt;li&gt;A genuinely &lt;strong&gt;fast&lt;/strong&gt; site, because "secure by default" and "few requests" are the same discipline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it is exotic: a strict &lt;code&gt;script-src&lt;/code&gt;, local fonts, a handful of headers, and the restraint to not ship trackers. Astro makes it painless because it starts from zero JS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't want to wire all this yourself?
&lt;/h2&gt;

&lt;p&gt;I got tired of re-doing it on every project — so I did it once, properly, and packaged it: &lt;strong&gt;Auric&lt;/strong&gt;, an Astro portfolio + blog theme with the strict CSP, self-hosted fonts, security headers and zero-tracking baked in (gold-on-dark, a light mode too, Markdown/MDX blog + RSS). Everything you edit lives in one config file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;strong&gt;&lt;a href="https://auric-theme.netlify.app" rel="noopener noreferrer"&gt;https://auric-theme.netlify.app&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Get it: &lt;strong&gt;&lt;a href="https://aurictheme.gumroad.com/l/auric" rel="noopener noreferrer"&gt;https://aurictheme.gumroad.com/l/auric&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But honestly — even if you roll your own, do the four things above. Your visitors' browsers will thank you, and your Lighthouse and securityheaders scores will too.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>security</category>
      <category>webdev</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
