A continuation of Janady13/crawler-signal-reference / framework-http-security-headers.md. The complete source identity, every recorded occurrence, and linked equations are on its first page.
Source transcript · continued
Download complete source text ↓2,397 lines · 104.7 KB
For sites that need to open cross origin popups (OAuth flows, payment processors):
```nginx
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
```
The popup can still communicate back via `window.opener.postMessage`, which is what OAuth flows need.
### 13.4 How To Verify
```bash
# 1. Confirm header
curl -sI https://example.com/ | grep -i cross-origin-opener-policy
# 2. Test in browser console:
# Open a popup: window.open("https://example.com", "_blank")
# In the popup: window.opener
# Expected: null when COOP is same-origin
# 3. Verify cross origin isolation status (combined with COEP):
# window.crossOriginIsolated
# Expected: true when COOP same-origin AND COEP require-corp/credentialless
```
### 13.5 Troubleshooting
**Symptom: OAuth popup completes auth but cannot communicate back.**
COOP is `same-origin`. The popup cannot reach `window.opener`.
Fix: change to `same-origin-allow-popups`.
**Symptom: window.crossOriginIsolated is false despite COEP plus COOP.**
Possible causes:
1. COOP is `unsafe-none` or `same-origin-allow-popups` instead of `same-origin`.
2. The document was opened by a cross origin window (COOP cannot retroactively isolate).
3. An iframe in the document is cross origin without proper headers.
**Symptom: window.opener is null in a popup that used to work.**
COOP enforcement is in place. The popup must be from same origin or use postMessage with explicit origin checks.
### 13.6 How To Fix Common Breakage
**Case: OAuth or social login broken after COOP enforcement.**
Use `same-origin-allow-popups`:
```nginx
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
```
The OAuth popup can communicate back via postMessage.
**Case: Need full cross origin isolation but OAuth must work.**
Move OAuth flow to a separate domain that does not have strict COOP, then transfer the auth result via secure cookie back to the isolated domain.
---
## 14. THE CROSS ORIGIN ISOLATION PATTERN (COEP PLUS COOP PLUS CORP)
### 14.1 What It Achieves
Cross origin isolation unlocks browser APIs that were disabled after Spectre:
* `SharedArrayBuffer`: shared memory between threads.
* `performance.now()` with high precision (nanoseconds instead of millisecond resolution).
* `performance.measureUserAgentSpecificMemory()`.
* WebAssembly threading.
These features are required for sophisticated WebAssembly applications, scientific computing, video editing, and gaming in the browser.
### 14.2 The Three Header Requirement
To achieve cross origin isolation, all three must be true:
1. **COOP: same-origin** on the document.
2. **COEP: require-corp** OR **COEP: credentialless** on the document.
3. **Every cross origin resource has CORP or CORS** allowing this document.
Verify with `window.crossOriginIsolated` in the console.
### 14.3 Full Configuration
```nginx
server {
listen 443 ssl;
server_name webapp.example.com;
# ===== Cross origin isolation =====
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "credentialless" always;
# ===== CORP on all served resources =====
location ~* \.(jpg|jpeg|png|webp|svg|woff2|css|js|wasm)$ {
add_header Cross-Origin-Resource-Policy "same-origin" always;
# ... caching etc ...
}
# ===== Permissions-Policy must NOT block cross-origin-isolated =====
# Default is allow; just ensure we do not block it
# add_header Permissions-Policy "cross-origin-isolated=(self), camera=(), ..." always;
}
```
### 14.4 How To Verify Cross Origin Isolation
```bash
# 1. Confirm all three headers
curl -sI https://webapp.example.com/ | grep -iE "cross-origin-(opener|embedder|resource)-policy"
# 2. In browser DevTools console on the document:
# window.crossOriginIsolated
# Expected: true
# 3. Verify SharedArrayBuffer is available:
# typeof SharedArrayBuffer
# Expected: "function"
# 4. Verify high precision performance.now:
# performance.now()
# Expected: many decimal places of precision (not capped at millisecond resolution)
```
### 14.5 Rollout Strategy
Cross origin isolation is invasive. Rolling it out site wide breaks anything that loads cross origin resources without CORP.
**Recommended approach:**
1. Scope cross origin isolation to a specific subdomain (e.g. `webapp.example.com`) rather than the whole site.
2. Self host every resource that subdomain needs.
3. Verify in DevTools that no resources are blocked.
4. Confirm `window.crossOriginIsolated === true`.
5. Only then enable the isolated APIs in code.
Most Bubbles client sites do not need cross origin isolation and should not enable COEP/COOP. Apply only to sites that explicitly require SharedArrayBuffer or related APIs.
---
## 15. HOW THESE HEADERS INTERACT
The nine headers form a layered system. Several specific interactions matter.
### 15.1 X-Frame-Options vs CSP frame-ancestors
Both control framing. Modern browsers honor frame-ancestors when both are present and conflict. Older browsers honor X-Frame-Options. Best practice: set both with consistent values.
### 15.2 CSP and Permissions-Policy
CSP controls resource loading. Permissions-Policy controls capability access. They are orthogonal. A page can have a permissive CSP but restrictive Permissions-Policy (or vice versa). Set both to maximize defense.
### 15.3 HSTS and CSP upgrade-insecure-requests
HSTS upgrades the connection. `upgrade-insecure-requests` (CSP) upgrades subresource references in the HTML. They work together: HSTS upgrades the top level request, CSP upgrade-insecure-requests upgrades references within the page.
### 15.4 CORP, COEP, and COOP
These three work together to provide cross origin isolation. None of them alone is sufficient. See Section 14 for the combined pattern.
### 15.5 Referrer-Policy and CSP report-uri
When CSP sends a violation report, the report includes the URL where the violation occurred (document-uri). If Referrer-Policy is `no-referrer`, the destination of the report does not see a Referer header, but the report body itself still contains URLs. The two are independent.
### 15.6 X-Content-Type-Options and CSP
nosniff is an absolute. CSP can layer additional restrictions but nosniff is non negotiable. Always set nosniff.
### 15.7 Permissions-Policy and Cross-Origin-Embedder-Policy
The `cross-origin-isolated` directive in Permissions-Policy can block isolation even when COOP and COEP are set. If you want isolation, do not block it: `Permissions-Policy: cross-origin-isolated=(self)` or just omit the directive.
---
## 16. ASSET CLASS AND USE CASE RECIPES
Paste ready blocks per use case.
### 16.1 Standard Bubbles client site (production, public marketing site)
```nginx
# /etc/nginx/snippets/common-security-headers.conf
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), browsing-topics=(), interest-cohort=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-site" always;
```
CSP is per site (set separately, not in the shared snippet, because each site's CSP is unique).
### 16.2 Standard CSP for a marketing site with GTM, GA4, AdSense
```nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://www.googleadservices.com https://googleads.g.doubleclick.net https://pagead2.googlesyndication.com https://*.googlesyndication.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.doubleclick.net; frame-src 'self' https://www.googletagmanager.com https://www.google.com https://googleads.g.doubleclick.net; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
```
### 16.3 Strict CSP with nonce for FastAPI sidecar dynamic pages
Generated per request in the upstream. nginx passes through:
```nginx
location /dashboard {
proxy_pass http://127.0.0.1:9090;
# Sidecar emits CSP with nonce; nginx does not override
}
```
See Section 6.4 for the FastAPI implementation.
### 16.4 Staging or development environment
```nginx
server {
server_name staging.example.com;
include snippets/common-security-headers.conf;
# Block all indexing
add_header X-Robots-Tag "noindex, nofollow" always;
# Permissive CSP for debugging
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' https:; report-uri /csp-report" always;
# Basic auth as additional layer
auth_basic "Staging";
auth_basic_user_file /etc/nginx/.staging-htpasswd;
}
```
### 16.5 OAuth or social login callback page
Needs `same-origin-allow-popups` to communicate back from the OAuth popup:
```nginx
location ~ ^/auth/(login|callback) {
include snippets/common-security-headers-no-coop.conf;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
}
```
### 16.6 WebAssembly application requiring cross origin isolation
```nginx
server {
server_name webapp.example.com;
include snippets/common-security-headers-base.conf;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "credentialless" always;
location / {
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri $uri/ /index.html;
}
}
```
### 16.7 Public embeddable resources (logos, badges)
```nginx
location /embed/ {
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Access-Control-Allow-Origin "*" always;
# CORS for fonts and CSS specifically
}
```
### 16.8 Healthcare or HIPAA aware site
```nginx
server {
server_name patient-portal.example.com;
include snippets/common-security-headers.conf;
# Stricter referrer policy
add_header Referrer-Policy "no-referrer" always;
# Stricter CSP
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
# Block all third party tracking
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), browsing-topics=(), interest-cohort=(), attribution-reporting=(), join-ad-interest-group=(), run-ad-auction=(), private-aggregation=()" always;
}
```
### 16.9 User upload serving endpoint (max security)
```nginx
location /user-uploads/ {
include snippets/common-security-headers.conf;
# Force download, prevent any execution
add_header Content-Type "application/octet-stream" always;
add_header Content-Disposition "attachment" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'none'" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
}
```
### 16.10 thatwebhostingguy.com wildcard (no preload, isolated demos)
```nginx
server {
server_name *.thatwebhostingguy.com;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# NO preload (different demos may not all support HTTPS)
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissive Permissions-Policy (demos may need various features)
add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(self), interest-cohort=(), browsing-topics=()" always;
# Wildcard demos: AdSense permitted
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https:; frame-src 'self' https:; frame-ancestors 'self'; base-uri 'self'; object-src 'none'" always;
}
```
---
## 17. BUBBLES NGINX REFERENCE BLOCK (PASTE READY)
The complete security header stanza, layered with caching (framework-http-caching-headers.md), content (framework-http-content-headers.md), and SEO (framework-http-seo-headers.md).
```nginx
# /etc/nginx/snippets/common-security-headers.conf
# Shared by every Bubbles client production site
# Transport
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Content
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Capabilities
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), browsing-topics=(), interest-cohort=()" always;
# Cross-origin (no isolation; just baseline hardening)
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-site" always;
```
```nginx
# /etc/nginx/sites-available/example.com
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/sites/example.com;
index index.html;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# ===== HTML PAGES =====
location ~* \.html$ {
include snippets/common-security-headers.conf;
# Per site CSP
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
# Plus caching, content, SEO headers from other frameworks
add_header Cache-Control "public, max-age=0, must-revalidate" always;
add_header Content-Language "en-US" always;
add_header X-Robots-Tag "max-snippet:200, max-image-preview:large, max-video-preview:-1" always;
}
# ===== STATIC ASSETS =====
location ~* \.(css|js|woff2|woff|ttf|otf)$ {
include snippets/common-security-headers.conf;
add_header Cross-Origin-Resource-Policy "same-site" always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
# ===== IMAGES =====
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico)$ {
include snippets/common-security-headers.conf;
add_header Cross-Origin-Resource-Policy "same-site" always;
add_header Cache-Control "public, max-age=2592000" always;
}
# ===== PDF DOWNLOADS =====
location ~* \.pdf$ {
include snippets/common-security-headers.conf;
add_header Content-Disposition "attachment" always;
add_header X-Robots-Tag "noindex" always;
add_header Cache-Control "public, max-age=86400" always;
}
# ===== USER UPLOADS (max security) =====
location /user-uploads/ {
include snippets/common-security-headers.conf;
add_header Content-Type "application/octet-stream" always;
add_header Content-Disposition "attachment" always;
add_header Content-Security-Policy "default-src 'none'" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cache-Control "public, max-age=86400" always;
}
# ===== API ENDPOINTS (FastAPI sidecar) =====
location /api/ {
include snippets/common-security-headers.conf;
add_header X-Robots-Tag "noindex, nofollow" always;
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CSP and other headers may be overridden by upstream
}
# ===== ROOT =====
location / {
include snippets/common-security-headers.conf;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
add_header Cache-Control "public, max-age=0, must-revalidate" always;
try_files $uri $uri/ $uri.html =404;
}
}
```
After deploying:
```bash
nginx -t && systemctl reload nginx
```
External validation:
```bash
echo "https://observatory.mozilla.org/analyze/example.com"
echo "https://securityheaders.com/?q=https://example.com/"
echo "https://csp-evaluator.withgoogle.com/?csp=https://example.com/"
```
Target: A or A+ on both observatory.mozilla.org and securityheaders.com.
---
## 18. AUDIT CHECKLIST
Run through these 60 items for any production site.
### Strict-Transport-Security
1. [ ] HTTPS responses include `Strict-Transport-Security` header.
2. [ ] HTTP responses do NOT include `Strict-Transport-Security` header.
3. [ ] max-age is at least 31536000 (1 year) for established sites.
4. [ ] `includeSubDomains` is present (after verifying all subdomains support HTTPS).
5. [ ] `preload` is present for sites submitted to or eligible for the preload list.
6. [ ] HTTP requests redirect to HTTPS with 301 (not 302).
7. [ ] Every subdomain serves HTTPS with a valid certificate.
8. [ ] certbot.timer is active and tested.
### Content-Security-Policy
9. [ ] CSP header is present on every HTML response.
10. [ ] `default-src` is set (fallback for unset directives).
11. [ ] `script-src` does not use `'unsafe-inline'` without justification.
12. [ ] `script-src` does not use `'unsafe-eval'` without justification.
13. [ ] If `'strict-dynamic'` is used, a fresh nonce is included on every response.
14. [ ] `object-src 'none'` is set (blocks Flash, deprecated plugins).
15. [ ] `base-uri 'self'` or `'none'` is set.
16. [ ] `form-action` is restricted appropriately.
17. [ ] `frame-ancestors` is set (replaces X-Frame-Options behavior).
18. [ ] `upgrade-insecure-requests` is set (during HTTPS migration).
19. [ ] CSP includes `report-uri` or `report-to` for violation monitoring.
20. [ ] CSP violations are monitored (not just emitted to /dev/null).
21. [ ] CSP rolled out in Report-Only mode first, then enforced.
22. [ ] CSP evaluator (csp-evaluator.withgoogle.com) shows no high severity issues.
### X-Frame-Options
23. [ ] X-Frame-Options is set on every HTML response.
24. [ ] Value is DENY or SAMEORIGIN (not ALLOW-FROM).
25. [ ] CSP frame-ancestors agrees with X-Frame-Options value.
### X-Content-Type-Options
26. [ ] `X-Content-Type-Options: nosniff` is set on every response.
27. [ ] All Content-Type values are accurate (see framework-http-content-headers.md).
28. [ ] User upload endpoints use `application/octet-stream` plus `attachment` disposition.
### Referrer-Policy
29. [ ] Referrer-Policy is set on every response.
30. [ ] Value is `strict-origin-when-cross-origin` or stricter.
31. [ ] Password reset and account pages use `no-referrer` or `same-origin`.
### Permissions-Policy
32. [ ] Permissions-Policy is set on every response.
33. [ ] Sensitive APIs (camera, microphone, geolocation, payment, USB, Bluetooth) are disabled unless needed.
34. [ ] `browsing-topics=()` is set (opt out of Topics tracking).
35. [ ] `interest-cohort=()` is set (historical FLoC opt out; harmless).
### Cross-Origin-Resource-Policy
36. [ ] CORP is set on assets, value appropriate to the use case.
37. [ ] Private user content uses `same-origin`.
38. [ ] Brand assets use `same-site`.
39. [ ] Publicly embeddable assets use `cross-origin`.
### Cross-Origin-Embedder-Policy
40. [ ] COEP is NOT set unless cross origin isolation is required.
41. [ ] If COEP is set, COOP is also set to `same-origin`.
42. [ ] If COEP is set, every cross origin resource has CORP or CORS.
### Cross-Origin-Opener-Policy
43. [ ] COOP is set on HTML responses.
44. [ ] Value is `same-origin` for typical sites.
45. [ ] OAuth flows use `same-origin-allow-popups`.
### Cross cutting
46. [ ] `nginx -t` passes without warnings.
47. [ ] `nginx -T` (effective config) shows all expected directives.
48. [ ] observatory.mozilla.org grade is A or A+.
49. [ ] securityheaders.com grade is A or A+.
50. [ ] CSP evaluator (csp-evaluator.withgoogle.com) shows no high severity issues.
51. [ ] Browser DevTools console shows no CSP violations on production.
52. [ ] All headers are sent with `always` (apply to error responses too).
53. [ ] Snippet include pattern is used (no add_header inheritance trap).
54. [ ] Headers consistent across all server blocks for the domain.
55. [ ] Headers consistent across HTTP/2 and HTTP/3.
56. [ ] Headers consistent across direct requests and FastAPI sidecar passthrough.
57. [ ] No header value contains user input without sanitization.
58. [ ] No header value contains protocol relative URLs.
59. [ ] No deprecated headers present (Public-Key-Pins, Expect-CT, Feature-Policy).
60. [ ] Site responds correctly to security scanner without producing false errors.
A site that passes all 60 has correctly configured security and trust signal headers and will earn A or A+ grades from external scanners.
---
## 19. COMMON PITFALLS
Fifteen patterns to recognize and avoid.
**Pitfall 1: HSTS preload submitted before verifying subdomains.**
Symptom: critical internal subdomain inaccessible permanently.
Why it breaks: preload list takes months to remove from. Any subdomain serving only HTTP after preload is locked out.
Fix prevention: audit every subdomain over HTTPS before submitting. Fix any HTTP only subdomain first.
**Pitfall 2: CSP enforced without Report-Only test phase.**
Symptom: GTM scripts blocked, analytics offline, third party widgets broken.
Why it breaks: real world resource graphs always include unexpected origins.
Fix prevention: Report-Only for at least a week, fix every violation, then enforce.
**Pitfall 3: 'unsafe-inline' in script-src.**
Symptom: XSS injection executes despite CSP.
Why it breaks: `'unsafe-inline'` allows any inline script, defeating the primary protection.
Fix: replace with nonce + strict-dynamic pattern. If pure static site, accept the lower security level.
**Pitfall 4: X-Frame-Options without CSP frame-ancestors.**
Symptom: scanner reports framing protection incomplete; some browsers do not enforce X-Frame-Options.
Why it breaks: X-Frame-Options is being deprecated; CSP frame-ancestors is the authoritative replacement.
Fix: set both for belt and suspenders.
**Pitfall 5: nosniff missing on user upload serving endpoint.**
Symptom: stored XSS via uploaded HTML file.
Why it breaks: browser sniffs uploaded file as HTML, renders it, runs embedded scripts.
Fix: set nosniff site wide AND serve user uploads with `application/octet-stream` plus `attachment` disposition.
**Pitfall 6: Referrer-Policy unset, default leaks tokens.**
Symptom: password reset tokens or session IDs appear in third party analytics logs.
Why it breaks: default Referer header includes full URL with query string.
Fix: set `strict-origin-when-cross-origin` site wide; `no-referrer` on sensitive endpoints.
**Pitfall 7: Permissions-Policy blocks legitimate features.**
Symptom: geolocation, camera, or other API silently refused.
Why it breaks: directive value is `()` (block) instead of `(self)` or appropriate allowlist.
Fix: set appropriate allowlist for features the site actually uses.
**Pitfall 8: COEP enabled, all cross origin images blocked.**
Symptom: layout broken, half the images missing.
Why it breaks: third party CDN images do not have CORP or CORS.
Fix: switch COEP to `credentialless`, OR self host the resources, OR add CORP at source.
**Pitfall 9: COOP same-origin breaks OAuth flow.**
Symptom: OAuth popup completes auth but cannot return data.
Why it breaks: same-origin breaks window.opener.
Fix: use `same-origin-allow-popups` instead.
**Pitfall 10: HSTS on HTTP server block.**
Symptom: nothing breaks but scanner notes anomaly.
Why it breaks: HSTS over HTTP is ignored by the spec but indicates configuration confusion.
Fix: remove HSTS from HTTP server blocks. HTTP server should only redirect.
**Pitfall 11: CSP with no fallback (no default-src).**
Symptom: some resource types unrestricted because their directive was not set.
Why it breaks: directives not explicitly set inherit from default-src; without default-src, they are unrestricted.
Fix: always set `default-src 'self'` as a baseline.
**Pitfall 12: add_header inheritance trap on security headers.**
Symptom: nosniff disappears on specific file types after a location block override.
Why it breaks: nested `add_header` wipes parent declarations.
Fix: snippet include pattern in every location.
**Pitfall 13: Header value too long for nginx default buffer.**
Symptom: nginx error or response truncated.
Why it breaks: CSP with many origins exceeds default 8 KB header buffer.
Fix: increase `large_client_header_buffers`, or simplify CSP using wildcards.
**Pitfall 14: Permissions-Policy uses old Feature-Policy syntax.**
Symptom: browser warning, header value ignored.
Why it breaks: Permissions-Policy uses `feature=(allowlist)` not `feature 'self'`.
Fix: convert to current syntax.
**Pitfall 15: Mixed HTTP and HTTPS resources block on COEP.**
Symptom: COEP blocks HTTP loaded images, fonts, scripts.
Why it breaks: HTTP resources cannot send CORP or pass CORS checks.
Fix: upgrade all resources to HTTPS; use CSP `upgrade-insecure-requests`.
---
## 20. DIAGNOSTIC COMMANDS
Reference of every command useful for security header investigation.
### Inspect all security headers at once
```bash
# Full security family
curl -sI https://example.com/ | grep -iE "^(strict-transport-security|content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy|cross-origin-(opener|embedder|resource)-policy):"
# Pretty print
curl -sI https://example.com/ | grep -iE "^(strict-|content-security|x-|referrer|permissions|cross-origin):" | while read line; do
KEY=$(echo "$line" | cut -d: -f1)
VAL=$(echo "$line" | cut -d: -f2- | sed 's/^ //')
echo ""
echo "=== $KEY ==="
echo "$VAL" | tr ';,' '\n' | sed 's/^ */ /'
done
```
### HSTS verification
```bash
# Header presence and content
curl -sI https://example.com/ | grep -i strict-transport-security
# HTTP redirect check
curl -sI http://example.com/ | head -3
# Preload eligibility
curl -s "https://hstspreload.org/api/v2/status?domain=example.com" | python3 -m json.tool
# All subdomains over HTTPS
for sub in $(dig +short ANY example.com | grep -oE "^[a-z0-9-]+\.example\.com\."); do
sub=${sub%.}
echo -n "$sub: "
curl -sI -m 5 "https://$sub/" 2>&1 | head -1 || echo "FAILED"
done
```
### CSP verification
```bash
# Header presence
curl -sI https://example.com/ | grep -i content-security-policy
# Evaluator (Google's checker)
echo "Visit: https://csp-evaluator.withgoogle.com/?csp=https%3A%2F%2Fexample.com%2F"
# Report endpoint test
curl -X POST https://example.com/csp-report \
-H "Content-Type: application/csp-report" \
-d '{"csp-report":{"document-uri":"https://example.com/","violated-directive":"script-src","blocked-uri":"https://evil.com/script.js"}}'
# Check for unsafe directives
curl -sI https://example.com/ | grep -i content-security-policy | grep -oE "unsafe-(inline|eval)"
```
### Cross origin verification
```bash
# All three cross origin headers
curl -sI https://example.com/ | grep -iE "^cross-origin-"
# Verify isolation in browser:
# In DevTools console: window.crossOriginIsolated
# Expected: true (if attempting isolation)
# Verify SharedArrayBuffer:
# typeof SharedArrayBuffer === "function"
```
### External scanners
```bash
# Mozilla Observatory
echo "https://observatory.mozilla.org/analyze/example.com"
# Security Headers
echo "https://securityheaders.com/?q=https://example.com/"
# Hardenize (comprehensive)
echo "https://www.hardenize.com/report/example.com"
# Internet.nl
echo "https://internet.nl/site/example.com/"
# Qualys SSL Labs (for TLS, not headers)
echo "https://www.ssllabs.com/ssltest/analyze.html?d=example.com"
```
### Server side investigation
```bash
# All add_header directives in effect
nginx -T 2>/dev/null | grep -iE "add_header.*(strict-transport|content-security|x-frame|x-content-type|referrer|permissions|cross-origin)"
# Confirm includes work
nginx -T 2>/dev/null | grep -i "include.*security"
# Find any conflict (multiple values for same header)
nginx -T 2>/dev/null | grep -i "add_header" | awk '{print $2}' | sort | uniq -c | sort -rn
# Apply changes
nginx -t && systemctl reload nginx
```
### Browser DevTools quick reference
1. **Application or Storage panel**: shows HSTS state (`chrome://net-internals/#hsts`).
2. **Network panel**: shows headers for each request. Click any request, "Headers" tab.
3. **Console panel**: shows CSP violations in red.
4. **Issues panel** (Chrome): shows security issues with explanations.
5. **Security panel**: shows certificate, connection, and origin info.
Useful console commands:
```javascript
// Cross origin isolation status
window.crossOriginIsolated
// Available APIs that require isolation
typeof SharedArrayBuffer
typeof performance.measureUserAgentSpecificMemory
// Trigger a CSP report (for testing report endpoint)
eval("test") // Will trigger script-src violation if unsafe-eval not allowed
```
---
## 21. CROSS-REFERENCES
* [framework-http-caching-headers.md](framework-http-caching-headers.md): Cache-Control, ETag, Last-Modified, Expires, Vary, Age. Security headers should be cached with the response; do not cache security headers separately or stripped.
* [framework-http-content-headers.md](framework-http-content-headers.md): Content-Type, Content-Language, Content-Encoding, Content-Length, Content-Disposition. nosniff pairs with strict Content-Type discipline. Content-Disposition is part of the user upload security pattern (Section 16.9 here).
* [framework-http-seo-headers.md](framework-http-seo-headers.md): X-Robots-Tag, Link, Location. Open redirect security in framework-http-seo-headers.md Section 7.4 is part of the overall security posture.
* [UNIVERSAL-RANKING-FRAMEWORK.md](UNIVERSAL-RANKING-FRAMEWORK.md): the master ranking reference. Security headers contribute to E-E-A-T trust signals.
* [SEO-BUILD-REFERENCE.md](SEO-BUILD-REFERENCE.md) v2.4: the build playbook.
* [framework-csp-deployment.md](framework-csp-deployment.md): deeper dive into CSP rollout strategy, nonce architecture, GTM integration.
* [framework-tls-config.md](framework-tls-config.md): TLS cipher selection, certificate management, OCSP stapling. Pairs with HSTS for transport security.
* Google CSP documentation: https://developers.google.com/tag-platform/security/guides/csp
* web.dev strict CSP guide: https://web.dev/articles/strict-csp
* HSTS preload list: https://hstspreload.org/
* MDN security headers reference: https://developer.mozilla.org/en-US/docs/Web/Security
* Mozilla Observatory: https://observatory.mozilla.org/
* securityheaders.com: https://securityheaders.com/
* CSP Evaluator: https://csp-evaluator.withgoogle.com/
* RFC 6797 (HSTS): https://www.rfc-editor.org/rfc/rfc6797
* RFC 7034 (X-Frame-Options): https://www.rfc-editor.org/rfc/rfc7034
* W3C CSP Level 3: https://www.w3.org/TR/CSP3/
* W3C Referrer Policy: https://www.w3.org/TR/referrer-policy/
* W3C Permissions Policy: https://www.w3.org/TR/permissions-policy/
---
## APPENDIX A: ONE PAGE QUICK REFERENCE
For the person who just wants the answer.
### Bubbles default security header stack
```nginx
# /etc/nginx/snippets/common-security-headers.conf
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), browsing-topics=(), interest-cohort=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-site" always;
```
### Standard CSP for Bubbles client site with GTM and GA4
```
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests
```
### Header purpose table
| Header | One line purpose |
|---|---|
| Strict-Transport-Security | Force HTTPS forever |
| Content-Security-Policy | Allow list of where scripts and resources may come from |
| X-Frame-Options | Block clickjacking (legacy) |
| X-Content-Type-Options | Block MIME sniffing |
| Referrer-Policy | Control what URL info leaks to other origins |
| Permissions-Policy | Disable sensitive browser APIs |
| Cross-Origin-Resource-Policy | Who may load this resource |
| Cross-Origin-Embedder-Policy | What this document may embed |
| Cross-Origin-Opener-Policy | What this document may share a window with |
### CSP value cheat sheet
| Want | Use |
|---|---|
| Only same origin scripts | `script-src 'self'` |
| Plus inline scripts safely | `script-src 'self' 'nonce-RANDOM' 'strict-dynamic'` |
| Plus GTM | `script-src 'self' 'unsafe-inline' https://www.googletagmanager.com` |
| Block all framing | `frame-ancestors 'none'` |
| Same site framing | `frame-ancestors 'self'` |
| Specific partner | `frame-ancestors 'self' https://partner.example.com` |
| No plugins ever | `object-src 'none'` |
| Auto upgrade HTTP refs | `upgrade-insecure-requests` |
### Five commands every operator should know
```bash
# 1. View all security headers
curl -sI https://example.com/ | grep -iE "^(strict|content-security|x-|referrer|permissions|cross-origin):"
# 2. Check observatory score
echo "Visit: https://observatory.mozilla.org/analyze/example.com"
# 3. Check securityheaders.com score
echo "Visit: https://securityheaders.com/?q=https://example.com/"
# 4. Evaluate CSP
echo "Visit: https://csp-evaluator.withgoogle.com/?csp=https://example.com/"
# 5. Apply nginx changes
nginx -t && systemctl reload nginx
```
### Three end to end tests
```bash
# 1. Verify HSTS is enforced
curl -sI https://example.com/ | grep -i strict-transport-security
# Expected: strict-transport-security: max-age=63072000; includeSubDomains; preload
# 2. Verify no CSP unsafe directives (or that they are justified)
curl -sI https://example.com/ | grep -i content-security-policy | grep -oE "unsafe-(inline|eval)"
# If output appears, document why or remove
# 3. Verify nosniff and X-Frame-Options site wide
for path in / /about /services /blog /contact; do
HEADERS=$(curl -sI "https://example.com$path" | grep -iE "x-content-type-options|x-frame-options")
echo "=== $path ==="
echo "$HEADERS"
done
# Both headers should appear on every page
```
If all three produce expected output AND observatory.mozilla.org returns A or A+, the security header stack is correctly wired.
---
End of framework-http-security-headers.md.