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

6.9 KiB

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 for the full list. There is no config file.

Running natively

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 — install it with:

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:

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

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.

Example Compose file (also in docker-compose.yml):

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.

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

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

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 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)

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 for that check in detail.