Configuration and Security

This guide centralizes environment configuration and security expectations.

For deployment mode selection, see DEPLOYMENT.md. For integration-specific setup values, see INTEGRATIONS.md.

Environment Variables

For the complete environment variable reference (all containers, defaults, and status labels), see ENVIRONMENT_VARIABLES.md.

The sections below provide guidance on configuration patterns, security hardening, and operational concerns that go beyond simple variable listings.

Role Split Guidance (Compose/Kubernetes)

For horizontally scaled split deployments, prefer:

For single-process deployments, keep BACKEND_PROCESS_ROLE=all.

External Access Settings

If users access soundspan from outside your local network, configure CORS and API routing intentionally.

Frontend Build-Time vs Runtime

NEXT_PUBLIC_API_URL, NEXT_PUBLIC_API_PATH_MODE, and NEXT_PUBLIC_LISTEN_TOGETHER_ALLOW_POLLING are frontend build-time variables.

Source-Build Direct Mode (optional)

If you build the frontend yourself and want direct browser calls to backend:

NEXT_PUBLIC_API_URL=https://soundspan-api.yourdomain.com
NEXT_PUBLIC_API_PATH_MODE=direct
ALLOWED_ORIGINS=http://localhost:3030,https://soundspan.yourdomain.com

Pre-Published Image Recommendation (no rebuild)

For users consuming published images:

NEXT_PUBLIC_API_PATH_MODE controls how the browser reaches backend APIs:

Set this in frontend build/dev environment (same place you set NEXT_PUBLIC_API_URL). For pre-published images, see reverse-proxy path routing guidance in REVERSE_PROXY_AND_TUNNELS.md.

For Listen Together, the frontend proxies /socket.io/listen-together to backend by default in split deployments. If you bypass frontend proxying intentionally, your edge proxy/tunnel must route /socket.io/listen-together to backend :3006. LISTEN_TOGETHER_ALLOW_POLLING=false is recommended for HA deployments; only enable polling fallback when sticky sessions are guaranteed end-to-end. For pre-published frontend images, browser polling fallback also requires rebuilding with NEXT_PUBLIC_LISTEN_TOGETHER_ALLOW_POLLING=true.

For multi-replica backend/frontend deployments, configure Redis as a highly available endpoint. A single Redis pod is a runtime SPOF for sessions, queues, and realtime coordination. Redis HA is an operator-managed prerequisite (external managed Redis/Dragonfly, Sentinel, or equivalent); soundspan consumes the configured endpoint for queues, claims, and realtime coordination and does not manage Redis HA topology itself.

Browser Content Security Policy

