Apache Security Headers
Chapter 6 — Apache Security Headers
Every HTTP response your server sends contains headers — metadata that tells the browser how to handle the response. Most of these headers are functional (Content-Type, Content-Length, Cache-Control). Security headers are a distinct category: they tell the browser what the server permits, so the browser can refuse anything outside those boundaries. They cost nothing to add and protect against a wide range of real attacks — from clickjacking to cross-site scripting.
How Security Headers Work
Security headers are client-side enforcement enforced by the browser — they don't stop a determined attacker from making raw HTTP requests, but they protect your actual users' browsers from being used against them. They're most effective against injected content, data leakage, and social-engineering attacks.
apache2ctl -M | grep headers — you should see headers_module. If not: sudo a2enmod headers && sudo systemctl restart apache2.
Hiding Server Information
By default, Apache tells the world exactly what version it's running. That version information is directly useful to attackers — they can look up which CVEs apply to that exact version and craft targeted exploits.
The Security Headers — One by One
DENY — never allow framing (recommended if you don't embed your own site in iframes).
SAMEORIGIN — only allow framing from the same domain (useful if you use iframes internally).
Being superseded by CSP's
frame-ancestors directive, but still valuable for older browser compatibility.
text/plain.Attack scenario: A user uploads an image file that contains JavaScript code. The server stores it and serves it as
image/jpeg. Without nosniff, some browsers execute the JS. With nosniff, the browser trusts the declared Content-Type and treats it as an image.This is a single fixed value — there are no other valid options for this header.
Referer header containing the full URL of the page they came from — including any query parameters, tokens, or user IDs in the URL.strict-origin-when-cross-origin — sends the full URL only to same-origin destinations; sends just the origin (domain name, no path) to cross-origin HTTPS destinations; sends nothing to HTTP destinations. A sensible default for most sites.
Alternatives:
no-referrer (nothing sent, most private), same-origin (nothing sent to other domains).
camera=() — deny camera access entirely (empty parentheses = no allowed origins).microphone=() — deny microphone access.geolocation=() — deny geolocation access.For a web hosting/tutorial site like osztromok.com, none of these features are needed — deny them all. Add only what your site actually requires.
http://. Don't add it again if you already set it in the VirtualHost — duplicate headers cause both values to be sent.
How Clickjacking Works
evil.com/prize.html — a page that appears to offer a free prize with a large "Claim Now" button. Hidden behind that button (via CSS opacity: 0) is an iframe loading osztromok.com/account/delete. The iframe is perfectly positioned so the attacker's button overlaps your site's "Confirm Delete" button.A logged-in visitor to evil.com clicks what looks like "Claim Now" — they actually click "Confirm Delete" on your site. The request goes through with their session cookie because the browser is genuinely loading your site, just invisibly.
X-Frame-Options: DENY prevents your site from loading inside any iframe at all — the attack fails at the first step.
Header always set X-Frame-Options "SAMEORIGIN" (or DENY if you never use iframes)Content Security Policy (CSP)
CSP is the most powerful security header — and the most complex. It defines a whitelist of trusted sources for every type of resource your page can load: scripts, stylesheets, images, fonts, API calls. The browser blocks anything that doesn't match the policy. A properly configured CSP makes XSS (cross-site scripting) attacks far harder to exploit — even if an attacker manages to inject HTML, the injected scripts won't run.
CSP Directives
'self''unsafe-inline' — it defeats much of CSP's XSS protection.<style> tags require 'unsafe-inline' or a hash/nonce. Google Fonts requires adding https://fonts.googleapis.com.data: allows base64-embedded images. Add CDN domains if you load images from third-party services.https://fonts.gstatic.com.frame-ancestors 'none' = DENY. More powerful — X-Frame-Options only controls one level of nesting.'self' prevents injected forms from submitting to attacker-controlled servers (data exfiltration via form).<base> element, which can redirect all relative URLs. 'self' or 'none' prevents base tag injection attacks.CSP Source Values
Green = safe and recommended. Amber = use only where necessary (weaken security). Red = avoid — these open significant holes.
Deploying CSP Safely — Report-Only First
Content-Security-Policy-Report-Only instead of Content-Security-Policy. The browser evaluates the policy and logs violations to the browser console (and optionally to a report-uri) but does not block anything. Your site works exactly as before.cdnjs.cloudflare.com, add it to script-src. If Google Analytics appears, add www.google-analytics.com and www.googletagmanager.com to script-src and connect-src.Content-Security-Policy-Report-Only to Content-Security-Policy in Apache. Reload Apache. Test every page of the site — especially any with forms, embedded maps, videos, or third-party widgets.Putting It All Together — Complete VirtualHost Config
All headers go in the *:443 VirtualHost (HTTPS only). The HTTP VirtualHost only handles redirecting to HTTPS — no need for security headers on the redirect response.
'unsafe-inline' is included for script-src and style-src because osztromok.com's lesson pages use a lot of inline CSS and JavaScript. Once you've identified all your legitimate sources via Report-Only violations, you can tighten this by removing 'unsafe-inline' and replacing inline scripts with nonces or hashes.
Switching CSP to Enforcing Mode
Testing Your Headers
Open Chrome or Firefox → F12 → Console tab. Browse every page of your site. CSP violations appear in red:
Two reliable options (no account needed):
• securityheaders.com — grades A+ through F, flags missing or misconfigured headers, shows what each one does.
• observatory.mozilla.org — Mozilla's scanner, checks headers, cookies, redirect chains, and HSTS.
Enter
osztromok.com and aim for B+ or above. An A+ requires a strict CSP with no 'unsafe-inline'.
How Cloudflare Interacts With These Headers
- Your custom security headers pass through unchanged — X-Frame-Options, CSP, X-Content-Type-Options, etc. all arrive at the browser exactly as Apache sent them.
- The Server: header is replaced by Cloudflare — visitors see
Server: cloudflare, notServer: Apache. This is actually better; ServerTokens Prod still matters for direct connections and internal tooling. - Cloudflare can also set security headers via Transform Rules (in the Cloudflare dashboard). If Cloudflare sets a header that Apache also sets, the visitor receives the header twice. This can cause unexpected behaviour — check with
curl -Iand look for duplicate header names. - Cloudflare's Bot Fight Mode and WAF add their own layer of protection before requests reach Apache, complementing your headers rather than replacing them.
curl -I https://osztromok.com | grep -i "x-frame" — you should see the header exactly once.
Troubleshooting
sudo systemctl reload apache2. (2) Syntax error prevented the config from loading — run sudo apache2ctl configtest; fix any errors. (3) Headers are in the wrong VirtualHost — if you put them in the HTTP (*:80) block but are testing HTTPS, they won't appear. Move them to the *:443 block. (4) mod_headers not enabled — check: apache2ctl -M | grep headers; enable with sudo a2enmod headers.
fonts.googleapis.com is blocked, add it to style-src). If many things break at once, temporarily add 'unsafe-inline' to script-src and style-src to get the site working, then identify and whitelist specific sources before removing 'unsafe-inline' again. If you're in a hurry: switch back to Content-Security-Policy-Report-Only to stop blocking while you refine the policy.
.htaccess file — remove from one. (2) Set in both Apache and a Cloudflare Transform Rule — remove one. (3) Set in both the server-level security.conf and the VirtualHost — pick one location and remove the other. Use sudo grep -r "X-Frame-Options" /etc/apache2/ to find every place a header is set.
Server: header is overwritten by Cloudflare — you'll always see Server: cloudflare when curling through the tunnel. ServerTokens Prod still works, but its effect isn't visible via Cloudflare. To verify it's working, test from localhost: curl -I http://localhost — the Server header should show Apache (not the version). Also check: is security.conf actually enabled? apache2ctl -t -D DUMP_INCLUDES shows which conf files are loaded.
<style> tags and style="" attributes are blocked unless 'unsafe-inline' is in style-src. For a site with many lesson pages using inline styles (like osztromok.com), the practical choice is to keep 'unsafe-inline' in style-src — it's less ideal from a CSP perspective but doesn't enable script injection. The security benefit of removing inline style is smaller than removing inline script. Focus your efforts on keeping script-src as tight as possible.
Quick Reference — Chapter 6
| Setting / Header | Recommended value | Protects against |
|---|---|---|
| ServerTokens | Prod | Version disclosure — only "Apache", no version number |
| ServerSignature | Off | Version in error page footers |
| X-Powered-By | unset | PHP / framework version disclosure |
| X-Frame-Options | SAMEORIGIN | Clickjacking via malicious iframes |
| X-Content-Type-Options | nosniff | MIME confusion / uploaded file execution |
| Referrer-Policy | strict-origin-when-cross-origin | URL leakage to third-party sites |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | Browser API abuse via injected scripts |
| Content-Security-Policy | default-src 'self'; ... | XSS, data injection, resource hijacking |
| Command | Purpose |
|---|---|
| curl -I https://osztromok.com | Check all response headers — verify headers are present and not duplicated |
| sudo apache2ctl configtest | Validate Apache config syntax before reload |
| apache2ctl -M | grep headers | Confirm mod_headers is loaded |
| sudo grep -r "Header" /etc/apache2/ | Find all places a header directive is set — hunt for duplicates |
| Browser DevTools → Console | See CSP violation messages while browsing (Report-Only or enforcing) |