Skip to content

Add CSP headers to nginx configuration #549

Open
heisbrot wants to merge 5 commits intomainfrom
csp
Open

Add CSP headers to nginx configuration #549
heisbrot wants to merge 5 commits intomainfrom
csp

Conversation

@heisbrot
Copy link
Copy Markdown
Contributor

@heisbrot heisbrot commented Feb 11, 2026

Summary by CodeRabbit

Release Notes

  • Security
    • Enhanced server security with additional HTTP headers (X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, Content-Security-Policy).
    • Added configurable Content Security Policy management with environment variables for flexible security policy control.
    • Optimized script handling during build process for improved CSP compliance.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

These changes implement a multi-layered security hardening and optimization framework for the Docker environment. HTTP security headers are added to nginx configuration, a dynamic Content-Security-Policy system generates policies from environment variables, and a post-build script extracts and externalizes inline JavaScript from Next.js output.

Changes

Cohort / File(s) Summary
Security Headers Configuration
docker/default.conf
Added HTTP security headers including Cache-Control (with always modifier), X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, and Content-Security-Policy to both root and error handler location blocks.
CSP Dynamic Assembly
docker/init_react_envs.sh
Introduced CSP policy assembly from multiple environment sources (NETBIRD_CSP, AUTH_AUTHORITY, AUTH_AUDIENCE, NETBIRD_MGMT_API_ENDPOINT, LETSENCRYPT_DOMAIN). Script deduplicates and formats CSP_CONNECT_SRC, CSP_FRAME_SRC, and CSP_SCRIPT_SRC, then updates nginx config and reloads it. Added exports for NETBIRD_CSP, NETBIRD_CSP_CONNECT_SRC, and NETBIRD_DISABLE_CSP.
Post-Build Optimization
package.json, postbuild.js
Added "postbuild" script to package.json that executes postbuild.js. New postbuild.js processes Next.js export output: extracts inline <script> blocks from HTML files, aggregates their contents, generates MD5-hashed JavaScript chunk at out/assets/chunk.<hash>.js, and replaces inline scripts with external references.

Sequence Diagram(s)

sequenceDiagram
    participant Build as Build System
    participant NextJS as Next.js Export
    participant HTML as HTML Files
    participant PostBuild as postbuild.js
    participant Assets as Assets Directory
    
    Build->>NextJS: Run build & export
    NextJS->>HTML: Generate HTML with inline scripts
    Build->>PostBuild: Execute postbuild script
    PostBuild->>HTML: Find all HTML files recursively
    PostBuild->>HTML: Extract inline script contents
    PostBuild->>PostBuild: Aggregate & clean script contents
    PostBuild->>PostBuild: Generate MD5 hash
    PostBuild->>Assets: Write chunk.<hash>.js
    PostBuild->>HTML: Replace inline scripts with<br/>external script references
    PostBuild->>Build: Script execution complete
Loading
sequenceDiagram
    participant Env as Environment Variables
    participant InitScript as init_react_envs.sh
    participant CSPBuilder as CSP Assembly Logic
    participant Nginx as Nginx Config
    participant Server as Nginx Server
    
    Env->>InitScript: Provide CSP/auth/domain vars
    InitScript->>InitScript: Check NETBIRD_DISABLE_CSP
    alt CSP Enabled
        InitScript->>CSPBuilder: Extract policy sources
        CSPBuilder->>CSPBuilder: Assemble from multiple sources
        CSPBuilder->>CSPBuilder: Deduplicate & format<br/>CONNECT_SRC, FRAME_SRC, SCRIPT_SRC
        CSPBuilder->>Nginx: Generate & replace<br/>add_header Content-Security-Policy
    else CSP Disabled
        InitScript->>Nginx: Remove CSP header
    end
    InitScript->>Server: Reload nginx
    Server->>Server: Apply new configuration
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Poem

