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.

Browser
Sec-Fetch-Site: same-origin
Trusted
External client
X-Api-Key header
Constant-time compare
Handler

Key behaviour

  • Key is generated with crypto/rand.Read — no Math.random, no time seeds.
  • Comparison uses crypto/subtle.ConstantTimeCompare to prevent timing attacks regardless of key length or match prefix.
  • The GET /health endpoint is intentionally unauthenticated so load balancers and container orchestrators can probe liveness without credentials.
  • The API key persists in /config/config.yaml and is stable across restarts.
  • The config API returns a masked key (****…); the full key is only available via a separate /api/v1/system/config/apikey endpoint.

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.

internal/config/secret.go
// 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.