Commit Graph

5 Commits

Author SHA1 Message Date
da0898f52a feat: native HTTPS support with bundled snakeoil default cert
All checks were successful
Build and Push Docker Image / build (push) Successful in 32s
CI / Security audit (push) Successful in 48s
CI / Tests & coverage (push) Successful in 56s
server/index.js:
- Import http and https modules
- Resolve TLS_ENABLED early (before Helmet) so upgradeInsecureRequests
  CSP directive fires when TLS is active directly (not only via proxy)
- loadTlsCredentials() reads TLS_CERT/TLS_KEY (defaulting to bundled
  snakeoil) and returns null on failure (graceful HTTP fallback)
- Start https.createServer or http.createServer depending on credentials
- Startup banner now shows protocol, TLS cert path, and snakeoil warning

certs/:
- Add bundled snakeoil self-signed certificate (RSA 2048, 10yr, SAN for
  localhost + 127.0.0.1) for out-of-the-box HTTPS without configuration
- .gitignore allows only snakeoil.{crt,key} — real certs must not be
  committed

Dockerfile:
- COPY certs/ into image so snakeoil default is always available
- HEALTHCHECK updated to https:// with --no-check-certificate

docker-compose.yaml:
- Port now exposes HTTPS directly by default
- TLS_CERT/TLS_KEY/TLS_ENABLED/TRUST_PROXY documented with Option A/B
- cert volume mount examples added (commented out)
- healthcheck updated to https with --no-check-certificate

.env.sample:
- New TLS/HTTPS section with TLS_ENABLED, TLS_CERT, TLS_KEY
- openssl self-signed cert generation example included

docs/ARCHITECTURE.md:
- Configuration table: TLS_ENABLED, TLS_CERT, TLS_KEY env vars added
- Docker image section: TLS default behaviour documented
- Docker Compose example: Option A (direct TLS) / Option B (proxy) layout
- Security checklist: HTTPS now first item, updated for TLS modes
2026-05-17 10:50:38 +01:00
bdbbcabfbc feat(security): production hardening for external deployment
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m2s
CI / Security audit (push) Successful in 3m29s
Container (Dockerfile):
- Multi-stage build (deps + runtime) for minimal attack surface
- Upgrade base image from node:18-alpine to node:22-alpine
- Run as non-root 'node' user (UID 1000); source files owned by root
- /app/data directory owned by node for SQLite + logs
- Docker HEALTHCHECK: wget /health every 30s

docker-compose.yaml:
- Port bound to 127.0.0.1 only (expose via reverse proxy)
- read_only: true filesystem; /tmp tmpfs for Node.js
- no-new-privileges:true, cap_drop: ALL
- Named volume sofarr-data for persistent data
- TRUST_PROXY, COOKIE_SECRET, NODE_ENV added

Helmet v7 + CSP nonce:
- Upgrade helmet@4 → helmet@7, express-rate-limit@6 → @7
- CSP with per-request nonce injected into index.html script/link tags
  (replaces blanket unsafe-inline; nonce changes every request)
- HSTS: max-age=1yr, includeSubDomains, preload
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: camera/mic/geolocation/payment/usb all off
- index.html served dynamically with nonce injection; static assets
  served normally via express.static({index:false})

Trust proxy:
- TRUST_PROXY env var configures app.set('trust proxy') so rate
  limiting and secure cookies work correctly behind Nginx/Caddy

Session & auth:
- Token store migrated from in-memory Map to SQLite via better-sqlite3
  (server/utils/tokenStore.js): survives restarts, WAL mode, 31-day TTL
- CSRF double-submit cookie pattern (server/middleware/verifyCsrf.js):
  POST/PUT/PATCH/DELETE on /api/* require X-CSRF-Token header matching
  the csrf_token cookie; timing-safe comparison
- CSRF token issued on login + GET /api/auth/csrf; cleared on logout
- Login input validation: username/password length + type checked before
  hitting Emby
- skipSuccessfulRequests:true on login rate limiter (only count failures)
- express.json({ limit: '64kb' }) to reject oversized payloads

Rate limiting:
- General API limiter: 300 req/15min per IP on all /api/* routes
- Login limiter unchanged (10 failures/15min) but now only counts fails

Logging:
- Log file moved from /app/server.log to DATA_DIR/server.log (writable
  by non-root node user in container)
- Size-based rotation: rotate at 10 MB, keep 3 files (server.log.1-3)
- DATA_DIR defaults to ./data locally, /app/data in container

Error handling:
- Global Express error handler: catches unhandled errors, logs message,
  returns generic 500 (no stack traces to clients)

Health/readiness:
- GET /health: returns {status:'ok', uptime:N} — used by HEALTHCHECK
- GET /ready: returns 503 if EMBY_URL not configured

Error sanitization (sanitizeError.js):
- Added patterns for password= params, bearer tokens, Basic auth in URLs

Supply chain:
- Remove unused cors dependency
- add better-sqlite3@^9
- CI: upgrade to Node 22, raise audit level to --audit-level=high
- .gitignore: add data/, *.db, *.db-wal, *.db-shm

Docs:
- SECURITY.md: threat model, hardening checklist, proxy examples,
  header table, rate limit table, Docker secrets guidance
- .env.example + .env.sample: TRUST_PROXY, DATA_DIR documented
2026-05-17 06:47:25 +01:00
663826e295 chore: add COOKIE_SECRET to .env, .env.example, .env.sample
Some checks failed
Build and Push Docker Image / build (push) Successful in 41s
CI / npm audit (push) Failing after 43s
Generated a 64-char hex secret (openssl rand -hex 32 equivalent) and
added it to .env. Updated .env.example and .env.sample with the new
required variable and a generation hint. This is the production secret
for HMAC-signing the emby_user session cookie.
2026-05-16 17:07:43 +01:00
e5b2fc8ea4 docs: add POLL_INTERVAL to README, .env.sample, and .env.example
- Document background polling and on-demand mode in README
- Add POLL_INTERVAL to all config examples (Docker, Compose, .env)
- New 'Background Polling' section in Features
- Sanitize leaked credentials in .env.example
2026-05-16 00:32:16 +01:00
f500f4db3b feat: fix download-to-user matching, add cover art to downloads
- Fix seriesMap key (use Sonarr internal id, not tvdbId)
- Fix Sonarr tag resolution (use tag map like Radarr)
- Use sourceTitle for history record matching
- Fall back to embedded movie/series objects when API timeouts
- Add includeMovie/includeSeries params to queue/history API calls
- Add coverArt field to all download responses (TMDB poster URLs)
- Add cover art display to frontend download cards
- Fix user-summary route to use instance config and tag maps
2026-05-15 14:54:21 +01:00