Files
vibesonic/docs/deployment.md
T
owenandClaude Sonnet 5 789bf91eed 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
2026-08-23 09:18:17 -05:00

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.