Security: - New internal/netguard package: podcast feed/enclosure fetching now refuses to dial non-public IPs (loopback/RFC1918/link-local/cloud metadata/CGNAT), validated per-connection including across redirects, closing an SSRF gap where a malicious or compromised feed URL could probe the server's internal network. Configurable via VIBESONIC_PODCASTS_ALLOW_PRIVATE_NETWORKS for intentional LAN feeds. - Podcast episode downloads capped at 4GB (was unbounded, disk-exhaustion risk) - Cover art decoding now rejects images above a 40-megapixel budget before full decode (decompression-bomb guard) - HTTP server sets ReadHeaderTimeout (slowloris hardening) - Admin folder-scan redirect now validated as same-site instead of trusting the raw Referer header (open-redirect fix) - Legacy Subsonic token/salt auth now uses a constant-time comparison Web UI completion: - Persistent player: shuffle and repeat (off/all/one) controls, wired into queue advance/prev logic and persisted like other player state - Admin user-permissions form: adminRole toggle (with a guard against demoting the last remaining admin) and per-user music-folder access checkboxes, backed by existing schema/enforcement that was previously unreachable from the UI - Playlist M3U8 import/export Docs: added docs/client-compatibility.md (honest tracking template — no client testing has been performed), docs/deployment.md, docs/security.md, docs/backup.md; refreshed README and cross-links; added a footnote in docs/subsonic-compatibility.md distinguishing spec-standard from VibeSonic-original OpenSubsonic extensions. Also added Go fuzz tests for the external-auth CIDR/IP parser and the LRC lyrics parser. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
69 lines
3.6 KiB
Markdown
69 lines
3.6 KiB
Markdown
# External authentication
|
|
|
|
VibeSonic supports optional Navidrome-style externalized authentication:
|
|
a trusted reverse proxy authenticates the browser user (via whatever
|
|
mechanism it likes — OIDC, SAML, HTTP Basic Auth, a VPN identity, ...)
|
|
and asserts that identity to VibeSonic via an HTTP header. **VibeSonic
|
|
never implements OIDC or talks to an identity provider itself** — the
|
|
proxy handles authentication, VibeSonic handles users and authorization.
|
|
|
|
## Configuration
|
|
|
|
| Variable | Default | Description |
|
|
|---|---|---|
|
|
| `VIBESONIC_EXTAUTH_ENABLED` | `false` | Enable external authentication |
|
|
| `VIBESONIC_EXTAUTH_TRUSTED_SOURCES` | *(none)* | Comma-separated list of IPv4/IPv6 CIDRs (or bare IPs) that are trusted to assert identity, e.g. `127.0.0.1/32,10.0.0.5/32` |
|
|
| `VIBESONIC_EXTAUTH_USER_HEADER` | `Remote-User` | Header name carrying the authenticated username |
|
|
| `VIBESONIC_EXTAUTH_LOGOUT_URL` | *(none)* | Optional URL to send users to on logout (e.g. the proxy's own logout endpoint), instead of VibeSonic's local `/login` |
|
|
| `VIBESONIC_EXTAUTH_AUTO_CREATE_USERS` | `true` | Automatically create a VibeSonic user the first time a trusted identity is seen |
|
|
|
|
## The critical security property
|
|
|
|
**The identity header is only trusted when the actual TCP connection
|
|
peer address matches a configured trusted CIDR.** This check
|
|
(`internal/auth/extauth.go`) reads `http.Request.RemoteAddr` — the real
|
|
socket peer — never a header.
|
|
|
|
Headers like `X-Forwarded-For`, `X-Real-IP`, or `Forwarded` are
|
|
**never** consulted to decide whether the peer is trusted. Those headers
|
|
are attacker-controlled: any client can set them to anything. If
|
|
VibeSonic trusted them, an arbitrary internet client could add
|
|
`X-Forwarded-For: 127.0.0.1` and a `Remote-User: admin` header to its own
|
|
direct request and be treated as an authenticated administrator.
|
|
|
|
This is why `VIBESONIC_EXTAUTH_TRUSTED_SOURCES` has **no default** — not
|
|
`0.0.0.0/0`, not `::/0`, not RFC1918 ranges. External auth trusts nothing
|
|
until you explicitly name a network (typically `127.0.0.1/32` for a
|
|
proxy on the same host, or the proxy's specific address on a Docker
|
|
bridge network).
|
|
|
|
See `internal/auth/extauth_test.go` for the regression tests covering
|
|
this, including a test that a spoofed `X-Forwarded-For`/`X-Real-IP`/
|
|
`Forwarded` header from an untrusted peer is ignored entirely.
|
|
|
|
## How it resolves to a user
|
|
|
|
A trusted identity resolves to an ordinary VibeSonic user (with
|
|
`external_auth = 1`), never a separate "external user" type. If
|
|
`VIBESONIC_EXTAUTH_AUTO_CREATE_USERS=true` and no user with that username
|
|
exists yet, one is created with no local password. If a user with that
|
|
username already exists (created locally or by a previous external
|
|
login), it's reused.
|
|
|
|
## Interaction with native auth
|
|
|
|
External auth and native session/password login coexist: the
|
|
`Authenticate` middleware tries the session cookie first, then falls back
|
|
to the trusted-proxy header. A user can still log in with a local
|
|
password on VibeSonic's own `/login` page even when external auth is
|
|
enabled, unless their account has no password hash set (auto-created
|
|
external users start this way).
|
|
|
|
`/rest/*` (the Subsonic/OpenSubsonic API) never consults external auth
|
|
at all: it authenticates exclusively via Subsonic's own token/salt,
|
|
password, or `apiKey` scheme (see `internal/subsonic/auth.go`), so real
|
|
Subsonic/OpenSubsonic clients keep working unmodified even when external
|
|
auth fronts the browser UI. This is also why a reverse proxy enforcing
|
|
SSO in front of VibeSonic should let `/rest/*` bypass that SSO layer —
|
|
see `docs/deployment.md` for example proxy configuration.
|