Skip to content

Set Apache security headers correctly

Severity: mediumApplies to: Apache httpd 2.4.xApplies to: Apache httpd 2.4.68
The fixconf-available/security-headers.conf
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "frame-ancestors 'self'"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Terminal window
sudo a2enmod headers
sudo a2enconf security-headers
sudo apachectl -t && sudo systemctl reload apache2

Read the HSTS warning in the safety section before deploying that last line.

Without always, Header set writes to Apache’s onsuccess table, which applies only to successful responses. Your 404s and 500s ship no security headers at all.

Header set X-Content-Type-Options "nosniff" # not on error pages
Header always set X-Content-Type-Options "nosniff" # on everything

Error pages are where reflected content and stack traces show up, so they are precisely where nosniff and a frame policy matter. Use always on every security header, every time. There is no downside.

Apache does not have nginx’s inheritance trap

Section titled “Apache does not have nginx’s inheritance trap”

Worth stating plainly, because it’s the first thing anyone arriving from nginx will worry about.

In nginx, a single add_header in a location block silently discards every inherited header. Apache’s mod_headers does not work that way: directives from the server config, the vhost, and <Directory> sections accumulate. Adding a header in a subdirectory does not delete the ones set above it.

So the nginx failure mode — one path quietly serving no security headers — does not happen here. That’s a genuine point in Apache’s favour and one less thing to audit.

The trap Apache has instead is Header unset and Header set on the same name: later directives can replace earlier values, and the processing order (server → vhost → directory → .htaccess) means a .htaccess can override your carefully set header — which is one more reason for AllowOverride None.

Every Apache hardening guide still recommends this:

Header always set X-XSS-Protection "1; mode=block" # don't

It is deprecated, and not harmlessly. The header enabled a browser XSS filter that researchers showed could be turned against the page it was protecting: a crafted URL could make the filter misidentify a legitimate script as an attack and selectively disable it — letting an attacker switch off a page’s own security scripts. MDN states it “can create XSS vulnerabilities in otherwise safe websites”.

Browsers removed the filter. The header does nothing current and it is a documented footgun. Delete the line; a real Content-Security-Policy is the replacement.

frame-ancestors in CSP is the standard and is strictly more capable — it takes a source list rather than one origin, and covers <frame>, <iframe>, <embed> and <object>.

Keeping X-Frame-Options: SAMEORIGIN alongside is harmless for old browsers. Just don’t set it instead, and don’t let the two disagree — frame-ancestors wins where it’s supported, which will confuse whoever debugs it later.