Security UI: Settings toggles for reverse-proxy mode + auth-proxy header

Config is DB-backed (metadata.app_config) — there is no config.json — so the
reverse-proxy settings I added earlier had NO way to be set by a user and were
effectively dead. Added them to Settings → Security, next to the launch-PIN toggle:

- "Behind a reverse proxy" checkbox (security.trust_reverse_proxy) — help text notes
  it's for nginx/Caddy/Traefik+TLS, to leave OFF for direct/LAN http://, and that it
  needs a restart (applied at app init).
- "Auth proxy user header" field (security.auth_proxy_header) — e.g. Remote-User,
  with the must-strip-client-headers warning; blank = off.

Wired into the existing settings load + save; the save loop already persists every
key in the security object via config_manager.set, so no backend change needed.
Fixed Support/REVERSE-PROXY.md to point at Settings → Security instead of a
nonexistent config.json. Off by default → zero impact for direct users.

64 script-split integrity tests pass.
This commit is contained in:
BoulderBadgeDad 2026-06-10 21:17:26 -07:00
parent 86d0a0dd62
commit fb8c8a71c6
3 changed files with 30 additions and 14 deletions

View file

@ -15,15 +15,8 @@ internet. This guide covers the safe setup.
By default SoulSync does **not** trust proxy headers (so a direct client can't spoof
its IP or pretend the connection is HTTPS). If you're behind a proxy that
terminates TLS, opt in by setting this in your `config.json`:
```json
{
"security": {
"trust_reverse_proxy": true
}
}
```
terminates TLS, turn on **Settings → Security → "Behind a reverse proxy"** and
**restart SoulSync** (this option applies at startup).
When enabled, SoulSync:
- trusts `X-Forwarded-For/Proto/Host/Port` from **one** proxy hop (correct client
@ -116,11 +109,8 @@ Pick one:
option for internet exposure.
SoulSync can **trust the proxy's authenticated-user header** so the launch PIN is
skipped once the proxy has logged you in. Set the header name in `config.json`:
```json
{ "security": { "auth_proxy_header": "Remote-User" } }
```
skipped once the proxy has logged you in. Set the header name in **Settings →
Security → "Auth proxy user header"** (e.g. `Remote-User`).
> ⚠️ **Only enable this behind a proxy you control that STRIPS any client-supplied
> copy of that header.** Otherwise a direct visitor could send `Remote-User: admin`

View file

@ -5993,6 +5993,24 @@
Origins (full URL, no trailing slash) allowed to open WebSocket connections to this instance — one per line, or comma-separated. Leave empty for same-origin only (the secure default; works for direct access and most reverse-proxy setups). Add your public domain here if you reach SoulSync via a reverse proxy or custom domain and the WebSocket fails to connect. Use <code>*</code> on its own line to allow any origin (insecure — only do this if you understand why you need it).
</div>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="security-trust-proxy">
<span>Behind a reverse proxy</span>
</label>
<div class="setting-help-text">
Enable only if SoulSync runs behind nginx / Caddy / Traefik that terminates HTTPS. Trusts the proxy's <code>X-Forwarded-*</code> headers, marks the session cookie HTTPS-only, and adds security headers. <strong>Leave OFF for direct or LAN access over http://</strong> — turning it on would make the session cookie HTTPS-only and break plain-HTTP access. <strong>Takes effect after a restart.</strong> See <code>Support/REVERSE-PROXY.md</code>.
</div>
</div>
<div class="form-group">
<label for="security-auth-proxy-header">Auth proxy user header <span style="opacity:0.6">(optional)</span>:</label>
<input type="text" id="security-auth-proxy-header" placeholder="Remote-User" autocomplete="off" style="width: 100%; font-family: monospace; font-size: 12px;">
<div class="setting-help-text">
If an auth proxy (Authelia / Authentik / oauth2-proxy) logs users in <em>in front of</em> SoulSync, enter the header it sets (e.g. <code>Remote-User</code>) and SoulSync will skip the launch PIN for already-authenticated requests. <strong>Only set this behind a proxy that strips any client-supplied copy of the header</strong> — otherwise it can be spoofed. Leave blank to disable (the default).
</div>
</div>
</div>
<!-- Discovery Settings -->

View file

@ -1398,6 +1398,12 @@ async function loadSettingsData() {
const corsField = document.getElementById('security-cors-origins');
if (corsField) corsField.value = corsOrigins;
// Reverse-proxy mode + auth-proxy header (default off / empty).
const trustProxy = document.getElementById('security-trust-proxy');
if (trustProxy) trustProxy.checked = settings.security?.trust_reverse_proxy || false;
const authHeader = document.getElementById('security-auth-proxy-header');
if (authHeader) authHeader.value = settings.security?.auth_proxy_header || '';
// Check if admin has a PIN set
const profilesRes = await fetch('/api/profiles');
const profilesData = await profilesRes.json();
@ -3146,6 +3152,8 @@ async function saveSettings(quiet = false) {
security: {
require_pin_on_launch: document.getElementById('security-require-pin')?.checked || false,
cors_origins: document.getElementById('security-cors-origins')?.value?.trim() || '',
trust_reverse_proxy: document.getElementById('security-trust-proxy')?.checked || false,
auth_proxy_header: document.getElementById('security-auth-proxy-header')?.value?.trim() || '',
}
};