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:

Report-Only mode: test without breaking anything

The snippet above sets Content-Security-Policy-Report-Only rather than Content-Security-Policy, and that one word is what makes a CSP rollout safe. In report-only mode the browser evaluates the policy exactly as it would when enforcing and logs every violation — then loads the resource anyway. Nothing breaks while you are still working out what the policy needs to allow.

The difference is the header name, nothing else:

# Report-Only — violations are logged, nothing is blocked
Header always set Content-Security-Policy-Report-Only "default-src 'self'"

# Enforcing — violations are blocked
Header always set Content-Security-Policy "default-src 'self'"

Both headers can be sent at the same time, and for an existing site that is the better way to tighten a policy: keep the current, working one in Content-Security-Policy and test the stricter candidate in Content-Security-Policy-Report-Only next to it. Visitors keep the protection they already have while you collect violations for the stricter version.

Reading the console output

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.

Faster approach: run our free scanner on your site. It crawls every page, finds all external resources, and generates the exact CSP you need — no trial and error. You can paste the result directly into .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

VirtualHost instead of .htaccess

If you have root access, the virtual host file is the better place for the header. Every directive in .htaccess is re-read and re-evaluated on every single request; a directive in the VirtualHost block is parsed once at startup.

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/example

    <IfModule mod_headers.c>
        Header always set Content-Security-Policy "default-src 'self'"
    </IfModule>

    # Optional, once no .htaccess is needed any more:
    <Directory /var/www/example>
        AllowOverride None
    </Directory>
</VirtualHost>

The file usually lives in /etc/apache2/sites-available/your-site.conf (Debian/Ubuntu) or /etc/httpd/conf.d/your-site.conf (RHEL/Fedora). Apache needs a reload afterwards — sudo systemctl reload apache2 — whereas an .htaccess change takes effect immediately. That is the trade-off: shared hosting leaves you with .htaccess, your own server makes the VirtualHost the cleaner choice.

The syntax of the header itself is identical in both places. Everything on this page applies either way.

.htaccess performance

For a single Header set the per-request overhead is negligible — this is not a reason to avoid .htaccess. It matters when a site accumulates dozens of rewrite rules and conditions over the years, because Apache walks the file for every request, in every directory up the tree.

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