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
231 lines
6.9 KiB
Markdown
231 lines
6.9 KiB
Markdown
# 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.
|