Security Headers for Apache: Complete .htaccess and httpd.conf Guide
Step-by-step Apache security headers configuration for .htaccess and httpd.conf. Covers CSP, HSTS, X-Frame-Options, and more - with copy-paste snippets for Apache 2.4.
Security Headers for Apache: Complete .htaccess and httpd.conf Guide
Apache HTTP Server is still the most widely deployed web server in the world, and yet security header configuration on Apache is frequently incomplete. The default Apache installation sends none of the headers that modern browsers rely on for protection against XSS, clickjacking, MIME sniffing, and protocol downgrade attacks.
This guide gives you complete, copy-paste configurations for every important security header - both via .htaccess (for shared hosting environments) and httpd.conf (for servers you control directly). Scan your domain on SecHead to see your current grade before and after applying these changes.
Prerequisites
Enable mod_headers
Security headers in Apache are set using the mod_headers module. Confirm it's enabled:
apache2ctl -M | grep headers
# or
httpd -M | grep headers
If it's not listed, enable it:
# Debian/Ubuntu
sudo a2enmod headers
sudo systemctl restart apache2
# RHEL/CentOS/Fedora
# Edit /etc/httpd/conf.modules.d/00-base.conf and uncomment:
# LoadModule headers_module modules/mod_headers.so
sudo systemctl restart httpd
Enable mod_rewrite (for HTTPS redirect)
sudo a2enmod rewrite
sudo systemctl restart apache2
The Complete Security Headers Block
For .htaccess (shared hosting / per-directory)
# -------------------------------------------------------
# Security Headers - SecHead recommended configuration
# Apache 2.4 / .htaccess
# -------------------------------------------------------
# Prevent MIME-type sniffing
Header always set X-Content-Type-Options "nosniff"
# Clickjacking protection
Header always set X-Frame-Options "DENY"
# HTTPS enforcement (only set on HTTPS sites)
# max-age=31536000 = 1 year; add includeSubDomains if all subdomains use HTTPS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Control referrer information sent in requests
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Restrict browser feature access
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
# Content Security Policy - adjust sources to match your actual dependencies
Header always set Content-Security-Policy "default-src 'none'; 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';"
# Remove server fingerprinting headers
Header always unset X-Powered-By
Header always unset Server
For httpd.conf or a VirtualHost block
<VirtualHost *:443>
ServerName example.com
# ... SSL configuration ...
# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Header always set Content-Security-Policy "default-src 'none'; 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 unset X-Powered-By
Header always unset Server
</VirtualHost>
Header-by-Header Breakdown
Strict-Transport-Security (HSTS)
HSTS tells browsers to only connect to your site over HTTPS - even if the user types http://. Once a browser sees this header, it will automatically upgrade every future request without a round-trip to the server.
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Important: Only add this header to your HTTPS VirtualHost, never the HTTP one. If you add it to port 80, browsers may lock users out if your SSL certificate expires.
Force the HTTP → HTTPS redirect first:
<VirtualHost *:80>
ServerName example.com
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
For more detail, see the complete HSTS guide.
Content-Security-Policy
CSP is the most powerful - and most complex - header. The snippet above uses a strict default-src 'none' baseline. You'll need to expand it to match your actual site:
If you load scripts from a CDN:
Header always set Content-Security-Policy "default-src 'none'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none';"
If you use inline scripts (requires nonces via PHP/server-side generation):
<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic'; style-src 'self'; ...");
?>
<script nonce="<?= htmlspecialchars($nonce) ?>">
// your inline script
</script>
See the CSP explained guide for the full breakdown of directives.
X-Frame-Options
Prevents your pages from being embedded in iframes on other domains, blocking clickjacking attacks.
Header always set X-Frame-Options "DENY"
Use SAMEORIGIN instead of DENY if your own site uses iframes internally (e.g., an embedded payment widget within the same domain).
Note: Modern CSP frame-ancestors supersedes X-Frame-Options. Include both for maximum compatibility:
Header always set X-Frame-Options "DENY"
# frame-ancestors is already in the CSP block above
X-Content-Type-Options
Prevents the browser from "sniffing" the MIME type of a response. Without this, a browser might interpret a .jpg upload that contains HTML as a valid HTML page - opening a vector for XSS via malicious file uploads.
Header always set X-Content-Type-Options "nosniff"
This is the simplest header to add and has zero compatibility risk.
Referrer-Policy
Controls how much of the current page URL is sent in the Referer header when a user clicks a link to another site.
Header always set Referrer-Policy "strict-origin-when-cross-origin"
This setting sends the full URL within your own site (useful for analytics) but only sends the domain (not the path or query string) to external sites. This prevents leaking sensitive URL parameters (session IDs, private tokens) to third parties.
Permissions-Policy
Restricts which browser APIs your site is permitted to use. Useful for preventing third-party scripts (ads, analytics, embeds) from accessing cameras, microphones, or location data you didn't intend to grant.
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
Adjust the list based on features your site actually uses - if you have a contact form with geolocation, you'd change geolocation=() to geolocation=(self).
Removing Fingerprinting Headers
Apache by default sends Server: Apache/2.4.x (Ubuntu) and may send X-Powered-By from PHP. These tell attackers exactly what software version you're running, making targeted exploit searches easier.
Remove from headers
Header always unset Server
Header always unset X-Powered-By
Suppress the Server version in httpd.conf
ServerTokens Prod
ServerSignature Off
ServerTokens Prod reduces the Server header value to just Apache with no version. ServerSignature Off removes the version footer from Apache-generated error pages.
Testing Your Configuration
Syntax check before restarting
# Apache 2.4 on Debian/Ubuntu
apache2ctl configtest
# RHEL/CentOS
httpd -t
Fix any Syntax error output before restarting. A broken config will take your server down.
Reload (not restart) for zero-downtime application
sudo systemctl reload apache2
# or
sudo apachectl graceful
Verify headers are being sent
curl -I https://example.com
You should see your new headers in the output. Then run a full scan on SecHead to get your grade and confirm nothing is missing.
Common Issues
Headers not appearing on static files
If your static files are served directly by Apache without going through PHP or another handler, verify that mod_headers is loaded and that your .htaccess is in the correct directory. The Header always set directive (with always) ensures headers are added even on error responses.
HSTS locking you out
If you accidentally set HSTS on an HTTP-only server or a server with an invalid certificate, browsers will refuse to connect. Recovery requires the user to manually clear HSTS state from their browser's security settings. Always test on staging before applying HSTS to production.
CSP blocking legitimate resources
Use Content-Security-Policy-Report-Only first (same syntax, different header name) to test your policy without blocking anything. This sends violations to your console without breaking the page.
Full Reference Configuration
# -------------------------------------------------------
# Complete Apache Security Headers - copy-paste ready
# Tested on Apache 2.4 with mod_headers enabled
# -------------------------------------------------------
# HTTPS redirect (HTTP VirtualHost)
<VirtualHost *:80>
ServerName example.com
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
# Main secure site (HTTPS VirtualHost)
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
# Security headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Header always set Content-Security-Policy "default-src 'none'; 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';"
# Remove fingerprinting headers
Header always unset X-Powered-By
Header always unset Server
DocumentRoot /var/www/example.com
</VirtualHost>
# httpd.conf additions
ServerTokens Prod
ServerSignature Off
After applying this configuration, run a SecHead scan on your domain to confirm your grade. For the equivalent configuration on other platforms, see the security headers for Next.js guide and the WordPress security headers guide. For the full explanation of what each header does and why it matters, the complete security headers checklist covers every header in one place.
Related articles
Free tool
Check your own security headers
Instant grade, plain-language explanations, and a full remediation plan - no signup needed.
Scan your site now →