A security header becomes useful when you can explain the browser behavior it changes and show that the application still works. A high scanner score alone cannot establish either. This exercise adds two response headers and observes a proposed Content Security Policy on one lab path before considering enforcement.
Prepare a small Apache lab
Use an Apache 2.4 host you administer, with a dedicated staging hostname, valid HTTPS certificate and no existing CSP. The example assumes Debian-style certificate paths, a document root of /var/www/headers-lab, and a real HTTPS reporting collector you control. Replace every example hostname and certificate path. Verify loaded modules with apachectl -M; module installation and service names vary by distribution. Keep your existing TLS settings and certificate renewal process.
Create /var/www/headers-lab/header-lab/index.html with the following harmless inline script. It gives the proposed policy a deliberate violation to detect:
<!doctype html>
<meta charset="utf-8">
<title>Header lab</title>
<p id="status">Inline script has not run.</p>
<script>document.getElementById('status').textContent = 'Inline script ran.';</script>Provision the reporting collector before enabling the reporting directives. It must accept both legacy application/csp-report JSON and Reporting API application/reports+json batches. Limit request size, validate untrusted fields, rate-limit ingestion and redact URLs or query strings that could contain private data. This configuration points to that collector; it does not implement one. If you have no collector, remove report-uri, report-to and Reporting-Endpoints together and use browser console warnings for the local exercise.
Place this virtual host in your server's enabled site configuration, not inside an unrelated site's .htaccess. The header rules apply only to /header-lab/ on this host:
# A dedicated Apache 2.4 lab virtual host, not a global .htaccess rule.
# Listen 80 and Listen 443 must already exist in the server configuration.
# Required loaded modules: ssl_module, headers_module, alias_module,
# authz_core_module, authz_host_module and mime_module.
<VirtualHost *:80>
ServerName headers-lab.example.com
Redirect permanent / https://headers-lab.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName headers-lab.example.com
DocumentRoot /var/www/headers-lab
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/headers-lab.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/headers-lab.example.com/privkey.pem
<Directory /var/www/headers-lab>
Require all granted
AllowOverride None
Options -Indexes
</Directory>
<Location /header-lab/>
Header onsuccess unset X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"
Header onsuccess unset Referrer-Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header onsuccess unset Content-Security-Policy-Report-Only
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; report-uri https://reports.example.net/csp; report-to csp-lab"
Header onsuccess unset Reporting-Endpoints
Header always set Reporting-Endpoints "csp-lab=\"https://reports.example.net/csp\""
</Location>
</VirtualHost>The onsuccess unset plus always set pattern avoids a duplicate value across Apache's two header tables. It deliberately makes this lab configuration the owner of these headers. Do not remove an application's existing CSP to copy this example. Multiple enforcing CSP headers are applied together and can unexpectedly restrict a page.
Check a response and an actual page
Run the syntax check first. Reload the server only if it succeeds, using your distribution's service command. Then request a GET response rather than assuming HEAD follows the same application path:
apachectl configtest
curl -sS -D - -o /dev/null https://headers-lab.example.com/header-lab/
curl -sS -D - -o /dev/null https://headers-lab.example.com/header-lab/missing.htmlIllustrative expected output, not an observed test result: Syntax OK from Apache; one X-Content-Type-Options: nosniff, one Referrer-Policy: strict-origin-when-cross-origin, and one Content-Security-Policy-Report-Only field on both the successful page and the missing-file response. Header names may be lowercase under HTTP/2. An unexpected second value means another application, virtual host or CDN rule still owns the header.
Open the lab URL in a normal browser and inspect Developer Tools, Console and Network. The paragraph should change to Inline script ran. while the console describes a report-only script-policy violation. Exact wording varies. The script running is intentional: report-only policy observes without blocking. An existing enforcing policy or extension could change this result.
The example includes report-to with Reporting-Endpoints and the deprecated report-uri fallback for browsers without the newer reporting support. A browser supporting report-to can ignore report-uri. Delivery can be delayed or absent, so confirm collector receipt separately; an empty dashboard does not prove compliance. Report-only cannot enforce framing protection, and its sandbox directive is ignored.
Interpret failures before broadening the policy
- No headers: confirm the hostname selects this virtual host, the path matches, modules loaded and any CDN forwards the fields. Test the visitor-facing response too.
- Broken assets after nosniff: inspect the response's actual MIME type. Fix a JavaScript file served as HTML rather than removing the header.
- Reports but no visible breakage: expected in report-only mode. Move required inline code to a same-origin file, or design per-response nonces or hashes for a real application. Do not solve the lab by adding unrestricted inline execution.
- A report endpoint returns 404 or rejects JSON: repair the collector or its format support. That is a reporting failure, not evidence that the policy is safe.
Before enforcement, test login, payment redirects, embeds, error pages and mobile flows against their real resource inventory. Then replace the report-only header with an enforcing policy on staging and repeat the journeys. For this static lab, move the script into /header-lab/status.js and load it with src first. Choose frame-ancestors to match actual embedding needs. Stage HSTS separately using the HSTS rollout exercise.
These checks establish response configuration and one script behavior. They cannot prove authorization, input handling or every page is secure. Keep a route list and record the response values after server or CDN changes.