Article

How to Add Security Headers in Apache

Add HTTP security headers in Apache with mod_headers using Header always set directives in your VirtualHost or .htaccess, then reload Apache. Here are copy-paste values for HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy.

By Paul Rudenko, Security ResearcherUpdated Jun 26, 20267 min read

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 apache2

On 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.

Use 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.

Frequently asked questions

How do I add security headers in Apache?

First enable the mod_headers module, which provides the Header directive. On Debian or Ubuntu run sudo a2enmod headers, then reload Apache with sudo systemctl reload apache2; on Red Hat-based systems the module is usually loaded already. Next, add Header always set directives either inside the relevant <VirtualHost> block of your site configuration (the recommended place) or in an .htaccess file in your document root if AllowOverride permits it. A typical set includes Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy and Content-Security-Policy. After saving, reload Apache so the changes take effect, then verify with curl -I that the headers appear in the response, including on error pages.

Header set vs Header always set — which should I use?

Use Header always set for security headers. The difference is which responses the header is attached to. Plain Header set only modifies successful responses on the normal output filter, which in practice means it can be skipped on internally generated error responses such as 404 Not Found and 500 Internal Server Error. Adding the always keyword places the directive on the error-handling filter as well, so the header is present on every response the server sends, not just the 2xx ones. Because attackers and scanners frequently probe error pages, you want your protections present there too. There is no real downside to always for these headers, so it should be your default for HSTS, nosniff, frame options, and the rest.

Can I use .htaccess for security headers?

Yes, but only if the server allows it. An .htaccess file in your document root can contain the same Header always set directives you would put in a VirtualHost, which is useful on shared hosting where you cannot edit the main configuration. However, Apache ignores .htaccess directives unless the directory is covered by an AllowOverride setting that permits them, such as AllowOverride FileInfo or AllowOverride All; with AllowOverride None they do nothing. .htaccess also adds a small per-request overhead because Apache re-reads the file on each request, and it depends on the file staying in the web root. When you control the main configuration, putting the directives in the VirtualHost is faster and more robust.

Why are my Apache headers not appearing?

The most common cause is that mod_headers is not enabled — without it every Header directive is silently ignored, so confirm it with apachectl -M | grep headers. The second is forgetting to reload Apache after editing the config; changes do not apply until you run systemctl reload apache2. If headers appear on normal pages but vanish on errors, you used Header set instead of Header always set. If you put the directives in .htaccess and nothing happens, AllowOverride probably does not permit them, so move them to the VirtualHost. Finally, check that the directives are inside the VirtualHost that actually serves the request, and that no later configuration or proxy is unsetting them.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →