Apache is one of the most widely deployed web servers, and adding HTTP security headers to it is straightforward once you know where the directives belong. Headers let the browser enforce stronger defaults: forcing HTTPS, restricting where scripts may load from, and limiting how much information leaks in the Referer header. This guide walks through enabling the right module, choosing between a VirtualHost and an .htaccess file, a complete copy-paste header block, how to roll out Content-Security-Policy safely, and how to verify the result.
How do I enable mod_headers?
Security headers in Apache are set with the Header directive, which is provided by the mod_headers module. On most distributions this module ships with Apache but is not enabled by default. On Debian and Ubuntu you enable it and reload Apache like this:
# Enable mod_headers (Debian / Ubuntu)
sudo a2enmod headers
sudo systemctl reload apache2On Red Hat, CentOS, Rocky, and AlmaLinux the module is usually compiled in or loaded by a line in /etc/httpd/conf.modules.d/. You can confirm it is active with apachectl -M | grep headers, which should print headers_module (shared). If the module is not loaded, every Header directive you write is silently ignored — Apache will not throw an error, your headers simply will not appear.
VirtualHost vs .htaccess
There are two common places to put header directives, and the choice matters. The recommended location is inside the <VirtualHost> block of your site configuration, typically a file under /etc/apache2/sites-available/ or /etc/httpd/conf.d/. Directives placed there are read once when Apache starts, apply to the whole site, and cannot be tampered with by anyone who can write files in the document root.
The alternative is an .htaccess file inside your web root. This is convenient on shared hosting where you do not control the main configuration, but it only works if the server has been configured with AllowOverride set to allow it (for example AllowOverride FileInfo or AllowOverride All). It also adds a small per-request cost because Apache re-reads the file on every request. When you can edit the main configuration, prefer the VirtualHost.
Header always set, not plain Header set. Plain Header set only applies to successful (2xx) responses, so error pages such as 404 and 500 ship without your security headers. Adding always attaches them to error responses too. Two more gotchas: .htaccess directives are ignored unless AllowOverride permits them, so on many servers you must use the VirtualHost instead; and if mod_headers is not enabled, all of these directives are silently ignored with no warning.The headers
Below is a complete block you can paste inside a <VirtualHost> (or into .htaccess if overrides are allowed). Each line is commented so you can see what it does and adjust it for your site. Read the Content-Security-Policy section before you uncomment the last line — a strict CSP can break a working site if rolled out without testing.
<VirtualHost *:443>
ServerName example.com
# ... your existing TLS and DocumentRoot config ...
# Force HTTPS for 2 years and cover subdomains.
# Only add this once you are confident every subdomain serves HTTPS.
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
# Stop the browser from MIME-sniffing a response away from its declared type.
Header always set X-Content-Type-Options "nosniff"
# Disallow this site from being framed (clickjacking protection).
Header always set X-Frame-Options "DENY"
# Send only the origin when navigating to another site; full URL same-origin.
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Disable powerful features the site does not use.
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
# Content-Security-Policy — START IN REPORT-ONLY (see next section).
# Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'"
</VirtualHost>A few notes. Strict-Transport-Security (HSTS) tells the browser to use HTTPS for the configured max-age window; start with a shorter value such as a few days if you are unsure, then raise it. X-Frame-Options DENY blocks all framing — use SAMEORIGIN instead if you embed your own pages in frames. frame-ancestors in CSP is the modern equivalent and supersedes X-Frame-Options in browsers that support it.
The recommended value for each header tracks the OWASP Secure Headers Project, which maintains the canonical best-practice configuration.
How do I roll out CSP safely?
Content-Security-Policy is the most powerful header here and also the easiest one to break a site with. A policy like default-src 'self' blocks every inline script, every third-party widget, and every external stylesheet that is not explicitly allowed. Rather than guess, deploy it in report-only mode first. The browser then evaluates the policy and reports what it would have blocked without actually blocking anything:
# Observe violations without enforcing — safe to ship to production.
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report"Watch the reports for a few days, add the sources you genuinely need to the relevant directives, and only then switch the header name from Content-Security-Policy-Report-Only to Content-Security-Policy to begin enforcement. Building a good policy is a topic on its own — see the dedicated Content-Security-Policy guide for directive-by-directive recommendations, nonces, and hashes.
How do I verify my headers?
After reloading Apache, confirm the headers are present. The quickest check is curl with the -I flag, which fetches only the response headers:
curl -I https://example.com
# Look for lines like:
# strict-transport-security: max-age=63072000; includeSubDomains
# x-content-type-options: nosniff
# x-frame-options: DENY
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: camera=(), microphone=(), geolocation=()If a header is missing, recheck that mod_headers is loaded, that you used Header always set, and that you reloaded the server after editing. Remember to test an error page too (for example a URL that returns 404) to confirm the always keyword is doing its job. For a continuous external view of how your headers grade, run them through the scanner below.
Reloaded Apache? See how your headers score from the outside.
Check your security headers →Where to go next
This page covers the Apache mechanics. For the full reasoning behind each header and recommended values across all servers, read the complete security headers guide. If you also run Nginx — for example as a reverse proxy in front of Apache — the same headers are configured differently there; see how to add security headers in Nginx.