Security Headers #
When a browser loads a website page, the web server sends a set of additional information at the HTTP response header level. Security Headers are special instructions telling the browser how it should treat our page content — whether it may be loaded inside another page’s frame (iframe), which protocols are allowed to load scripts, what types of resources may execute, and even restrictions on client hardware access. Configuring Security Headers correctly is a critical, very effective step for protecting your web applications from common attacks like Cross-Site Scripting (XSS), Clickjacking, MIME Sniffing, and technology information leakage (information disclosure). We’ll discuss in depth the essential header configurations, composing Content Security Policy (CSP), dynamic random token filling (CSP Nonce), browser feature restrictions (Permissions Policy), and compliance testing methods for industry security standards.
The Urgency and How Security Headers Work #
Logically, the web server acts as data provider, while the browser acts as the interpreter and executor of that data. Security problems arise because browsers inherently trust any data the server sends. If your server suffers a malicious script injection attack (XSS), visitor browsers execute that script without suspicion.
Security Headers function as an access restriction policy for the browser. When the browser receives a web page accompanied by Security Headers, the browser activates its internal protection modules (like disabling file type guessing, refusing to render iframes from other domains, or limiting Javascript execution to trusted domains only).
Caddy makes writing these headers easy through its built-in header directive, which can be applied globally or per specific route.
Essential Security Header Configuration #
Here’s an example Caddyfile configuration including a set of essential Security Headers to protect normal web traffic:
# Essential Security Headers configuration in Caddy
example.com {
header {
# 1. Transport Encryption Protection (HSTS)
# Instructs the browser to always use HTTPS for the next 2 years
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# 2. Content Type Sniffing Protection (MIME Sniffing)
# Refuses the browser's instruction to guess file types beyond the Content-Type declaration
X-Content-Type-Options "nosniff"
# 3. Clickjacking Protection
# The page may only be loaded inside an iframe by the same domain
X-Frame-Options "SAMEORIGIN"
# 4. URL Referral Privacy Policy (Referrer Policy)
# Only send referer data when moving to the same HTTPS protocol
Referrer-Policy "strict-origin-when-cross-origin"
# 5. Minimize Server Information Leakage
# Remove the Caddy web server version and backend language info from response headers
-Server
-X-Powered-By
}
file_server {
root /var/www/html
}
}
Let’s dissect each header’s function in depth:
1. HTTP Strict Transport Security (HSTS) #
HSTS tells the browser that your domain (and all subdomains below it) may only be accessed through a secure HTTPS connection. If a user tries typing http://example.com, the browser automatically changes it to https://example.com on the client side before the request is sent to the network, preventing SSL Stripping attacks.
The configuration rules include:
max-age: Specifies the browser’s memory duration in seconds. The value63072000equals 2 years (recommended for production).includeSubDomains: Applies the same rule to all subdomains (e.g.,app.example.comandapi.example.com).preload: Registers your domain into the official HSTS preload list embedded directly in browser source code (Chrome, Firefox, Safari). Once your domain enters this list, browsers never try opening the plain HTTP version even on the very first visit.
[!WARNING] Enable
preloadvery carefully! Once a domain is registered in the browser HSTS preload list, its removal process is very complicated and takes months. If you later need to run a plain HTTP server on one of the subdomains for technical reasons, that subdomain will be completely dead (unreachable) for all visitors. Start testing HSTS withoutpreloadwith amax-age=300value (5 minutes), then raise it to86400(1 day), until you’re finally ready to commit to a 2-year duration.
2. X-Content-Type-Options: nosniff #
This header prevents browsers from doing MIME Sniffing — analyzing downloaded file bytes to guess their data type independently. For example, if your server serves an image file (.png) but an attacker managed to upload malicious Javascript code inside the image bytes, a browser without the nosniff header would execute that image as script code. Setting this header to "nosniff" forces browsers to strictly obey the server-sent Content-Type header declaration.
3. X-Frame-Options: SAMEORIGIN #
This header protects websites from Clickjacking attacks, where attackers load your web page inside a transparent frame (iframe) over their malicious website to trick users into clicking important buttons without realizing it.
DENY: Refuses the page being loaded in an iframe by anyone.SAMEORIGIN: Allows the page to be loaded in an iframe only if the wrapping domain is the same as this website’s domain.
4. Referrer-Policy: strict-origin-when-cross-origin #
Controls how much original URL referral information (Referer) the browser sends when a user clicks a link leaving your website. The "strict-origin-when-cross-origin" choice is the best balance point between privacy and functionality: it sends the full URL path within the same domain, sends only the main domain when moving to another domain, and sends no information at all when moving from HTTPS to plain unencrypted HTTP.
5. Hiding the Technology Stack Identity #
By default, Caddy (and backend frameworks like PHP or Express) includes their identity information on response headers (like Server: Caddy or X-Powered-By: PHP/8.2). This information gives attackers valuable clues for finding exploits specific to that technology version. You can remove them from response headers by adding a minus sign (-) before the header name in Caddy’s header directive.
Advanced Content Security Policy (CSP) #
Content Security Policy (CSP) is the strongest defense you have for detecting and mitigating script injection attacks (Cross-Site Scripting / XSS) and data injection. CSP acts as an allowlist for the browser that strictly declares from which domains digital assets (Javascript, CSS, Images, Fonts, WebSocket connections, Iframes) may be loaded.
Here’s a strict yet flexible CSP configuration for modern web applications:
# Strict CSP configuration in the Caddyfile
example.com {
header {
Content-Security-Policy "
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https: blob:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
object-src 'none';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
"
}
reverse_proxy localhost:3000
}
Detailed CSP Directive Explanation #
default-src 'self': The fallback rule. All asset types not specifically defined below may only be loaded from the same domain ('self').script-src 'self' ...: Restricts Javascript execution origins. Only allow local scripts and scripts from trusted CDNs (cdn.jsdelivr.net).style-src 'self' 'unsafe-inline' ...: Restricts CSS stylesheet origins. Using'unsafe-inline'is sometimes needed if your CSS framework (like Tailwind/Vuetify) writes style tags dynamically in HTML.img-src 'self' data: https: blob:: Allows image loading from local domains, Base64 schemes (data:), external HTTPS protocols, and binary objects (blob:).object-src 'none': Completely forbids loading legacy browser plugins vulnerable to security holes like Flash, Java Applet, or Active-X (<object>,<embed>).frame-ancestors 'none': Forbids this page being loaded in an iframe by any other domain. This directive is the modern version ofX-Frame-Optionsfully supported by modern browsers.upgrade-insecure-requests: Instructs the browser to automatically change all HTTP asset requests (like images or scripts) to HTTPS before loading them, preventing Mixed Content issues.
Dynamic CSP Nonce Generation #
Applying a strict CSP often hits obstacles when your web application needs inline script writing (scripts written directly inside the HTML document, like <script>console.log("hello")</script>). By default, a secure CSP blocks those inline scripts because attackers often insert XSS through this method.
The best solution to safely allow legitimate inline scripts without opening XSS holes is using a CSP Nonce. A nonce is a random one-time cryptographic token (number used once) generated by Caddy on every request.
Caddy inserts this nonce value into the CSP header, and your backend application must insert the same nonce value on the <script> tags in HTML. Browsers only execute scripts if the nonce value on the HTML tag matches the nonce value on the HTTP response header:
# CSP Nonce configuration using Caddy's built-in UUID
example.com {
header {
# We use the unique request UUID placeholder {http.request.uuid} as the nonce
Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-{http.request.uuid}' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
object-src 'none';
"
}
# Forward this nonce UUID to the backend so the backend can render it in HTML templates
reverse_proxy localhost:8080 {
header_up X-CSP-Nonce {http.request.uuid}
}
}
On the backend side (e.g., using the Go HTML Template), you capture that header and render it on the script tag:
<!-- backend index.html template -->
<!DOCTYPE html>
<html>
<head>
<title>Secure Application</title>
</head>
<body>
<h1>Welcome</h1>
<!-- Inline script tag is safe because it has a matching nonce -->
<script nonce="{{.CspNonce}}">
console.log("This inline script executes safely!");
</script>
</body>
</html>
Through this method, if an attacker tries to insert a custom <script> tag, the script is outright rejected by the browser because the attacker can’t guess the random UUID token value Caddy generated for that request.
CSP Report-Only for the Testing Phase #
Enabling a strict CSP directly in production is very risky for breaking web application functionality if there’s an important Javascript library or stylesheet you forgot to register in the permission list.
To prevent this mistake, you should use the monitoring mode first using the Content-Security-Policy-Report-Only header. In this mode, browsers don’t block rule-violating assets, but only send violation reports to your reporting server:
# Testing CSP in production without breaking functionality
example.com {
header {
# Report-Only Mode: Only records, doesn't block
Content-Security-Policy-Report-Only "
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
report-uri https://telemetry-log.example.com/csp-reports;
"
}
reverse_proxy localhost:3000
}
After monitoring the violation report logs for a few weeks and being confident there are no false positives (no legitimate scripts missed), you can change that header name to Content-Security-Policy to actively enable blocking.
Permissions Policy (Browser Feature Restrictions) #
Permissions Policy (formerly known as Feature Policy) lets you explicitly control which browser features and APIs your web page and its iframes may use.
For example, if your web application is just a simple blog, there’s no reason for the system to access the user’s camera, microphone, or GPS location. Disabling these features prevents exploitation if attackers manage to sneak in malicious ads (malvertising) trying to access user devices:
# Restricting client browser API access
example.com {
header {
# Empty parentheses () completely deny feature access
Permissions-Policy "
camera=(),
microphone=(),
geolocation=(),
payment=(),
usb=(),
accelerometer=(),
gyroscope=(),
fullscreen=(self) # Only allow fullscreen mode from our own domain
"
}
file_server
}
Applying Dry Snippets for Configuration Efficiency #
If you manage many subdomains or several virtual hosts in one Caddyfile, rewriting the same header block repeatedly makes your Caddyfile long and hard to maintain.
You can use the Snippet feature (reusable code blocks) to wrap all your Security Headers, then import them into every site block instantly:
# ─── SECURITY HEADERS SNIPPET DEFINITION ───
(global_security) {
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
# Hide server signatures
-Server
-X-Powered-By
-X-AspNet-Version
}
}
# ─── APPLYING THE SNIPPET TO SITE BLOCKS ───
# 1. Securing the main site
example.com {
import global_security
root * /var/www/html
file_server
}
# 2. Securing the API subdomain
api.example.com {
import global_security
reverse_proxy localhost:8080
}
Configuring Headers at the Site Level vs Reverse Proxy Level #
When composing the Caddyfile, you must understand the scope differences of the header directive placement:
- Site Level (Global in the Site Block): Headers are placed directly inside the domain block. This rule applies to all responses leaving Caddy, including internal error pages Caddy generates itself (like the 502 Bad Gateway error page when the backend is down).
- Reverse Proxy Level (Local in
reverse_proxy): Headers are written using theheader_downsubdirective inside the reverse proxy block. This rule only applies to successful responses forwarded from the backend server.
# Site Level vs Proxy Level Headers comparison
example.com {
# Site Level: Guarantees Caddy's error pages also have these security headers
header X-Frame-Options "SAMEORIGIN"
reverse_proxy localhost:3000 {
# Proxy Level: Specific manipulation for backend responses
# Remove the X-Powered-By header leaking from the NodeJS/Express server
header_down -X-Powered-By
# Add a custom header for internal debugging
header_down X-Backend-Server "node-worker-01"
}
}
Security Header Testing and Verification Methods #
After applying all the Security Headers in the Caddyfile, you must validate the results to ensure browsers receive those parameters correctly.
1. Command Line Testing (CLI Verification) #
You can use the curl utility with the -I parameter (only requesting HTTP response headers) and filter it using grep to verify the presence of security headers:
# Request the response headers from our production website
curl -I -s https://example.com | grep -E -i "strict-transport|x-frame|x-content|referrer|content-security|permissions-policy|server"
# Expected output:
# strict-transport-security: max-age=63072000; includeSubDomains; preload
# x-content-type-options: nosniff
# x-frame-options: SAMEORIGIN
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: camera=(), microphone=(), geolocation=()
# (Note: There should be no 'server: Caddy' output because we already removed it)
2. Testing Using Online Analysis Tools #
To get a comprehensive industry-recognized assessment, you’re strongly advised to test your website through the following free online audit services:
- SecurityHeaders.com: This service scans your website’s response headers and gives a letter grade from F (very bad) to A+ (high-level security). Applying the
global_securitysnippet configuration above helps your website get an A or A+ score instantly. - Observatory.mozilla.org: An in-depth security analysis tool developed by Mozilla to validate your CSP policies, HSTS, and encryption configuration resilience.
- SSLLabs.com: Assesses SSL/TLS configuration quality. By default, Caddy uses modern TLS parameters (TLS 1.2 and 1.3) that are secure, so it immediately gets an A+ grade without needing additional modifications.
Summary #
- Main Function: Security Headers are HTTP instructions telling browsers how to secure page content from client-side attacks (XSS, Clickjacking, MIME Sniffing).
- HSTS Protection: Use
Strict-Transport-Securityto enforce HTTPS. Be careful with thepreloadparameter because it’s permanent and hard to remove from browsers.- Preventing Clickjacking: Apply
X-Frame-Options: SAMEORIGINor the CSPframe-ancestors 'none'directive to forbid loading your web page inside unauthorized iframes.- Strict CSP Policy: Use
Content-Security-Policyto restrict script asset loading origins. UseContent-Security-Policy-Report-Onlyfor a safe testing phase.- CSP Nonce Advantage: Prevent XSS holes in inline scripts by inserting the dynamic UUID value
{http.request.uuid}as the proof-of-authenticity token.- Permissions Policy: Minimize misuse holes by disabling unused browser sensor APIs (like camera, microphone, GPS) using the empty
()parameter.- Reusable Snippets: Use the
(security_headers)Caddyfile snippet to apply security policies consistently across all subdomains without code duplication.- Signature Cleanup: Always remove informative technology stack headers (
-Serverand-X-Powered-By) so attackers struggle to map specific security holes.