feat(cinny): nginx for Element Call on call.chat.lotusguild.org (cinny #43)
Lint / Shell (shellcheck) (push) Successful in 21s
Lint / JS (eslint) (push) Successful in 14s
Lint / No secrets in webhook configs (push) Successful in 6s
Lint / Landing page is rendered (matrix (push) Successful in 6s
Lint / Python (ruff) (push) Successful in 7s
Lint / Python deps (pip-audit) (push) Successful in 52s
Lint / Secret scan (gitleaks) (push) Successful in 8s
Lint / Shell (shellcheck) (pull_request) Successful in 17s
Lint / JS (eslint) (pull_request) Successful in 13s
Lint / No secrets in webhook configs (pull_request) Successful in 6s
Lint / Landing page is rendered (matrix (pull_request) Successful in 6s
Lint / Python (ruff) (pull_request) Successful in 8s
Lint / Python deps (pip-audit) (pull_request) Successful in 53s
Lint / Secret scan (gitleaks) (pull_request) Successful in 8s

- cinny/nginx.conf: a call.chat.lotusguild.org server block that serves ONLY
  /public/element-call/ (the same files chat.lotusguild.org already serves
  there) and 404s everything else, including source maps and dotfiles.
- cinny/nginx-security-headers-call.conf (new snippet): frame-ancestors
  https://chat.lotusguild.org instead of X-Frame-Options SAMEORIGIN, which
  would block the now cross-origin parent.
- cinny/nginx-security-headers.conf: the app's Permissions-Policy delegates
  autoplay/camera/display-capture/microphone to the call origin (without it
  the cross-origin frame's getUserMedia is refused). Outer quotes switched to
  single: the inner "origin" quotes broke nginx parsing.

Nothing changes for users until config.json sets elementCallUrl (separate
step). Snippets are installed by hand on LXC 106; nginx.conf deploys on merge.

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-26 23:24:32 -04:00
co-authored by Claude Opus 5.5
parent 1dccca914c
commit 9c5a183025
3 changed files with 68 additions and 1 deletions
+14
View File
@@ -0,0 +1,14 @@
# Headers for the Element Call page on its own origin, call.chat.lotusguild.org
# (cinny #43). Installed on LXC 106 as /etc/nginx/snippets/cinny-security-headers-call.conf
# (deploy/lxc106-cinny.sh does NOT copy snippets — install by hand, like the others).
#
# frame-ancestors: only the web app may frame the call page (replaces
# X-Frame-Options SAMEORIGIN, which would block the now cross-origin parent;
# browsers honour frame-ancestors over X-Frame-Options anyway).
# Permissions-Policy: "self" here is the call origin, which is what uses the
# mic/camera/screen; the app's own policy delegates them to this origin.
add_header Content-Security-Policy "frame-ancestors https://chat.lotusguild.org" always;
add_header X-Content-Type-Options nosniff 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=(), gyroscope=(), magnetometer=(), microphone=(self), midi=(), payment=(), usb=()" always;
+4 -1
View File
@@ -3,10 +3,13 @@
# 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.
# Permissions-Policy delegates autoplay/camera/display-capture/microphone to the
# call page's own origin (call.chat.lotusguild.org, cinny #43); without it the
# cross-origin call frame's getUserMedia is refused (NotAllowedError).
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 Permissions-Policy 'accelerometer=(), autoplay=(self "https://call.chat.lotusguild.org"), camera=(self "https://call.chat.lotusguild.org"), display-capture=(self "https://call.chat.lotusguild.org"), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(), magnetometer=(), microphone=(self "https://call.chat.lotusguild.org"), 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;
+50
View File
@@ -108,3 +108,53 @@ server {
rewrite ^(.+)$ /index.html break;
}
}
# [cinny #43] Element Call on its own origin. The web app frames
# https://call.chat.lotusguild.org/public/element-call/index.html (config.json
# `elementCallUrl`), so the call page can no longer read the app's storage
# (login token, crypto store) or use its service worker. Serves ONLY the call
# page — the same files as chat.lotusguild.org/public/element-call/ — and 404s
# everything else, so this hostname exposes nothing new.
server {
listen 80;
listen [::]:80;
server_name call.chat.lotusguild.org;
brotli on;
brotli_static on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json
image/svg+xml application/wasm font/woff2;
root /var/www/html;
server_tokens off;
limit_req zone=chat_limit burst=60 nodelay;
limit_conn chat_conn 25;
include snippets/cinny-security-headers-call.conf;
location ^~ /public/element-call/ {
include snippets/cinny-security-headers-call.conf;
location ~* \.map$ {
return 404;
}
location ~ /\. {
return 404;
}
location ~* \.html$ {
include snippets/cinny-security-headers-call.conf;
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}
location ~* \.(?:js|css|woff2?|png|svg|ico|webp|wasm)$ {
include snippets/cinny-security-headers-call.conf;
expires 1y;
add_header Cache-Control "public, immutable" always;
}
try_files $uri =404;
}
location / {
return 404;
}
}