Complete Phase 4: security hardening, player/playlist/admin UI, docs
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
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
# Deployment
|
||||
|
||||
VibeSonic is a single static binary plus `ffmpeg`/`ffprobe` on `PATH`
|
||||
(and optionally `mpv`, for the jukebox feature). Docker is convenient
|
||||
but never required for normal operation.
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is via `VIBESONIC_*` environment variables — see
|
||||
[`docs/configuration.md`](configuration.md) for the full list. There is
|
||||
no config file.
|
||||
|
||||
## Running natively
|
||||
|
||||
```sh
|
||||
go build -o vibesonic ./cmd/vibesonic
|
||||
|
||||
export VIBESONIC_DATA_DIR=/var/lib/vibesonic
|
||||
export VIBESONIC_MUSIC_DIRS=/music
|
||||
export VIBESONIC_SESSION_SECRET="$(openssl rand -hex 32)"
|
||||
./vibesonic
|
||||
```
|
||||
|
||||
For a persistent deployment, see
|
||||
[`systemd/vibesonic.service`](../systemd/vibesonic.service) — install it
|
||||
with:
|
||||
|
||||
```sh
|
||||
sudo useradd --system --home /var/lib/vibesonic --shell /usr/sbin/nologin vibesonic
|
||||
sudo install -m 0755 vibesonic /usr/local/bin/vibesonic
|
||||
sudo cp systemd/vibesonic.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now vibesonic
|
||||
```
|
||||
|
||||
The unit runs as a dedicated `vibesonic` user (not `DynamicUser`,
|
||||
because it needs a stable UID with read access to your music library —
|
||||
`DynamicUser` allocates a random UID per start, which doesn't compose
|
||||
well with a fixed set of file permissions on external media). Grant that
|
||||
user read access to your music folders:
|
||||
|
||||
```sh
|
||||
sudo usermod -aG <group-that-owns-your-music> vibesonic
|
||||
# or, more simply, make the music tree group- or world-readable
|
||||
```
|
||||
|
||||
`StateDirectory=vibesonic` in the unit creates and owns
|
||||
`/var/lib/vibesonic` (mode `0700`) for you — don't create it manually
|
||||
with different permissions first. The unit also applies standard
|
||||
hardening (`ProtectSystem=strict`, `NoNewPrivileges`, `PrivateDevices`,
|
||||
etc.) and `Restart=on-failure`.
|
||||
|
||||
Remember to set a real `VIBESONIC_SESSION_SECRET` in the unit (or in an
|
||||
`EnvironmentFile=`) — the shipped example is a placeholder.
|
||||
|
||||
## Running with Docker
|
||||
|
||||
```sh
|
||||
docker build -t vibesonic .
|
||||
```
|
||||
|
||||
Runtime image is `debian:bookworm-slim` with `ffmpeg`, `mpv`, and CA
|
||||
certificates installed, running as a non-root `vibesonic` user (uid
|
||||
10001). See [`Dockerfile`](../Dockerfile).
|
||||
|
||||
Example Compose file (also in [`docker-compose.yml`](../docker-compose.yml)):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
vibesonic:
|
||||
image: vibesonic
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./data:/var/lib/vibesonic
|
||||
- /mnt/music:/music:ro
|
||||
environment:
|
||||
VIBESONIC_SESSION_SECRET: "change-me-to-a-random-string"
|
||||
```
|
||||
|
||||
Mount your music read-only (`:ro`) — VibeSonic never writes to it.
|
||||
`./data` (or a named volume) holds the SQLite database, cover art cache,
|
||||
and downloaded podcast episodes; back it up per
|
||||
[`docs/backup.md`](backup.md).
|
||||
|
||||
## Reverse proxy
|
||||
|
||||
Run VibeSonic behind a reverse proxy for TLS termination in any
|
||||
real deployment; the examples below forward to VibeSonic on
|
||||
`127.0.0.1:8080`.
|
||||
|
||||
VibeSonic does not use WebSockets. Streaming and transcoded responses
|
||||
are ordinary long-lived HTTP responses using standard range requests —
|
||||
make sure your proxy doesn't buffer the entire response body before
|
||||
forwarding it (buffering breaks range requests and adds needless
|
||||
latency to playback start).
|
||||
|
||||
### Caddy
|
||||
|
||||
```
|
||||
music.example.com {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
```
|
||||
|
||||
Caddy handles TLS automatically and streams request/response bodies
|
||||
without buffering by default — no extra configuration needed for range
|
||||
requests or large responses.
|
||||
|
||||
### nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name music.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/music.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/music.example.com/privkey.pem;
|
||||
|
||||
client_max_body_size 0;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`proxy_buffering off` matters here: nginx buffers proxied responses to
|
||||
disk/memory by default, which delays the start of playback and range
|
||||
responses. `client_max_body_size 0` avoids nginx rejecting large file
|
||||
uploads if the upload feature is used.
|
||||
|
||||
### Traefik
|
||||
|
||||
```yaml
|
||||
http:
|
||||
routers:
|
||||
vibesonic:
|
||||
rule: "Host(`music.example.com`)"
|
||||
service: vibesonic
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
services:
|
||||
vibesonic:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://127.0.0.1:8080"
|
||||
```
|
||||
|
||||
Traefik streams responses without buffering by default, same as Caddy.
|
||||
|
||||
## Reverse proxy + external (trusted-proxy) authentication
|
||||
|
||||
If you're using [`docs/external-auth.md`](external-auth.md) to have the
|
||||
proxy handle SSO (OIDC, SAML, HTTP Basic Auth, ...) for the browser UI,
|
||||
two things are essential:
|
||||
|
||||
1. **The proxy must strip any client-supplied identity header before
|
||||
inserting its own.** If it doesn't, a client can set
|
||||
`Remote-User: admin` (or whatever header name you configured) on its
|
||||
own request and have the proxy forward it unchanged, impersonating
|
||||
any user. Every proxy example below includes this.
|
||||
|
||||
2. **`/rest/*` must bypass the proxy's SSO layer.** Real Subsonic/
|
||||
OpenSubsonic clients (mobile apps, desktop players) authenticate
|
||||
directly against VibeSonic's own token/API-key scheme and have no way
|
||||
to complete a browser-based SSO flow (OIDC redirect, SAML POST,
|
||||
etc.). Exclude `/rest/*` from the SSO requirement so those clients
|
||||
keep working, while still letting `/rest/*` reach VibeSonic normally
|
||||
— VibeSonic's own `/rest/*` auth is independent of external auth
|
||||
either way.
|
||||
|
||||
### Caddy example (with `forward_auth` for SSO + header stripping)
|
||||
|
||||
```
|
||||
music.example.com {
|
||||
# Strip any client-supplied identity header before forward_auth
|
||||
# potentially sets a real one, so a client can't forge it.
|
||||
header_down -Remote-User
|
||||
|
||||
@needsAuth {
|
||||
not path /rest/*
|
||||
}
|
||||
forward_auth @needsAuth auth.example.com:9091 {
|
||||
uri /verify
|
||||
copy_headers Remote-User
|
||||
}
|
||||
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
```
|
||||
|
||||
### nginx example (with `auth_request` for SSO + header stripping)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
# ... TLS config as above ...
|
||||
|
||||
location /rest/ {
|
||||
# Subsonic/OpenSubsonic clients: no SSO, straight through.
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Remote-User ""; # never forward a client-supplied value
|
||||
}
|
||||
|
||||
location / {
|
||||
auth_request /sso-verify;
|
||||
auth_request_set $remote_user $upstream_http_remote_user;
|
||||
proxy_set_header Remote-User $remote_user;
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
|
||||
location = /sso-verify {
|
||||
internal;
|
||||
proxy_pass http://auth.example.com/verify;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Either way, also set `VIBESONIC_EXTAUTH_TRUSTED_SOURCES` to only the
|
||||
proxy's own address (e.g. `127.0.0.1/32` if it's on the same host) —
|
||||
VibeSonic independently verifies the real TCP peer, so even a
|
||||
misconfigured proxy can't be used to spoof identity from the internet
|
||||
directly. See [`docs/external-auth.md`](external-auth.md) for that
|
||||
check in detail.
|
||||
Reference in New Issue
Block a user