Content Security Policy in Apache (.htaccess)
A Content Security Policy locks down which resources the browser is allowed to load on your Apache-served website. This guide walks through the minimal, working deployment: enable mod_headers, drop the header into .htaccess, test in report-only mode, then switch to enforcing. Five minutes end to end once mod_headers is enabled.
Prerequisites: enable mod_headers
Apache's mod_headers module is what lets .htaccess rewrite response headers. On Debian/Ubuntu derivatives:
sudo a2enmod headers
sudo systemctl restart apache2
On RHEL/CentOS/Fedora, open /etc/httpd/conf/httpd.conf and make sure this line is present and uncommented:
LoadModule headers_module modules/mod_headers.so
Restart Apache afterwards. You can verify the module is loaded with apachectl -M | grep headers.
The minimal .htaccess snippet
Open the .htaccess file at your document root (create it if it doesn't exist) and add:
<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
</IfModule>
A few things to note:
<IfModule mod_headers.c>guards the directive so Apache doesn't error out if the module isn't loaded.Header always set— thealwaysflag is critical. Without it, Apache only sets the header on 2xx responses. That means 404 and 500 error pages would have no CSP, creating an XSS hole on error pages.Content-Security-Policy-Report-Only— the browser logs violations to the console but doesn't block anything yet. This is the safe way to roll out.
Test it
Reload your site, open browser DevTools (F12) → Console. You'll see red warnings for every resource that would be blocked, like:
[Report Only] Refused to load the script 'https://cdn.example.com/analytics.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'".
Each of these is a real resource your site loads that isn't covered by the current policy. You have two options: add the domain to the corresponding directive, or remove the dependency. Keep iterating until the console is clean.
.htaccess.
Switch to enforcing mode
Once the console is quiet, change the header name from Content-Security-Policy-Report-Only to Content-Security-Policy and reload:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
</IfModule>
That's it. Your site is now enforcing a CSP. Any violation from now on is actually blocked by the browser, not just reported.
Common pitfalls
"mod_headers is not loaded"
If you see this in the Apache error log, you skipped the prerequisite. Run sudo a2enmod headers && sudo systemctl restart apache2 (Debian/Ubuntu) or uncomment the LoadModule line in httpd.conf.
The header shows up but the policy is ignored
Check whether there's a second CSP header being set somewhere else (by PHP, a virtual host file, or another .htaccess). If multiple Content-Security-Policy headers are sent, browsers enforce the intersection — which is often more restrictive than intended. Use curl -I https://your-site.example to see all headers. If you find duplicates, keep only the one in .htaccess and delete the others.
WordPress/PHP apps require 'unsafe-inline'
Most CMS and PHP frameworks emit inline <script> and <style> blocks. Without 'unsafe-inline' in script-src and style-src, the site breaks. The stricter alternative is to use nonces or hashes, but that requires code changes — see the WordPress guide for details.
CORS and CSP are different things
A common confusion: if your site gets blocked loading resources from another domain, that might be CORS (Access-Control-Allow-Origin) rather than CSP. CSP tells the browser what your page is allowed to load; CORS tells the other server whether it's willing to be loaded by your page. Both can block resources but the fix is different.
Hot-reloading development servers
Webpack dev server, Vite, Next.js in dev mode — they all inject inline scripts for hot reloading. If your CSP blocks them, your dev environment breaks. Solution: deploy the strict CSP only in production. In Apache you can do this via SetEnvIf or simply by keeping .htaccess production-only.
Apache-specific edge cases
.htaccess performance
Every directive in .htaccess is re-evaluated on every request. For a single Header set the overhead is negligible, but if you want maximum performance move the directive to the virtual host block in /etc/apache2/sites-enabled/your-site.conf and disable .htaccess entirely with AllowOverride None. Same syntax, no per-request cost.
Using Header append vs. Header set
Always use set, never append. append would concatenate multiple CSP values, which browsers treat as the intersection — usually not what you want.
Reporting endpoints
Once you move to enforcing mode, consider adding a report-uri directive so you can see violations that happen in the wild (not just in your own testing):
Header always set Content-Security-Policy "default-src 'self'; ... ; report-uri https://your-reporting-endpoint.example/csp"
Services like report-uri.com offer free tiers; you can also self-host a collector.
Generate your exact CSP in 30 seconds → We crawl your site, find every resource, and produce a tight .htaccess block for your real stack — not a guess