How to Set Security Headers in Apache

A complete guide to configuring all seven essential HTTP security headers in Apache using mod_headers, including VirtualHost and .htaccess approaches.


Apache uses the mod_headers module to add, modify, and remove HTTP response headers. This guide shows how to configure all seven essential security headers — HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and X-XSS-Protection — using both the VirtualHost configuration and .htaccess files.

Prerequisites

  • Apache 2.4+ installed and serving your site over HTTPS.
  • The mod_headers module enabled (see below).
  • A valid SSL certificate (required before enabling HSTS).
  • Access to the server configuration or .htaccess files.

Step 1: Enable mod_headers

On Debian/Ubuntu systems, enable the module and restart Apache:

bash
sudo a2enmod headers sudo systemctl restart apache2

On RHEL/CentOS systems, mod_headers is typically compiled in or loaded via the main configuration. Verify it is active:

bash
httpd -M | grep headers

Step 2: Add Headers to Your VirtualHost

The recommended approach is to add headers directly to the <VirtualHost> block in your site configuration. Open the relevant file (usually in /etc/apache2/sites-available/ or /etc/httpd/conf.d/):

apache
<VirtualHost *:443> ServerName example.com ServerAlias www.example.com SSLEngine on SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem DocumentRoot /var/www/example.com # ── Security Headers ────────────────────────────────────────────────── <IfModule mod_headers.c> # Force HTTPS for 1 year; include subdomains; opt into preload lists Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" # Restrict resource loading; adjust script-src for any CDNs you use Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" # Prevent clickjacking Header always set X-Frame-Options "DENY" # Prevent MIME-type sniffing Header always set X-Content-Type-Options "nosniff" # Send origin only on same-site requests; nothing on cross-site downgrades Header always set Referrer-Policy "strict-origin-when-cross-origin" # Block sensitive browser features from the page and iframes Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=()" # Disable the defunct IE/Chrome XSS Auditor Header always set X-XSS-Protection "0" </IfModule> # ── End Security Headers ────────────────────────────────────────────── <Directory /var/www/example.com> Options -Indexes AllowOverride None Require all granted </Directory> </VirtualHost> <VirtualHost *:80> ServerName example.com ServerAlias www.example.com # Redirect all HTTP to HTTPS RewriteEngine On RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] </VirtualHost>

Alternative: Using .htaccess

If you do not have access to the VirtualHost configuration (e.g., on shared hosting), you can add headers in a .htaccess file in your document root. This requiresAllowOverride FileInfo or AllowOverride All to be set for the directory.

apache
# .htaccess <IfModule mod_headers.c> Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" Header always set X-Frame-Options "DENY" Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=()" Header always set X-XSS-Protection "0" </IfModule>
.htaccess PerformanceApache reads .htaccess files on every request unless AllowOverride None is set. For production sites, prefer setting headers in the VirtualHost configuration and use AllowOverride None to disable .htaccess parsing entirely.

Header-by-Header Explanation

Strict-Transport-Security (HSTS)

Instructs browsers to always use HTTPS when connecting to your domain. The max-age is in seconds (31536000 = 1 year). Add includeSubDomains only after confirming all subdomains serve valid HTTPS. Add preload only when committed to HTTPS permanently — inclusion in browser preload lists is difficult to reverse.

Content-Security-Policy (CSP)

Defines which origins can supply scripts, styles, images, and other resources. The example policy is conservative. Expand script-src to add any CDNs or analytics services your site uses. Test with Content-Security-Policy-Report-Only first.

Test CSP in Report-Only ModeReplace Content-Security-Policy with Content-Security-Policy-Report-Only during development. Violations are logged to the browser console but resources are not blocked, letting you tune the policy safely.

X-Frame-Options

Prevents your pages from being loaded inside iframes on other domains, blocking clickjacking attacks. DENY blocks all framing. Use SAMEORIGIN if your application legitimately embeds its own pages in iframes.

X-Content-Type-Options

Forces the browser to respect the declared Content-Type and disables MIME sniffing. Prevents an attacker from uploading a file (e.g., an image) that is actually a script and having the browser execute it.

Referrer-Policy

Controls how much URL information is included in the Referer header on navigation. strict-origin-when-cross-origin protects sensitive URL paths from leaking to third-party sites while still providing referrer data for same-site analytics.

Permissions-Policy

Limits which browser APIs (camera, microphone, geolocation, etc.) can be accessed by your page and any embedded iframes. Blocking features you do not use prevents a compromised third-party script or embed from silently accessing them.

X-XSS-Protection

Set to 0 to disable the deprecated XSS Auditor in older browsers. Chrome removed the auditor in 2019; Safari removed it in 2022. Sending 0 prevents any lingering IE11 instances from running the legacy (and potentially exploitable) filter.

Step 3: Test and Reload

bash
# Test configuration for syntax errors sudo apachectl configtest # Reload Apache to apply changes sudo systemctl reload apache2 # Verify headers with curl curl -I https://example.com

Or use the ShowDNS Security Headers Scanner to check all headers at once and get a graded report.

Frequently Asked Questions

Why wrap headers in IfModule mod_headers.c?

The <IfModule> wrapper prevents Apache from failing to start if mod_headers is not loaded. On well-maintained servers where the module is always present, it is optional but harmless.

What does Header always mean?

The always condition tells Apache to set the header regardless of the HTTP response status — including error responses like 404 or 500. Without always, headers are only added to successful (2xx) responses.

Can I set different headers for different directories?

Yes. Wrap header directives in a <Directory>, <Location>, or <Files> block to apply them selectively. More specific contexts override less specific ones for the same header name.

Related Articles