diff --git a/.env.example b/.env.example index 5e6fab3..121dff4 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,12 @@ DATABASE_URL=postgresql://lyra:lyra@db:5432/lyra # 32 random bytes, base64. Generate with: openssl rand -base64 32 LYRA_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= +# Shared password protecting the whole web UI + API. When set, every request must sign in +# (one shared password, no per-user accounts). LEAVE UNSET AND THE APP IS OPEN to anyone +# who can reach the host — only safe on a trusted LAN or behind a VPN/auth proxy. Strongly +# recommended for any exposed deployment. See README "Security". +# LYRA_PASSWORD=change-me + # Host directory for the downloaded music library (bind-mounted into the worker at /music) MUSIC_DIR=./music diff --git a/README.md b/README.md index e785ae3..ea98ea1 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,20 @@ docker compose start web worker `--clean --if-exists` drops and recreates each object, so restoring over an existing DB is safe. To restore onto a brand-new host, bring up just `db` first (`docker compose up -d db`), restore, then `up -d --build` the rest. + +## Security + +Lyra has **no per-user accounts**. Two layers protect it: + +- **Shared-password gate.** Set `LYRA_PASSWORD` in `.env` to require a single shared + password for the entire UI and API. A request without a valid session cookie is + redirected to `/login` (or gets `401` for `/api/*`). The cookie is httpOnly and carries + an HMAC of the password keyed by `LYRA_SECRET_KEY`, so it can't be forged. **If + `LYRA_PASSWORD` is unset, the app is completely open** — anyone who can reach the host + can browse the library, trigger downloads, and read/overwrite the stored credentials in + Settings. Set it for any deployment that isn't on a fully trusted, isolated network. + +- **Network.** The shared password is deliberately minimal (single password, bearer + cookie, plain HTTP on the LAN). For anything internet-facing, also put Lyra behind a + reverse proxy with TLS and/or a VPN or auth gateway — don't rely on the shared password + alone. diff --git a/docker-compose.yml b/docker-compose.yml index afe68db..8d415aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,8 @@ services: environment: DATABASE_URL: ${DATABASE_URL} LYRA_SECRET_KEY: ${LYRA_SECRET_KEY} + # Optional shared-password gate for the whole UI/API. Unset ⇒ app is open. See README. + LYRA_PASSWORD: ${LYRA_PASSWORD:-} ports: # host:container — host 8770 avoids the Quill app on 3000 (and Lidarr on 8686) - "8770:3000" diff --git a/web/src/app/_ui/contents-nav.tsx b/web/src/app/_ui/contents-nav.tsx index a6c88b3..3c31c38 100644 --- a/web/src/app/_ui/contents-nav.tsx +++ b/web/src/app/_ui/contents-nav.tsx @@ -13,6 +13,7 @@ const LINKS: { href: string; label: string }[] = [ export function ContentsNav() { const pathname = usePathname(); + if (pathname === "/login") return null; // no section nav on the login screen const active = (href: string) => (href === "/" ? pathname === "/" : pathname.startsWith(href)); return (