Security Model
Luminarr is designed for self-hosted environments where you control the machine, the network, and the data. Its security model is layered: strong authentication at the boundary, careful handling of secrets throughout, and explicit protection against server-side request forgery for every outbound connection.
API Authentication
The web UI works without any API key — the server trusts same-origin browser requests
using the Sec-Fetch-Site header, a Fetch Metadata Request Header that
browsers set automatically and JavaScript cannot spoof. Cross-origin requests send
cross-site, so CSRF is handled automatically.
External API consumers (scripts, Home Assistant, other *arr apps) authenticate with
an X-Api-Key header. The key is generated automatically on first start
using Go's crypto/rand — 32 bytes of cryptographically random data,
rendered as a 64-character hex string. Find it in Settings → General.
Sec-Fetch-Site: same-originX-Api-Key headerKey behaviour
- Key is generated with
crypto/rand.Read— no Math.random, no time seeds. - Comparison uses
crypto/subtle.ConstantTimeCompareto prevent timing attacks regardless of key length or match prefix. - The
GET /healthendpoint is intentionally unauthenticated so load balancers and container orchestrators can probe liveness without credentials. - The API key persists in
/config/config.yamland is stable across restarts. - The config API returns a masked key (
****…); the full key is only available via a separate/api/v1/system/config/apikeyendpoint.
WebSocket authentication
WebSocket connections go through the same auth middleware as REST endpoints. Browser
connections must include Sec-Fetch-Site: same-origin; external clients
must send a valid X-Api-Key header. The auth check runs before
the HTTP-to-WebSocket upgrade — unauthenticated connections are rejected at the HTTP
level and never reach the WebSocket handler.
Import path validation
Content paths submitted through the import API are validated using
pathutil.ValidateContentPath, which rejects path traversal attempts
(.. components), symlink escapes, and paths outside allowed directories.
This prevents a compromised download client from writing files to arbitrary locations.
Email header injection
SMTP notification fields (recipient, subject) are stripped of \r and
\n characters before being set as mail headers. This prevents header
injection attacks that could add arbitrary recipients or headers to outgoing email.
The Secret Type
Credentials that Luminarr stores — indexer API keys, download client
passwords, notification webhook URLs — are all wrapped in a Secret type
that actively prevents accidental exposure. It is not enough to be careful; the type
makes leakage structurally impossible in the common cases.
// Every rendering surface redacts the value automatically. func (s Secret) String() string { return "***" } func (s Secret) MarshalJSON() ([]byte, error) { return json.Marshal("***") } func (s Secret) LogValue() slog.Value { return slog.StringValue("***") } func (s Secret) GoString() string { return "***" } func (s Secret) MarshalText() ([]byte, error) { return []byte("***"), nil } // Call .Value() only when you actually need to send the secret. func (s Secret) Value() string { return s.value }
This means that printing a Secret to a log line, marshalling it to JSON
for an API response, or formatting it in a %v or %#v call
all produce "***". The only way to retrieve the actual value is an explicit
call to .Value(), which is reserved for the moment the credential is
sent to the external service it belongs to.
Credentials covered by this type include:
- Indexer API keys (Torznab, Newznab)
- Download client passwords (qBittorrent, Deluge)
- Notification webhook URLs and auth tokens (Discord, email credentials)
SSRF Protection
Server-side request forgery is a meaningful threat when users can configure arbitrary
URLs — for indexers, download clients, notification targets, and media servers.
Luminarr wraps net.Dialer in a custom safedialer package
that intercepts every outbound TCP connection, resolves the hostname, and validates
every IP returned by DNS before allowing the connection to proceed.
Two modes are available, applied per connection type based on whether the target is expected to be on your local network.
Strict mode
Blocks loopback (127.0.0.0/8, ::1), all RFC-1918 private ranges, link-local (169.254.0.0/16, fe80::/10), CGNAT and Tailscale (100.64.0.0/10), and unspecified (0.0.0.0, ::). Used by indexers and notification webhooks, which should always resolve to public IPs.
LAN mode
Allows RFC-1918 private addresses so you can point Luminarr at self-hosted services on your home network. Still blocks cloud metadata endpoints (169.254.169.254 and equivalents) and IPv6 link-local. Used by download clients and media servers.
| CIDR range | Description | Strict | LAN |
|---|---|---|---|
127.0.0.0/8 |
IPv4 loopback | Blocked | Blocked |
::1/128 |
IPv6 loopback | Blocked | Blocked |
10.0.0.0/8 |
RFC-1918 private | Blocked | Allowed |
172.16.0.0/12 |
RFC-1918 private | Blocked | Allowed |
192.168.0.0/16 |
RFC-1918 private | Blocked | Allowed |
169.254.0.0/16 |
Link-local / cloud metadata | Blocked | Blocked |
fe80::/10 |
IPv6 link-local | Blocked | Blocked |
100.64.0.0/10 |
CGNAT / Tailscale | Blocked | Allowed |
0.0.0.0/8 |
Unspecified | Blocked | Blocked |
Security Headers & Limits
Every response from the Luminarr server includes a standard set of security headers. Request bodies are also limited in size to prevent resource exhaustion attacks from malformed or oversized payloads.
| Header / Limit | Value |
|---|---|
Content-Security-Policy |
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://image.tmdb.org; font-src 'self' |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
same-origin |
| Request body limit | 1 MiB |
The Content Security Policy limits image sources to TMDB's image CDN in addition to
same-origin assets. This is intentional — movie posters and backdrops are fetched
directly from image.tmdb.org and displayed inline. All other external
origins are blocked.
Network Boundaries
Luminarr makes outbound connections only to services you have explicitly configured. The table below documents every connection type, the direction, when it occurs, and which transport mode is used.
| Service | Direction | When | Transport |
|---|---|---|---|
| TMDB | Outbound | Movie metadata lookup, poster fetch | Strict |
| Indexers (Torznab/Newznab) | Outbound | RSS feed poll, manual search | Strict |
| Download clients | Outbound | Add torrent, queue poll, import | LAN |
| Media servers | Outbound | Library refresh after import | LAN |
| Notification targets | Outbound | Grab, import, health events | Strict (SSRF-protected) |
| Browser | Inbound | UI requests, API calls, WebSocket | Auth required |
No undeclared connections. Luminarr never initiates connections to services you haven't explicitly configured. There are no analytics endpoints, no crash reporting servers, and no update check URLs.
Script Sandbox
Luminarr can execute user-provided shell scripts in response to events — post-import processing, custom notifications, library maintenance. Because scripts are a power-user feature, the security model is explicit rather than restrictive: scripts run with full container user permissions by design, but the path to invoking them is hardened against injection.
Path traversal protection
Script names submitted through the API are validated before any filesystem access
occurs. Only plain filenames are accepted — no forward slashes, no backslashes, no
.. components. The filename is then resolved through three independent
checks: filepath.Join, filepath.Clean, and
filepath.Rel. All three must confirm the resolved path stays within the
configured scripts directory. If any check fails, the request is rejected.
Execution model
- Scripts inherit the environment of the Luminarr process, including any variables set at container start.
- A configurable timeout (default 30 seconds) bounds execution time. After the timeout, a SIGKILL is sent — the script cannot prevent termination.
- Standard output and standard error are captured and returned in the API response for debugging.
- Exit codes are reported; non-zero exits are treated as failures and logged.
Scripts run as the Luminarr process user. Only place scripts you trust in /config/scripts/. If your container runs as root, so do your scripts. See the Custom Scripts guide for the recommended setup with a non-root user.
What Luminarr Does Not Do
These are not aspirational statements — they are structural properties of the codebase. There is no telemetry library. There is no analytics endpoint. There is no update check URL. The source code is auditable.
No telemetry
Zero usage analytics. No event tracking, no page view counters, no behavioral data collection of any kind.
No crash reporting
Errors are logged locally. No panic data, stack traces, or diagnostic payloads are sent to any external service.
No update checks
Luminarr never phones home to check for new versions. Update availability is communicated through GitHub releases only.