cinny(nginx): security headers on every location; sync repo with live (cinny #210, #214, #155)
Lint / Shell (shellcheck) (push) Successful in 13s
Lint / JS (eslint) (push) Successful in 12s
Lint / No secrets in webhook configs (push) Successful in 6s
Lint / Python (ruff) (push) Successful in 9s
Lint / Python deps (pip-audit) (push) Successful in 1m9s
Lint / Secret scan (gitleaks) (push) Successful in 10s

Applied on LXC 106 (backed up, `nginx -t`, reloaded) and recorded here:

- Security headers (incl. CSP) moved to snippets/cinny-security-headers.conf
  and included at server level plus in the /sw.js, static-asset and
  json/html locations. nginx drops inherited add_header in any block that
  sets its own, so static assets were served without nosniff and /sw.js
  without a CSP (a service worker takes its CSP from its own script).
  Now every path carries all seven headers. Verified: the SW installs and
  controls the page under the CSP with no violations.
- #214: CSP no longer allows fonts.googleapis.com / fonts.gstatic.com
  (VT323 is self-hosted since cinny 6f250353).
- #155: the /share-target → /share 303 is now live (it was only in the
  repo; the 106 matrix-deploy hook has been dead since May, so repo edits
  never reached it). absolute_redirect off makes it relative.
- README: the snippet, and that 106 needs these applied by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-23 23:17:53 -04:00
co-authored by Claude Opus 5.5
parent 1dacf29d53
commit 23ad133dc3
3 changed files with 29 additions and 18 deletions
+12
View File
@@ -0,0 +1,12 @@
# Security headers for chat.lotusguild.org (matrix repo: cinny/nginx-security-headers.conf).
# Included at server level AND in every location that sets its own add_header:
# nginx drops inherited add_header directives in any block that defines one,
# so without the include, static assets lost nosniff and /sw.js lost its CSP
# (a service worker's CSP comes from its own script response). cinny #210.
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Permissions-Policy "accelerometer=(), autoplay=(self), camera=(self), display-capture=(self), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(), magnetometer=(), microphone=(self), midi=(), payment=(), usb=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://drive.lotusguild.org https://media.giphy.com https://media0.giphy.com https://media1.giphy.com https://media2.giphy.com https://media3.giphy.com https://media4.giphy.com https://www.openstreetmap.org https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' https://matrix.lotusguild.org wss://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://chat.mozilla.org https://vector.im https://api.giphy.com https://*.giphy.com wss:; media-src 'self' https: blob:; frame-src 'self' https:; worker-src 'self' blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
+13 -16
View File
@@ -18,21 +18,16 @@ server {
limit_conn chat_conn 25;
index index.html;
# Security headers
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# Security headers (incl. CSP) — see the snippet
include snippets/cinny-security-headers.conf;
# HSTS: TLS terminates upstream (this server is listen 80), so this reaches
# the browser only if the front proxy passes upstream response headers
# through; otherwise set it at the TLS terminator. includeSubDomains covers
# all *.lotusguild.org (all HTTPS); `preload` is inert until submitted to
# hstspreload.org.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Permissions-Policy: allow only what the app uses (self) — calls
# (camera/microphone/display-capture), location share (geolocation), sounds
# (autoplay), Element Call (fullscreen/encrypted-media) — and deny the rest.
add_header Permissions-Policy "accelerometer=(), autoplay=(self), camera=(self), display-capture=(self), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(), magnetometer=(), microphone=(self), midi=(), payment=(), usb=()" always;
# Block all source map files and dotfiles from public access
location ~* \.(js|css)\.map$ {
@@ -48,27 +43,36 @@ server {
return 404;
}
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://drive.lotusguild.org https://media.giphy.com https://media0.giphy.com https://media1.giphy.com https://media2.giphy.com https://media3.giphy.com https://media4.giphy.com https://www.openstreetmap.org https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' https://matrix.lotusguild.org wss://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://chat.mozilla.org https://vector.im https://api.giphy.com https://*.giphy.com wss:; media-src 'self' https: blob:; frame-src 'self' https:; worker-src 'self' blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
# Service worker must never be cached so updates are picked up immediately
location = /sw.js {
include snippets/cinny-security-headers.conf;
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}
# Cache content-addressed static assets aggressively
location ~* \.(?:js|css|woff2?|png|svg|ico|webp)$ {
include snippets/cinny-security-headers.conf;
expires 1y;
add_header Cache-Control "public, immutable" always;
}
# Never cache HTML or JSON (index.html, config.json, manifest.json)
location ~* \.(json|html)$ {
include snippets/cinny-security-headers.conf;
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}
# [Gitea #155] PWA share target. The service worker normally answers this
# POST itself; if it isn't controlling the page yet, land on /share
# (the shared files are lost, but nothing 405s).
location = /share-target {
absolute_redirect off;
return 303 /share;
}
# Auto-deploy webhook — proxied to local webhook service
location = /hooks/lotus-deploy {
proxy_pass http://127.0.0.1:9001/hooks/lotus-deploy;
@@ -77,13 +81,6 @@ server {
proxy_connect_timeout 5;
}
# [Gitea #155] PWA share target. The service worker normally answers this
# POST itself; if it isn't controlling the page yet, land on /share
# (the shared files are lost, but nothing 405s).
location = /share-target {
return 303 /share;
}
location / {
rewrite ^/config\.json$ /config.json break;
rewrite ^/manifest\.json$ /manifest.json break;