🐰 Hops of security, so very divine,
Headers and policies fall in line,
Scripts get bundled, hashed up tight,
The config assembles through morning's light,
CSP dances, nginx reloads with glee,
A fortress of code, as safe as can be! 🔒

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The PR title 'Add CSP headers to nginx configuration' accurately describes the primary change across modified files: nginx config updates with security headers and CSP-related environment variable handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch csp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@heisbrot
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@heisbrot heisbrot marked this pull request as ready for review February 11, 2026 13:25
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@docker/default.conf`:
- Line 21: Replace the permissive placeholder Content-Security-Policy header set
by add_header Content-Security-Policy (the current value including
'unsafe-inline' and 'unsafe-eval') with a fail-closed restrictive default such
as "default-src 'none'; base-uri 'self'; frame-ancestors 'none';" (or similarly
strict minimal policy) for both occurrences where add_header
Content-Security-Policy is used, and ensure init_react_envs.sh still performs
its runtime replacement; update any test or comment referencing the old
permissive value so the script can safely overwrite a restrictive baseline
rather than leaving an insecure fallback.

In `@docker/init_react_envs.sh`:
- Around line 146-150: The current sed invocation that replaces the CSP header
uses "|| { echo ... }" which swallows failures; change it so a failed
replacement becomes fatal: run the sed replacement for the CSP_HEADER into
/etc/nginx/http.d/default.conf without the trailing "||" fallback and
immediately verify the replacement succeeded (e.g., grep or string check that
$CSP_HEADER is present in /etc/nginx/http.d/default.conf) and if the check
fails, write a clear error and exit 1; update the block surrounding the sed call
(the line containing sed -i "s|add_header Content-Security-Policy \"[^\"]*\"
always;|$CSP_HEADER|g") to perform this verification and exit on failure.
🧹 Nitpick comments (6)
postbuild.js (3)

50-56: Redundant src= guard, but the regex lookahead is actually overly broad.

The negative lookahead (?![^>]*src) rejects any <script> tag containing the substring src anywhere in its attributes — not just the src= attribute. This means a tag like <script data-src="...">inline code</script> would be silently skipped. The line 54 guard (attributes.includes("src=")) is more precise but unreachable for such cases because the regex already filters them out.

If Next.js ever emits inline scripts with attributes containing "src" as a substring (e.g., data-src), they won't be extracted.

Consider tightening the lookahead to match only the src attribute:

Proposed fix
-    /<script(?![^>]*src)([^>]*)>(.+?)<\/script>/gs,
+    /<script(?![^>]*\ssrc[\s=])([^>]*)>(.+?)<\/script>/gs,

71-74: Log message missing the filename being processed.

When no inline scripts are found, the log doesn't indicate which file was skipped. With many HTML files, this makes debugging harder.

Proposed fix
-    console.log(`No inline scripts found`);
+    console.log(`No inline scripts found in ${file}`);

44-94: Each HTML file writes its own chunk — consider deduplicating writes.

Every HTML file independently hashes its extracted scripts and writes a chunk file. When many pages share identical inline scripts (common in Next.js static exports), the same file is written repeatedly. This isn't a correctness bug, but a single-pass approach (collect all unique chunks first, then rewrite HTML files) would be cleaner and avoid redundant I/O.

docker/init_react_envs.sh (1)

128-138: default.conf has a permissive fallback CSP — document the intent.

The static CSP in default.conf is "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" (essentially allows everything over HTTPS), while the dynamically generated policy here is properly restrictive (default-src 'none', explicit allowlists). This works because the script replaces it at runtime, but if the replacement fails silently (see prior comment), the permissive policy is served. Consider adding a comment in default.conf to make this intentional fallback/placeholder relationship clear.

docker/default.conf (2)

15-24: Security headers look good. Consider adding Referrer-Policy and Permissions-Policy.

The set of headers is solid. Two commonly recommended headers that are absent:

  • Referrer-Policy: strict-origin-when-cross-origin — limits referrer leakage to third parties.
  • Permissions-Policy: camera=(), microphone=(), geolocation=() — explicitly disables browser features the dashboard likely doesn't need.

Neither is critical, but they complement the existing hardening nicely.

Also applies to: 27-36


6-13: Wasm location blocks don't inherit the security headers.

In nginx, add_header in a location block replaces — not supplements — directives from the parent. Since there are no add_header directives at the server level, the wasm locations have zero security response headers. The wasm files themselves aren't a direct risk, but responses without X-Content-Type-Options: nosniff could theoretically be MIME-sniffed.

If adding headers to these blocks, be careful not to add Cache-Control: no-store since wasm files should be cacheable.

Comment thread docker/default.conf
Comment thread docker/init_react_envs.sh
@heisbrot heisbrot changed the title Add csp Add CSP headers to nginx configuration Feb 11, 2026
@renne
Copy link
Copy Markdown

renne commented Mar 25, 2026

Hi! Just wanted to flag that the add_header Cache-Control ... always fix in this PR is particularly important beyond the CSP work.

Without the always keyword, nginx only sends Cache-Control headers on 2xx/3xx responses — not on 404s. This means browsers can heuristically cache a transient 404 (e.g. during a container restart/update), and the cached 404 persists for all future visits until the browser cache is manually cleared. A hard refresh (Ctrl+Shift+R) bypasses it for one load but doesn't evict the cached entry.

This PR fixes it correctly by adding always to the Cache-Control header in both location / and location = /404.html. The other security headers (X-Frame-Options, Strict-Transport-Security, CSP, etc.) are a great addition too.

Would love to see this merged — the always fix addresses a real user-facing bug in self-hosted deployments. Thanks for the work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants