feat: native HTTPS support with bundled snakeoil default cert
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
This commit is contained in:
@@ -661,6 +661,9 @@ The status panel refreshes on a fixed 5-second timer and shows each SSE client w
|
||||
| `DATA_DIR` | No | `./data` | Directory for `tokens.json` and `server.log`. Must be writable by the server process. In Docker: `/app/data` (named volume). |
|
||||
| `COOKIE_SECRET` | No | — | If set, signs all session cookies with HMAC-SHA256. Strongly recommended in production. |
|
||||
| `TRUST_PROXY` | No | — | Express `trust proxy` setting. Set to `1` (or a hop count) when behind a reverse proxy (nginx, Traefik, etc.) so `req.ip` and `req.secure` are correct. |
|
||||
| `TLS_ENABLED` | No | `true` | Set to `false` to disable HTTPS and run plain HTTP (e.g. when TLS is terminated by a reverse proxy). |
|
||||
| `TLS_CERT` | No | `certs/snakeoil.crt` | Path to the TLS certificate file (PEM). Defaults to the bundled self-signed snakeoil certificate. |
|
||||
| `TLS_KEY` | No | `certs/snakeoil.key` | Path to the TLS private key file (PEM). Defaults to the bundled snakeoil key. |
|
||||
|
||||
#### Emby
|
||||
|
||||
@@ -726,6 +729,7 @@ The production image uses a two-stage build on `node:22-alpine`:
|
||||
Key environment variables set in the image:
|
||||
- `NODE_ENV=production` — enables production startup validation and logging
|
||||
- `DATA_DIR=/app/data` — token store and log file location
|
||||
- TLS is **enabled by default** using the bundled snakeoil self-signed certificate (`certs/snakeoil.crt`). Set `TLS_CERT`/`TLS_KEY` to your own certificate, or set `TLS_ENABLED=false` when terminating TLS at a reverse proxy.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
@@ -736,12 +740,17 @@ services:
|
||||
container_name: sofarr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3001"
|
||||
- "3001:3001" # HTTPS by default (snakeoil cert if no TLS_CERT set)
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATA_DIR=/app/data
|
||||
- COOKIE_SECRET=change-me-to-a-long-random-string
|
||||
- TRUST_PROXY=1 # set if behind nginx/Traefik
|
||||
# Option A: direct TLS (default). Supply your own cert/key:
|
||||
# - TLS_CERT=/app/certs/server.crt
|
||||
# - TLS_KEY=/app/certs/server.key
|
||||
# Option B: behind a TLS-terminating reverse proxy:
|
||||
# - TLS_ENABLED=false
|
||||
# - TRUST_PROXY=1
|
||||
- EMBY_URL=https://emby.example.com
|
||||
- EMBY_API_KEY=your-emby-api-key
|
||||
- SONARR_INSTANCES=[{"name":"main","url":"...","apiKey":"..."}]
|
||||
@@ -751,7 +760,10 @@ services:
|
||||
- POLL_INTERVAL=5000
|
||||
- LOG_LEVEL=info
|
||||
volumes:
|
||||
- sofarr-data:/app/data # persists tokens.json and server.log
|
||||
- sofarr-data:/app/data
|
||||
# Uncomment to supply your own certificate (Option A):
|
||||
# - /path/to/server.crt:/app/certs/server.crt:ro
|
||||
# - /path/to/server.key:/app/certs/server.key:ro
|
||||
|
||||
volumes:
|
||||
sofarr-data:
|
||||
@@ -759,10 +771,10 @@ volumes:
|
||||
|
||||
### Security hardening checklist
|
||||
|
||||
- **Use HTTPS** — TLS is on by default (snakeoil cert). Supply `TLS_CERT`/`TLS_KEY` pointing to a CA-signed certificate for trusted HTTPS. Alternatively terminate TLS at a reverse proxy and set `TLS_ENABLED=false` + `TRUST_PROXY=1`.
|
||||
- **Set `COOKIE_SECRET`** — enables HMAC-signed cookies, preventing client-side forgery.
|
||||
- **Set `TRUST_PROXY=1`** when behind a reverse proxy — ensures `req.secure` is `true` so the `secure` cookie flag is enforced and HTTPS-upgrade CSP fires.
|
||||
- **Set `TRUST_PROXY=1`** only when a TLS-terminating reverse proxy sits in front — ensures `req.secure` is correct and the CSP `upgrade-insecure-requests` + `secure` cookie flag fire correctly.
|
||||
- **Mount a named volume** for `DATA_DIR` — token store and log file survive container recreates.
|
||||
- **Use HTTPS** — set `TRUST_PROXY=1` to enable the CSP `upgrade-insecure-requests` directive, the `secure` cookie flag, and HSTS (1-year `maxAge`).
|
||||
- **Rate limiting** — the login endpoint is limited to 10 failed attempts per IP per 15 minutes; all API endpoints share a 300 req/15 min window. Set `TRUST_PROXY` correctly so the client IP is not the proxy's IP.
|
||||
|
||||
### CI / CD
|
||||
|
||||
Reference in New Issue
Block a user