| id | security |
|---|---|
| slug | /security |
| description | How to put Maintainerr behind a login, and what is at risk if you do not. |
| title | Security & Authentication |
:::info Who this page is for Maintainerr has no built-in login, so anyone who can reach the UI or API can read your connected-service credentials and change collections. On a network you trust this is fine: running Maintainerr locally and reaching it remotely over a VPN (the most common setup) needs nothing on this page. You only need this if you want to put Maintainerr directly on the internet, or add a login in front of it for some other reason. :::
Maintainerr's security model is deliberate: it is built to run as an appliance on a network you control, and it assumes that anyone who can reach it is a trusted administrator. That is a design choice, not an oversight. In practice it means Maintainerr does not, on its own:
- authenticate requests (there is no login, and the API key in Settings is only for internal calls - see below),
- rate-limit or throttle requests, or
- encrypt its stored data at rest.
Security is expected at the boundary you already control - your LAN, a VPN, or an authenticating reverse proxy - which is what the rest of this page is about. Keep Maintainerr on a trusted network or reach it over a VPN and that boundary is already there; expose it more widely and you add the boundary yourself with a reverse proxy.
Maintainerr is built to keep your data on your own hardware and to be careful with it internally:
- Everything stays local. All configuration and state lives in a single SQLite database in your data directory (
/opt/data). There is no cloud component, no telemetry, and no analytics - Maintainerr never phones home. The only outbound traffic goes to the services you configure (your media server, the *arrs, Seerr, and so on) and to the metadata providers (TMDB/TVDB) used to match your library. - Outbound connections use verified TLS. Calls to your services and to metadata providers use HTTPS with normal certificate verification, which the app never disables on its own. Notification email can use TLS and can optionally be PGP-encrypted.
- Secrets are kept out of the logs. Every log line passes through a sanitizer that masks API keys, tokens,
Authorizationheaders, and credential-bearing URLs, so secrets do not leak into log files or error dumps. - The rules engine cannot run code or shell out. Rules are evaluated by a typed comparator, never
eval-ed. Database access is fully parameterized, so there is no SQL-injection surface, and the server runs no shell commands. Settings you submit are schema-validated before they are saved. - Cross-origin access is locked down. In production the API sends no CORS headers by default, so another website cannot read it from your browser. If you serve a separate front end from a different origin, allow it explicitly with
CORS_ALLOWED_ORIGINS. - Destructive actions are deliberately conservative. Deletes are tied to explicit collection and rule actions, and the folder-cleanup path is fail-closed: it refuses unexpected paths, rejects symlinks and
..traversal, canonicalizes withrealpath, and only removes a folder once it has proven the folder is empty and safely inside the intended directory. - The container is hardened. The official image runs as a non-root user with least-privilege file permissions, is built in multiple stages from a digest-pinned base, and pins security-sensitive dependencies.
One important caveat: the credentials you enter (Plex token, *arr and Seerr keys, qBittorrent and SMTP passwords, notifier tokens) are stored unencrypted in that SQLite database - Maintainerr does not encrypt data at rest. So the database file, and any backup of it, is as sensitive as the credentials it holds: keep the data directory private, restrict its permissions, and encrypt your backups. And because Maintainerr has no login of its own, none of this replaces putting it behind an authenticating reverse proxy when you expose it (the rest of this page).
If you do put it on the internet without a login, nothing in the API checks who is calling, so:
GET /api/settings/database/downloaddownloads the whole database, with nothing hidden. This is the big one: it holds every credential and setting you have saved.GET /api/settings/radarr,/api/settings/sonarr, and the other per-service endpoints hand back the saved settings, including API keys.- Anyone can create, change, or delete rules, and start deleting media right away.
- The live-log stream (
/api/logs/stream) shows what the app is doing inside.
Do not map Maintainerr's container port to a public address. Instead, put a reverse proxy in front of it, make only the proxy reachable from outside your network, and add the login at the proxy.
services:
maintainerr:
image: ghcr.io/maintainerr/maintainerr:latest
# No `ports:` mapping - only the reverse proxy can reach this container.
environment:
TZ: Europe/Amsterdam
volumes:
- ./data:/opt/data
networks:
- proxy
networks:
proxy:
external: trueWith no ports: entry, only other containers on the same Docker network can reach it. Your reverse proxy joins that network and passes traffic through; nothing else can get in.
If you need to reach it locally while troubleshooting, without opening the port to the world, bind it to loopback only:
ports:
- "127.0.0.1:6246:6246"authentik is a free identity provider. It puts a login in front of any web app, including Maintainerr, without changing Maintainerr at all.
This is the same approach authentik's own documentation already uses for Sonarr, Tautulli, Seerr, and Jellyfin.
authentik's Proxy Provider runs a small outpost container that catches every request. If you are not logged in, it sends you to the authentik login page. Once you are, it passes the request on to Maintainerr, which never has to deal with any of it.
There are two sub-modes:
| Mode | Use when |
|---|---|
| Proxy mode | You want the authentik outpost to act as the reverse proxy itself, replacing nginx/Caddy/Traefik for this application. |
| Forward auth - single application | You already run an nginx/Caddy/Traefik instance and want it to call authentik for auth on every request, while continuing to handle the proxying. |
- In the authentik Admin Interface, go to Applications -> Providers -> Create.
- Select Proxy Provider.
- Set External host to the public URL of Maintainerr (e.g.
https://maintainerr.example.com). - Set Internal host to the Maintainerr container's URL (e.g.
http://maintainerr:6246). This is where the outpost sends requests once you are logged in. - Select Proxy mode.
- Create or select an Outpost and bind the provider to it.
- Create an Application that points to the provider, and assign it to the users or groups you want to allow.
Now the outpost is the only thing reachable at maintainerr.example.com. Maintainerr itself stays off the public network.
If you already run nginx, you can have it check the authentik outpost on each request instead of replacing nginx:
server {
listen 443 ssl;
server_name maintainerr.example.com;
# Forward-auth check against the authentik outpost
location /outpost.goauthentik.io {
proxy_pass https://<authentik-outpost-url>/outpost.goauthentik.io;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
# The outpost's response headers can exceed nginx's default buffer,
# producing "upstream sent too big header". These apply to the auth
# subrequest, so they must live here, not under `location /`.
proxy_buffer_size 32k;
proxy_buffers 8 16k;
}
location / {
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_proxy_signin;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
proxy_pass http://maintainerr:6246;
# Required for Server-Sent Events (live logs and task progress).
# Note: proxy_buffers is ignored while buffering is off, so the
# buffer sizes above belong with the outpost location, not here.
proxy_buffering off;
}
location @goauthentik_proxy_signin {
internal;
add_header Set-Cookie $auth_cookie;
return 302 /outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
}
}:::note Server-Sent Events and proxy_buffering
Maintainerr sends live logs and task updates as Server-Sent Events from /api/logs/stream and /api/events/stream, and it does not set the X-Accel-Buffering: no header. Under nginx forward auth, the Logs page and live task progress look frozen unless you set proxy_buffering off on the location that forwards to Maintainerr. authentik's own Proxy mode outpost sends data through right away, so it is not affected.
:::
With Traefik, add a forwardAuth middleware that checks the authentik outpost, then attach it to the Maintainerr router. This uses Traefik's file provider:
# traefik-dynamic.yml
http:
middlewares:
authentik:
forwardAuth:
address: http://<authentik-outpost-host>:9000/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uidThen attach that middleware to Maintainerr, and add a router so the outpost's own paths are served on the same hostname. With Docker labels on the two containers:
services:
maintainerr:
image: ghcr.io/maintainerr/maintainerr:latest
# Still no published port - only Traefik can reach this container.
labels:
traefik.enable: "true"
traefik.http.routers.maintainerr.rule: "Host(`maintainerr.example.com`)"
traefik.http.routers.maintainerr.entrypoints: "websecure"
traefik.http.routers.maintainerr.tls: "true"
traefik.http.routers.maintainerr.middlewares: "authentik@file"
traefik.http.services.maintainerr.loadbalancer.server.port: "6246"
networks:
- proxy
authentik-outpost:
image: ghcr.io/goauthentik/proxy:latest
# Standard authentik outpost env (AUTHENTIK_HOST, AUTHENTIK_TOKEN, ...).
labels:
traefik.enable: "true"
# Route the outpost's auth and redirect paths on the same hostname.
traefik.http.routers.authentik.rule: "Host(`maintainerr.example.com`) && PathPrefix(`/outpost.goauthentik.io/`)"
traefik.http.routers.authentik.entrypoints: "websecure"
traefik.http.routers.authentik.tls: "true"
traefik.http.services.authentik.loadbalancer.server.port: "9000"
networks:
- proxyUnlike nginx, Traefik passes responses straight through and does not buffer them by default, so Server-Sent Events just work - there is no proxy_buffering setting to change, and no header buffers to tune.
Maintainerr does not receive any webhooks - it only makes outgoing calls. The UI and API share one port. The Docker HEALTHCHECK runs inside the container, so it never goes through the proxy.
The only path worth leaving open is /api/health/*, and only if an outside uptime monitor needs to reach the health check without logging in. Keep it tight: in authentik's proxy mode, an open path skips the outpost completely and gets no session headers. Opening anything more than the health check is not needed and only gives an attacker more to work with.
authentik is a recommendation, not a requirement. Any of these also work, and for many people the VPN option at the bottom is all they need:
| Option | Notes |
|---|---|
| Authelia | Open-source SSO and 2FA proxy. Works as forward-auth middleware for nginx, Caddy, and Traefik. |
| Tinyauth | Lightweight single-user forward-auth server, easier to set up than Authelia or authentik when you only need one user. |
| Cloudflare Access | Zero-trust tunnel; no self-hosted infrastructure required. Maintainerr does not need to be reachable from the public internet at all. |
| Reverse proxy basic auth | nginx's auth_basic or Caddy's basicauth directive. Simple but credentials are sent in every request and there is no SSO. Acceptable if TLS is in place. |
| VPN only | Publish nothing at all, and reach Maintainerr remotely over WireGuard or Tailscale as if you were on its local network. The simplest option when you want remote access without exposing anything. |
The Settings page has an API key field with a regenerate button. Maintainerr creates this key on first start and uses it only for internal calls between its own services. It is never checked on requests coming from outside the container, so do not rely on it in place of real network access control.
If you want to run Maintainerr as safely as possible:
- Do not publish the container port. Reach it only through a reverse proxy, over a VPN, or on your LAN.
- Put a login in front of it if it is reachable from the internet - authentik, Authelia, Tinyauth, Cloudflare Access, or basic auth.
- Do not allowlist
/api/settings/*at your proxy. In particular,/api/settings/database/downloadhands out the entire database./api/health/*is the only path safe to leave open, and only if you actually need it. - Keep the data directory private. It holds your credentials in cleartext, so restrict its permissions on the host and make sure only Maintainerr and you can read it.
- Encrypt backups of the data directory, and do not commit it or paste its contents anywhere.
- Use least-privilege API keys for the connected services where they support it, so a leaked key does less damage.
- Run as a non-root user with a persistent, well-permissioned volume (the official image already runs as UID 1000).
- Keep it updated - pull new images so dependency and security fixes land.
Only needed if your Maintainerr was reachable from the internet without a login.
- Rotate the API key for every connected service: Plex token, Sonarr/Radarr/Sportarr API keys, Seerr API key, Tautulli API key, Tracearr API key, Jellyfin/Emby API key, TMDB API key, TVDB API key, and the qBittorrent download-client password. (Streamystats needs nothing separate - Maintainerr authenticates to it with the Jellyfin API key already listed here.)
- Rotate any webhook URLs or SMTP credentials configured in Maintainerr's notification agents.
- Review recent collection runs in Maintainerr's logs for unexpected deletions or rule changes.
- Check Sonarr/Radarr/Seerr audit logs if available.
- Place Maintainerr behind authentication before bringing it back online (see above).
- Review all other services that share any of the rotated credentials.
- Reverse Proxy - nginx and SWAG configurations for putting Maintainerr behind a proxy.
- API Docs - full API surface including health endpoints.