The frontend sends a nonce-based Content Security Policy on every Next-served document, including unauthenticated /share/* pages and PWA navigations. API response CSP headers are separate and do not protect documents rendered by the browser.

The production policy is:

default-src 'self'; script-src 'self' 'nonce-{PER_REQUEST_NONCE}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' ws: wss:; worker-src 'self' blob:; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

The frontend generates a new unpredictable nonce for each request. Next applies it to framework, page, and inline runtime scripts. This avoids script-src 'unsafe-inline'. Development mode adds script-src 'unsafe-eval' because the Next development runtime requires it. style-src 'unsafe-inline' remains necessary for framework and application inline styles.

Nonce propagation requires dynamic rendering, so Next static optimization, ISR, and default CDN document caching are disabled. Static assets remain cacheable. The policy contains no deployment hostname. Browser API, cover-art, provider-thumbnail, playback, and Listen Together requests use same-origin frontend proxy paths by default. Enforcing CSP does not support a separately originated NEXT_PUBLIC_API_URL; use NEXT_PUBLIC_API_PATH_MODE=proxy or the default unset configuration before enforcement.

The policy defaults to Content-Security-Policy-Report-Only. Use this rollout sequence:

  1. Leave CSP_ENFORCE unset or set it to false.
  2. Review browser console violations during normal playback, Listen Together, offline/PWA use, unauthenticated share pages, and administrator workflows.
  3. Fix or explicitly account for every required source.
  4. Set CSP_ENFORCE=true and restart the frontend or AIO container.
  5. Repeat the same workflow checks against the enforcing header.

Set CSP_REPORT_URI to a root-relative path handled by your reverse proxy or to an HTTPS collector URL to receive reports. soundspan does not include a report collector. When configured, the frontend emits both the legacy report-uri directive and the report-to directive with a matching Reporting-Endpoints header. Treat reports as potentially sensitive because browsers can include document and blocked-resource URLs. Values containing header delimiters, credentials, fragments, unsupported schemes, or protocol-relative URLs are ignored.

SVG image optimization exception

images.dangerouslyAllowSVG remains enabled for cover-art compatibility. The Next image optimizer is explicitly configured to return optimized images with Content-Disposition: attachment and this image-specific CSP:

default-src 'self'; script-src 'none'; frame-src 'none'; sandbox;

This sandbox applies to optimized image responses independently of the document policy. External MusicBrainz, Last.fm, Deezer, podcast RSS, TIDAL, YouTube Music, and other provider images are fetched through the backend cover/browse proxy or the same-origin Next image optimizer. The browser document policy therefore does not allow provider image hosts directly.

Sensitive Variables

Never commit .env files or credentials.

Variable Purpose Required
SESSION_SECRET Required JWT signing fallback (32+ chars) Yes
SETTINGS_ENCRYPTION_KEY Encryption of stored credentials (32+ chars, must not be the published default) Yes
INTERNAL_API_SECRET Service-to-service authentication (32+ chars) Yes
METRICS_TOKEN Bearer authentication for backend and worker Prometheus scrapes Required to scrape unless metrics are explicitly public
POSTGRES_PASSWORD PostgreSQL authentication Yes, production
API_KEY_PEPPER HMAC pepper for API keys; keep stable or existing hashed keys become invalid Optional; falls back to encryption/JWT secrets
OIDC_CLIENT_SECRET OIDC confidential client authentication If using OIDC
LIDARR_API_KEY Lidarr integration If using Lidarr
OPENAI_API_KEY AI features Optional
LASTFM_API_KEY Artist recommendations Optional
LASTFM_SHARED_SECRET Signed Last.fm user authentication and scrobble forwarding If using Last.fm scrobbling
FANART_API_KEY Artist images Optional
YTMUSIC_STREAMER_URL YouTube Music sidecar URL If using YouTube Music
TIDAL_SIDECAR_URL TIDAL sidecar URL If using TIDAL

Soulseek credentials are configured via System Settings and stored encrypted in the database. Last.fm lookups require a key you provide. Set LASTFM_API_KEY in the environment or store a key in System Settings when you want Last.fm-backed recommendations and metadata; otherwise those lookups remain unavailable. Last.fm scrobble forwarding also requires LASTFM_SHARED_SECRET as a runtime secret. Per-user Last.fm session keys and ListenBrainz tokens are encrypted with SETTINGS_ENCRYPTION_KEY before database storage and are never returned by status endpoints.

Metrics exposure

GET /metrics is private by default. It accepts only a bearer token matching METRICS_TOKEN and fails closed when that value is absent. Keep the endpoint on an internal network, store the token as a runtime secret, and configure the scraper to send it in the Authorization header.

METRICS_PUBLIC=true removes this gate. Treat that setting as unsafe outside an isolated private network. The exposition includes process health, route classes, queue names and state, cache behavior, and federation sync outcomes. It never uses user IDs, media IDs, raw URLs, cache keys, peer IDs, or error text as labels.

Authentication and Credential Security

Every authenticated request uses an explicit credential transport — there is no cookie-session authentication, so an ambient cookie cannot silently take precedence over a bearer token or API key and the API has no ambient-cookie CSRF credential surface. The HTTP-only OIDC flow-binding cookie remains; it binds a login transaction to its initiating browser and does not authenticate API requests. The one deliberate exception is the queue dashboard: Bull Board is a server-rendered page that cannot carry a bearer token, so Open queue dashboard on the Admin page mints a 15-minute, HTTP-only, SameSite=Strict cookie whose path is limited to /api/admin/queues. Only that mount accepts it, only for administrators, and the token inside it carries a dashboard-only purpose claim that every other route rejects.

Surface Credential transport Lifetime Revocation path
Web UI and first-party API Authorization: Bearer JWT access token plus refresh token in the refresh request body Access: 24 hours. Refresh: 30 days. A self-service password change or administrator-set password increments tokenVersion, invalidating outstanding access and refresh JWTs. Ordinary logout removes the current browser's tokens only; there is no separate logout-all-devices endpoint.
OpenSubsonic /rest Per-request token plus salt, password transport, or an ssap_ app password; API keys use the separate apiKey parameter The token is a per-request digest with no independent server-side lifetime. App-password credentials remain valid until revoked. The legacy dedicated Subsonic password is deprecated: it cannot be set from Settings, but direct API updates remain possible and existing stored values are honored. Revoke an app password individually. Account password changes and administrator-set passwords clear any legacy dedicated Subsonic password.
External API clients X-API-Key header 90 days from creation Delete the API key.
Queue dashboard (/api/admin/queues) Bearer token, API key, or the path-scoped dashboard cookie minted by POST /api/admin/queues/session Cookie: 15 minutes. DELETE /api/admin/queues/session (End dashboard access on the Admin page), or let it expire.
Federation peer API Dedicated opaque Authorization: Bearer peer credential No automatic expiry. Rotate the credential, revoke the peer, or delete the peer.
Internal sidecar requests x-internal-secret header No automatic expiry. Rotate INTERNAL_API_SECRET across the backend and sidecars, then restart them.

Reverse Proxy Trust and Rate Limits

soundspan is designed to run behind a reverse proxy. The backend uses its configured Express trust proxy policy, so the trusted X-Forwarded-For chain determines the client IP used by per-IP rate limits. Some authentication, registration, OIDC, OpenSubsonic, share-link, webhook, and federation endpoints can be unauthenticated and exposed to the internet. Set TRUST_PROXY_HOPS to the actual number of trusted proxy hops. Do not leave a permissive proxy chain in a deployment where clients can connect to the backend directly and supply their own forwarding headers.

Security-relevant counters use the existing REDIS_URL connection. This makes authentication and account-management, admin and invite-management, OIDC, OpenSubsonic authentication, share-link, webhook, and federation limits common to every backend replica and preserves them across backend restarts. Redis keys are separated by limiter name. A Redis command has a 250 ms deadline. If Redis is disconnected or a command fails or times out, credential-guarding limits fall back to an in-process sliding window of up to 10,000 keys and one million retained hit timestamps per limiter, while other Redis-backed limits remain availability-first and allow the request. The fallback is per backend process during an outage, so replicas enforce separate budgets until Redis recovers; each fallback decision emits a rate-limited warning.

The general high-ceiling API limiter and high-volume playback, image, download, lyrics, and provider-request limiters remain per-process and in memory. They are hot-path safeguards against client bugs, accidental loops, bandwidth bursts, and upstream-provider overload. They are not the distributed abuse-control boundary, and avoiding Redis on those paths keeps their latency and Redis load bounded.

OIDC and App-Password Security

soundspan uses the OIDC Authorization Code flow with state, nonce, and S256 PKCE. Login and account-link attempts keep their pending state on the server. The callback sends the browser a short-lived, single-use exchange code. It never puts access or refresh tokens in a redirect URL.

OIDC requires the web app and API to be same-site. A single public hostname needs no extra redirect setting. A same-site callback on a sibling API subdomain or different port must set OIDC_WEB_BASE_URL to the canonical web origin. Cross-site deployments on different registrable domains are not supported because the flow-binding cookie is SameSite=Lax.

OpenSubsonic app passwords are restricted to /rest. Their ssap_ secrets are encrypted at rest with authenticated encryption under SETTINGS_ENCRYPTION_KEY. Users can revoke each app password independently.

See the OIDC_SSO.md topology matrix for supported layouts. The same guide covers provider setup, account linking, provisioning, role ownership, app passwords, MFA, and break-glass recovery.

Streaming Credential Security

Federation Credential Security

Network egress policy remains a defense-in-depth control. Administrators must treat a linked peer as trusted and limit admin access accordingly.

Webhook and Admin Security

Optional VPN Notes

If using Mullvad VPN for Soulseek:

Generating Secrets

# Required JWT signing fallback
openssl rand -base64 32

# Settings encryption key
openssl rand -base64 32

Network Safety Guidance


See also