From ae804d0ba3ad0166d725215fdcdea0c7455c6284 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:56:48 +0000 Subject: [PATCH 1/9] Initial plan From 5e0355dc9889bc15c33702266694d8d2a3124ca1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:02:25 +0000 Subject: [PATCH 2/9] Add Security & Authentication page with authentik guidance Co-authored-by: enoch85 <4511254+enoch85@users.noreply.github.com> --- docs/API.md | 2 +- docs/Security.md | 151 +++++++++++++++++++++++++++++++++++++++++++++++ sidebars.js | 1 + 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 docs/Security.md diff --git a/docs/API.md b/docs/API.md index 619761af4..3ddbf16a9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -9,7 +9,7 @@ hide: --- :::danger -:fire: :fire: The API, and all of Maintainerr for that matter, does not have an authentication method. There are certain API calls, that if you make your instance public facing, will expose your entire settings configuration. This could include all of your service's API keys. Proceed with extreme caution if you choose to expose Maintainerr to the public. :fire: :fire: +:fire: :fire: The API, and all of Maintainerr for that matter, does not have an authentication method. There are certain API calls, that if you make your instance public facing, will expose your entire settings configuration. This could include all of your service's API keys. Proceed with extreme caution if you choose to expose Maintainerr to the public. See the [Security & Authentication](/security) page for guidance on protecting your instance. :fire: :fire: ::: diff --git a/docs/Security.md b/docs/Security.md new file mode 100644 index 000000000..d7fcb634c --- /dev/null +++ b/docs/Security.md @@ -0,0 +1,151 @@ +--- +id: security +slug: /security +description: How to secure Maintainerr with an identity-aware reverse proxy, and what the risks are if you do not. +title: Security & Authentication +--- + +:::danger +Maintainerr has **no built-in authentication**. Anyone who can reach the UI or API can read your full settings — including all service API keys — and trigger destructive collection actions. Read this page before exposing Maintainerr outside your local network. +::: + +## What an unauthenticated instance exposes + +Every endpoint in Maintainerr's API is unauthenticated. In particular: + +- `GET /api/settings` returns credentials for every configured service (Plex/Jellyfin/Emby tokens, Sonarr/Radarr API keys, Overseerr/Jellyseerr API keys, and so on). +- Collection and rule endpoints let any caller create, modify, or delete rules and trigger immediate media deletion. +- The live-log stream (`/api/logs/stream`) exposes internal application activity. + +There is no masking on these responses that constitutes a protection boundary. The only safe assumption is that **the port is secret**. + +## The core rule: never publish the Maintainerr port directly + +Do not map the Maintainerr container port to a public interface. Instead, let only a reverse proxy that sits in front of it be reachable from outside your network, and add authentication at that proxy layer. + +### Docker Compose example — no published port + +```yaml +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:/usr/src/app/data + networks: + - proxy + +networks: + proxy: + external: true +``` + +When there is no `ports:` entry, the container is reachable only from other containers on the same Docker network. Your reverse proxy container joins that network and forwards traffic; nothing else can. + +## Recommended approach: authentik Proxy Provider + +[authentik](https://goauthentik.io/) is an open-source identity provider that can place an authenticated outpost in front of any web application, including Maintainerr, without any changes to Maintainerr itself. + +This approach mirrors how [authentik's own documentation](https://docs.goauthentik.io/integrations/) already covers Sonarr, Tautulli, Seerr, and Jellyfin. + +### How it works + +authentik's **Proxy Provider** deploys a small outpost container that intercepts every request. Unauthenticated requests are redirected to the authentik login page. Once authenticated, the outpost forwards the request to Maintainerr with no involvement from Maintainerr itself. + +There are two sub-modes: + +| Mode | Use when | +| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Proxy mode** (recommended) | Maintainerr and authentik share the same domain (e.g. `maintainerr.example.com` → authentik outpost → Maintainerr container). Cookies are scoped to the subdomain. | +| **Forward auth — single application** | Your existing nginx/Caddy/Traefik instance calls authentik's `/outpost.goauthentik.io/auth/nginx` endpoint on every request and only proxies through when authentik returns `200`. | + +### Proxy mode setup + +1. In the authentik Admin Interface, go to **Applications → Providers → Create**. +2. Select **Proxy Provider**. +3. Set **External host** to the public URL of Maintainerr (e.g. `https://maintainerr.example.com`). +4. Select **Proxy mode**. +5. Create or select an **Outpost** and bind the provider to it. +6. Create an **Application** that points to the provider, and assign it to the users or groups you want to allow. + +The outpost container is now the only thing that should be reachable at `maintainerr.example.com`. The Maintainerr container itself stays off the public network. + +### Forward auth (single application) — nginx example + +If you already run an nginx reverse proxy, you can call the authentik outpost as a forward-auth server instead of replacing nginx: + +```nginx +server { + listen 443 ssl; + server_name maintainerr.example.com; + + # Forward-auth check + location /outpost.goauthentik.io { + proxy_pass https:///outpost.goauthentik.io; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + } + + 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) + proxy_buffering off; + } + + location @goauthentik_proxy_signin { + internal; + add_header Set-Cookie $auth_cookie; + return 302 /outpost.goauthentik.io/start?rd=$request_uri; + } +} +``` + +:::note Server-Sent Events and `proxy_buffering` +Maintainerr streams live logs and task events over **Server-Sent Events** from `/api/logs/stream` and `/api/events/stream`. These endpoints do not send the `X-Accel-Buffering: no` header. Under nginx forward auth, the Logs page and live task progress will appear to hang unless `proxy_buffering off` is set for the location that forwards to Maintainerr. authentik's own Proxy mode outpost flushes immediately and is not affected. +::: + +### What does not need an authentication exemption + +Maintainerr has no inbound webhook receivers — all integrations are outbound. The UI and API share a single port. The Docker `HEALTHCHECK` runs inside the container and bypasses the proxy entirely. + +The only path worth allowlisting is `/api/health/*`, and only if an **external uptime monitor** needs unauthenticated access to the health endpoint. Everything else can remain protected. + +## Alternatives + +The recommendation to use authentik is not a hard requirement. Any of the following also work: + +| 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** | Expose nothing publicly; access Maintainerr through WireGuard or Tailscale. The simplest option if you do not need remote access. | + +## The API key in Settings is not a protection boundary + +Maintainerr's Settings page contains an **API key** field. This key is used for **outbound requests that Maintainerr makes** (for example, when other tools call Maintainerr's API). It does not protect the UI, and it does not gate any of the endpoints listed above. Do not treat it as a substitute for network-level access control. + +## Credential rotation checklist + +If your Maintainerr instance has been publicly reachable without authentication: + +- [ ] Rotate the API key for every connected service (Plex token, Sonarr/Radarr/Lidarr/Readarr API keys, Overseerr/Jellyseerr API key, Tautulli API key, Jellyfin/Emby API key). +- [ ] Review recent collection runs in Maintainerr's logs for unexpected deletions or rule changes. +- [ ] Check Sonarr/Radarr/Jellyseerr/Overseerr 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. + +## See also + +- [Reverse Proxy](/reverseproxy) — nginx and SWAG configurations for putting Maintainerr behind a proxy. +- [API Docs](/api) — full API surface including health endpoints. diff --git a/sidebars.js b/sidebars.js index deb76cd04..f3a08aba2 100644 --- a/sidebars.js +++ b/sidebars.js @@ -11,6 +11,7 @@ const sidebars = { "works", "common", "reverseproxy", + "security", "downgrade", "migration", ], From 6b5cf9b96d528d57e8dd4f9de56f3b1d25ffe397 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:15:58 +0000 Subject: [PATCH 3/9] Fix Security.md: correct endpoints, nginx snippet, volume path, proxy setup, API key description, and checklist Co-authored-by: enoch85 <4511254+enoch85@users.noreply.github.com> --- docs/Security.md | 73 +++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/docs/Security.md b/docs/Security.md index d7fcb634c..445aacff3 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -6,34 +6,35 @@ title: Security & Authentication --- :::danger -Maintainerr has **no built-in authentication**. Anyone who can reach the UI or API can read your full settings — including all service API keys — and trigger destructive collection actions. Read this page before exposing Maintainerr outside your local network. +Maintainerr has **no built-in authentication**. Anyone who can reach the UI or API can read credentials for every connected service and trigger destructive collection actions. Read this page before exposing Maintainerr outside your local network. ::: ## What an unauthenticated instance exposes -Every endpoint in Maintainerr's API is unauthenticated. In particular: +Every endpoint in Maintainerr's API is unauthenticated. The most severe examples: -- `GET /api/settings` returns credentials for every configured service (Plex/Jellyfin/Emby tokens, Sonarr/Radarr API keys, Overseerr/Jellyseerr API keys, and so on). +- `GET /api/settings/database/download` streams the entire SQLite database file, unredacted. This is the single most dangerous endpoint: it contains all stored credentials and configuration. +- `GET /api/settings/radarr`, `/api/settings/sonarr`, and equivalent per-service endpoints return raw repository rows including API keys. - Collection and rule endpoints let any caller create, modify, or delete rules and trigger immediate media deletion. - The live-log stream (`/api/logs/stream`) exposes internal application activity. -There is no masking on these responses that constitutes a protection boundary. The only safe assumption is that **the port is secret**. +The only safe assumption is that **the port is secret**. ## The core rule: never publish the Maintainerr port directly Do not map the Maintainerr container port to a public interface. Instead, let only a reverse proxy that sits in front of it be reachable from outside your network, and add authentication at that proxy layer. -### Docker Compose example — no published port +### Docker Compose example - no published port ```yaml services: maintainerr: image: ghcr.io/maintainerr/maintainerr:latest - # No `ports:` mapping — only the reverse proxy can reach this container. + # No `ports:` mapping - only the reverse proxy can reach this container. environment: TZ: Europe/Amsterdam volumes: - - ./data:/usr/src/app/data + - ./data:/opt/data networks: - proxy @@ -44,11 +45,18 @@ networks: When there is no `ports:` entry, the container is reachable only from other containers on the same Docker network. Your reverse proxy container joins that network and forwards traffic; nothing else can. +If you need local access while troubleshooting without exposing the port publicly, bind only to loopback: + +```yaml +ports: + - "127.0.0.1:6246:6246" +``` + ## Recommended approach: authentik Proxy Provider [authentik](https://goauthentik.io/) is an open-source identity provider that can place an authenticated outpost in front of any web application, including Maintainerr, without any changes to Maintainerr itself. -This approach mirrors how [authentik's own documentation](https://docs.goauthentik.io/integrations/) already covers Sonarr, Tautulli, Seerr, and Jellyfin. +This approach mirrors how [authentik's own documentation](https://integrations.goauthentik.io/) already covers Sonarr, Tautulli, Seerr, and Jellyfin. ### How it works @@ -56,23 +64,24 @@ authentik's **Proxy Provider** deploys a small outpost container that intercepts There are two sub-modes: -| Mode | Use when | -| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Proxy mode** (recommended) | Maintainerr and authentik share the same domain (e.g. `maintainerr.example.com` → authentik outpost → Maintainerr container). Cookies are scoped to the subdomain. | -| **Forward auth — single application** | Your existing nginx/Caddy/Traefik instance calls authentik's `/outpost.goauthentik.io/auth/nginx` endpoint on every request and only proxies through when authentik returns `200`. | +| 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. | ### Proxy mode setup -1. In the authentik Admin Interface, go to **Applications → Providers → Create**. +1. In the authentik Admin Interface, go to **Applications -> Providers -> Create**. 2. Select **Proxy Provider**. 3. Set **External host** to the public URL of Maintainerr (e.g. `https://maintainerr.example.com`). -4. Select **Proxy mode**. -5. Create or select an **Outpost** and bind the provider to it. -6. Create an **Application** that points to the provider, and assign it to the users or groups you want to allow. +4. Set **Internal host** to the upstream URL of the Maintainerr container (e.g. `http://maintainerr:6246`). This is the address the outpost forwards authenticated requests to. +5. Select **Proxy mode**. +6. Create or select an **Outpost** and bind the provider to it. +7. Create an **Application** that points to the provider, and assign it to the users or groups you want to allow. The outpost container is now the only thing that should be reachable at `maintainerr.example.com`. The Maintainerr container itself stays off the public network. -### Forward auth (single application) — nginx example +### Forward auth (single application) - nginx example If you already run an nginx reverse proxy, you can call the authentik outpost as a forward-auth server instead of replacing nginx: @@ -81,16 +90,19 @@ server { listen 443 ssl; server_name maintainerr.example.com; - # Forward-auth check + # Forward-auth check against the authentik outpost location /outpost.goauthentik.io { proxy_pass https:///outpost.goauthentik.io; proxy_pass_request_body off; proxy_set_header Content-Length ""; - proxy_set_header X-Original-URI $request_uri; + 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; } location / { - auth_request /outpost.goauthentik.io/auth/nginx; + 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; @@ -98,13 +110,15 @@ server { proxy_pass http://maintainerr:6246; # Required for Server-Sent Events (live logs and task progress) - proxy_buffering off; + proxy_buffering off; + proxy_buffer_size 32k; + proxy_buffers 8 16k; } location @goauthentik_proxy_signin { internal; add_header Set-Cookie $auth_cookie; - return 302 /outpost.goauthentik.io/start?rd=$request_uri; + return 302 /outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri; } } ``` @@ -115,9 +129,9 @@ Maintainerr streams live logs and task events over **Server-Sent Events** from ` ### What does not need an authentication exemption -Maintainerr has no inbound webhook receivers — all integrations are outbound. The UI and API share a single port. The Docker `HEALTHCHECK` runs inside the container and bypasses the proxy entirely. +Maintainerr has no inbound webhook receivers - all integrations are outbound. The UI and API share a single port. The Docker `HEALTHCHECK` runs inside the container and bypasses the proxy entirely. -The only path worth allowlisting is `/api/health/*`, and only if an **external uptime monitor** needs unauthenticated access to the health endpoint. Everything else can remain protected. +The only path worth allowlisting is `/api/health/*`, and only if an **external uptime monitor** needs unauthenticated access to the health endpoint. Be conservative: in authentik's proxy mode, allowlisted paths bypass outpost processing entirely and receive no session headers. Allowlisting anything beyond health endpoints is not necessary and widens the attack surface. ## Alternatives @@ -133,19 +147,20 @@ The recommendation to use authentik is not a hard requirement. Any of the follow ## The API key in Settings is not a protection boundary -Maintainerr's Settings page contains an **API key** field. This key is used for **outbound requests that Maintainerr makes** (for example, when other tools call Maintainerr's API). It does not protect the UI, and it does not gate any of the endpoints listed above. Do not treat it as a substitute for network-level access control. +Maintainerr's Settings page contains an **API key** field with a regenerate button. This key is generated at first boot and is used only for internal loopback calls between Maintainerr's own services. It is never validated on any inbound request from outside the container. Do not treat it as a substitute for network-level access control. ## Credential rotation checklist If your Maintainerr instance has been publicly reachable without authentication: -- [ ] Rotate the API key for every connected service (Plex token, Sonarr/Radarr/Lidarr/Readarr API keys, Overseerr/Jellyseerr API key, Tautulli API key, Jellyfin/Emby API key). +- [ ] Rotate the API key for every connected service: Plex token, Sonarr/Radarr API keys, Seerr API key, Tautulli API key, Jellyfin/Emby API key, Streamystats credentials, TMDB API key, TVDB API key, and the qBittorrent download-client password. +- [ ] 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/Jellyseerr/Overseerr audit logs if available. +- [ ] 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. ## See also -- [Reverse Proxy](/reverseproxy) — nginx and SWAG configurations for putting Maintainerr behind a proxy. -- [API Docs](/api) — full API surface including health endpoints. +- [Reverse Proxy](/reverseproxy) - nginx and SWAG configurations for putting Maintainerr behind a proxy. +- [API Docs](/api) - full API surface including health endpoints. From 322d2d9bfab0b38579faded6f8685c57f908336d Mon Sep 17 00:00:00 2001 From: enoch85 Date: Tue, 4 Aug 2026 20:38:51 +0000 Subject: [PATCH 4/9] Fix Security.md nits: buffer placement, credential list, Traefik, config link - Move proxy_buffer_size/proxy_buffers into the outpost location where the auth_request subrequest reads the outpost response; keep only proxy_buffering off under location / (proxy_buffers is inert there). - Rotation checklist: drop non-existent "Streamystats credentials" (it stores only a URL and auths with the Jellyfin key), add Sportarr and Tracearr keys. - Add a Traefik forward-auth (single application) example. - Link the Security page from Configuration for publish-online users. --- docs/Configuration.md | 4 +++ docs/Security.md | 69 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 7852fbef0..e619fb76b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -17,6 +17,10 @@ All Base URL settings are to be entered without the leading slash. ::: +:::danger Publishing Maintainerr online +Maintainerr has **no built-in authentication**, and anyone who can reach it can read the service credentials you configure below. If you plan to make this instance reachable from outside your local network, put it behind an authenticating reverse proxy first. See [Security & Authentication](/security) for how to do that and what is exposed if you do not. +::: + ## General These settings are OK for most installations. diff --git a/docs/Security.md b/docs/Security.md index 445aacff3..fb5f2c0bb 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -99,6 +99,12 @@ server { 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 / { @@ -109,10 +115,10 @@ server { proxy_pass http://maintainerr:6246; - # Required for Server-Sent Events (live logs and task progress) - proxy_buffering off; - proxy_buffer_size 32k; - proxy_buffers 8 16k; + # 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 { @@ -127,6 +133,59 @@ server { Maintainerr streams live logs and task events over **Server-Sent Events** from `/api/logs/stream` and `/api/events/stream`. These endpoints do not send the `X-Accel-Buffering: no` header. Under nginx forward auth, the Logs page and live task progress will appear to hang unless `proxy_buffering off` is set for the location that forwards to Maintainerr. authentik's own Proxy mode outpost flushes immediately and is not affected. ::: +### Forward auth (single application) - Traefik example + +With Traefik, define a `forwardAuth` middleware that calls the authentik outpost, then attach it to the Maintainerr router. This dynamic configuration uses Traefik's file provider: + +```yaml +# traefik-dynamic.yml +http: + middlewares: + authentik: + forwardAuth: + address: http://:9000/outpost.goauthentik.io/auth/traefik + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + - X-authentik-groups + - X-authentik-email + - X-authentik-name + - X-authentik-uid +``` + +Then attach the middleware to Maintainerr and give the outpost's own paths a router on the same hostname. Using Docker labels on the two containers: + +```yaml +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: + - proxy +``` + +Unlike nginx, Traefik streams upstream responses and does not buffer them by default, so Server-Sent Events work without extra configuration - there is no `proxy_buffering` equivalent to set, and no outpost header-buffer tuning is required. + ### What does not need an authentication exemption Maintainerr has no inbound webhook receivers - all integrations are outbound. The UI and API share a single port. The Docker `HEALTHCHECK` runs inside the container and bypasses the proxy entirely. @@ -153,7 +212,7 @@ Maintainerr's Settings page contains an **API key** field with a regenerate butt If your Maintainerr instance has been publicly reachable without authentication: -- [ ] Rotate the API key for every connected service: Plex token, Sonarr/Radarr API keys, Seerr API key, Tautulli API key, Jellyfin/Emby API key, Streamystats credentials, TMDB API key, TVDB API key, and the qBittorrent download-client password. +- [ ] 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. From a5527e8f01601d3b7014094ac31532f1bb1e5b5b Mon Sep 17 00:00:00 2001 From: enoch85 Date: Tue, 4 Aug 2026 20:44:37 +0000 Subject: [PATCH 5/9] Fix VPN-only note: a VPN enables remote access, it isn't for local-only --- docs/Security.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Security.md b/docs/Security.md index fb5f2c0bb..e1efc4ded 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -196,13 +196,13 @@ The only path worth allowlisting is `/api/health/*`, and only if an **external u The recommendation to use authentik is not a hard requirement. Any of the following also work: -| 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** | Expose nothing publicly; access Maintainerr through WireGuard or Tailscale. The simplest option if you do not need remote access. | +| 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. (If you only ever use Maintainerr on your own LAN, you do not need a VPN or a proxy at all.) | ## The API key in Settings is not a protection boundary From 0954c784d6a22ec82dd80966284f8038de119683 Mon Sep 17 00:00:00 2001 From: enoch85 Date: Tue, 4 Aug 2026 20:49:23 +0000 Subject: [PATCH 6/9] Plain-english + tone: scope Security page to internet-exposed setups Frame the page as advice for people who want to expose Maintainerr or add a login, not a warning for everyone. Local and VPN-only setups (the common case) need none of it. Rewrite the prose plainly throughout and downgrade the Configuration callout from danger to info. --- docs/Configuration.md | 4 +-- docs/Security.md | 62 +++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index e619fb76b..153e18441 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -17,8 +17,8 @@ All Base URL settings are to be entered without the leading slash. ::: -:::danger Publishing Maintainerr online -Maintainerr has **no built-in authentication**, and anyone who can reach it can read the service credentials you configure below. If you plan to make this instance reachable from outside your local network, put it behind an authenticating reverse proxy first. See [Security & Authentication](/security) for how to do that and what is exposed if you do not. +:::info Exposing Maintainerr to the internet +Maintainerr has no built-in login, so anyone who can reach it can read the credentials you enter below. Running it locally or reaching it over a VPN is fine and needs nothing extra. Only if you plan to put it directly on the internet, add an authenticating reverse proxy in front first - see [Security & Authentication](/security). ::: ## General diff --git a/docs/Security.md b/docs/Security.md index e1efc4ded..e41ee00e9 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -1,28 +1,28 @@ --- id: security slug: /security -description: How to secure Maintainerr with an identity-aware reverse proxy, and what the risks are if you do not. +description: How to put Maintainerr behind a login, and what is at risk if you do not. title: Security & Authentication --- -:::danger -Maintainerr has **no built-in authentication**. Anyone who can reach the UI or API can read credentials for every connected service and trigger destructive collection actions. Read this page before exposing Maintainerr outside your local network. +:::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. ::: -## What an unauthenticated instance exposes +## What is exposed if you publish it -Every endpoint in Maintainerr's API is unauthenticated. The most severe examples: +This only matters if people you do not trust can reach Maintainerr. On a private network or over a VPN, none of the below is reachable. If you do put it on the internet without a login, though, nothing in the API checks who is calling, so: -- `GET /api/settings/database/download` streams the entire SQLite database file, unredacted. This is the single most dangerous endpoint: it contains all stored credentials and configuration. -- `GET /api/settings/radarr`, `/api/settings/sonarr`, and equivalent per-service endpoints return raw repository rows including API keys. -- Collection and rule endpoints let any caller create, modify, or delete rules and trigger immediate media deletion. -- The live-log stream (`/api/logs/stream`) exposes internal application activity. +- `GET /api/settings/database/download` downloads 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. -The only safe assumption is that **the port is secret**. +So if you expose it, treat the port as a secret and put a login in front. -## The core rule: never publish the Maintainerr port directly +## If you expose it, do not publish the port directly -Do not map the Maintainerr container port to a public interface. Instead, let only a reverse proxy that sits in front of it be reachable from outside your network, and add authentication at that proxy layer. +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. ### Docker Compose example - no published port @@ -43,9 +43,9 @@ networks: external: true ``` -When there is no `ports:` entry, the container is reachable only from other containers on the same Docker network. Your reverse proxy container joins that network and forwards traffic; nothing else can. +With 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 local access while troubleshooting without exposing the port publicly, bind only to loopback: +If you need to reach it locally while troubleshooting, without opening the port to the world, bind it to loopback only: ```yaml ports: @@ -54,13 +54,13 @@ ports: ## Recommended approach: authentik Proxy Provider -[authentik](https://goauthentik.io/) is an open-source identity provider that can place an authenticated outpost in front of any web application, including Maintainerr, without any changes to Maintainerr itself. +[authentik](https://goauthentik.io/) is a free identity provider. It puts a login in front of any web app, including Maintainerr, without changing Maintainerr at all. -This approach mirrors how [authentik's own documentation](https://integrations.goauthentik.io/) already covers Sonarr, Tautulli, Seerr, and Jellyfin. +This is the same approach authentik's [own documentation](https://integrations.goauthentik.io/) already uses for Sonarr, Tautulli, Seerr, and Jellyfin. ### How it works -authentik's **Proxy Provider** deploys a small outpost container that intercepts every request. Unauthenticated requests are redirected to the authentik login page. Once authenticated, the outpost forwards the request to Maintainerr with no involvement from Maintainerr itself. +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: @@ -74,16 +74,16 @@ There are two sub-modes: 1. In the authentik Admin Interface, go to **Applications -> Providers -> Create**. 2. Select **Proxy Provider**. 3. Set **External host** to the public URL of Maintainerr (e.g. `https://maintainerr.example.com`). -4. Set **Internal host** to the upstream URL of the Maintainerr container (e.g. `http://maintainerr:6246`). This is the address the outpost forwards authenticated requests to. +4. 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. 5. Select **Proxy mode**. 6. Create or select an **Outpost** and bind the provider to it. 7. Create an **Application** that points to the provider, and assign it to the users or groups you want to allow. -The outpost container is now the only thing that should be reachable at `maintainerr.example.com`. The Maintainerr container itself stays off the public network. +Now the outpost is the only thing reachable at `maintainerr.example.com`. Maintainerr itself stays off the public network. ### Forward auth (single application) - nginx example -If you already run an nginx reverse proxy, you can call the authentik outpost as a forward-auth server instead of replacing nginx: +If you already run nginx, you can have it check the authentik outpost on each request instead of replacing nginx: ```nginx server { @@ -130,12 +130,12 @@ server { ``` :::note Server-Sent Events and `proxy_buffering` -Maintainerr streams live logs and task events over **Server-Sent Events** from `/api/logs/stream` and `/api/events/stream`. These endpoints do not send the `X-Accel-Buffering: no` header. Under nginx forward auth, the Logs page and live task progress will appear to hang unless `proxy_buffering off` is set for the location that forwards to Maintainerr. authentik's own Proxy mode outpost flushes immediately and is not affected. +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. ::: ### Forward auth (single application) - Traefik example -With Traefik, define a `forwardAuth` middleware that calls the authentik outpost, then attach it to the Maintainerr router. This dynamic configuration uses Traefik's file provider: +With Traefik, add a `forwardAuth` middleware that checks the authentik outpost, then attach it to the Maintainerr router. This uses Traefik's file provider: ```yaml # traefik-dynamic.yml @@ -153,7 +153,7 @@ http: - X-authentik-uid ``` -Then attach the middleware to Maintainerr and give the outpost's own paths a router on the same hostname. Using Docker labels on the two containers: +Then 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: ```yaml services: @@ -184,17 +184,17 @@ services: - proxy ``` -Unlike nginx, Traefik streams upstream responses and does not buffer them by default, so Server-Sent Events work without extra configuration - there is no `proxy_buffering` equivalent to set, and no outpost header-buffer tuning is required. +Unlike 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. -### What does not need an authentication exemption +### What you do not need to leave open -Maintainerr has no inbound webhook receivers - all integrations are outbound. The UI and API share a single port. The Docker `HEALTHCHECK` runs inside the container and bypasses the proxy entirely. +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 allowlisting is `/api/health/*`, and only if an **external uptime monitor** needs unauthenticated access to the health endpoint. Be conservative: in authentik's proxy mode, allowlisted paths bypass outpost processing entirely and receive no session headers. Allowlisting anything beyond health endpoints is not necessary and widens the attack surface. +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. ## Alternatives -The recommendation to use authentik is not a hard requirement. Any of the following also work: +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 | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -204,13 +204,13 @@ The recommendation to use authentik is not a hard requirement. Any of the follow | **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. (If you only ever use Maintainerr on your own LAN, you do not need a VPN or a proxy at all.) | -## The API key in Settings is not a protection boundary +## The API key in Settings does not protect anything -Maintainerr's Settings page contains an **API key** field with a regenerate button. This key is generated at first boot and is used only for internal loopback calls between Maintainerr's own services. It is never validated on any inbound request from outside the container. Do not treat it as a substitute for network-level access control. +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. ## Credential rotation checklist -If your Maintainerr instance has been publicly reachable without authentication: +Only needed if your Maintainerr was reachable from the internet without a login. If it has only ever been local or behind a VPN, you can skip this. - [ ] 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. From 67d69ad49e2caa016fe378e364a68e687754bb82 Mon Sep 17 00:00:00 2001 From: enoch85 Date: Tue, 4 Aug 2026 20:53:05 +0000 Subject: [PATCH 7/9] DRY: state the local/VPN-is-fine caveat once, in the intro callout Drop the repeated reassurances in the exposure lead, the port section bridge, the VPN table row, and the rotation intro. The intro callout is the single source. --- docs/Security.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/Security.md b/docs/Security.md index e41ee00e9..bbb05e28b 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -11,15 +11,13 @@ Maintainerr has **no built-in login**, so anyone who can reach the UI or API can ## What is exposed if you publish it -This only matters if people you do not trust can reach Maintainerr. On a private network or over a VPN, none of the below is reachable. If you do put it on the internet without a login, though, nothing in the API checks who is calling, so: +If you do put it on the internet without a login, nothing in the API checks who is calling, so: - `GET /api/settings/database/download` downloads 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. -So if you expose it, treat the port as a secret and put a login in front. - ## If you expose it, do not publish the port directly 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. @@ -196,13 +194,13 @@ The only path worth leaving open is `/api/health/*`, and only if an **outside up 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. (If you only ever use Maintainerr on your own LAN, you do not need a VPN or a proxy at all.) | +| 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 API key in Settings does not protect anything @@ -210,7 +208,7 @@ The Settings page has an **API key** field with a regenerate button. Maintainerr ## Credential rotation checklist -Only needed if your Maintainerr was reachable from the internet without a login. If it has only ever been local or behind a VPN, you can skip this. +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. From d338bf4d8fb26b761ccae29decdd4e2cf0276ed3 Mon Sep 17 00:00:00 2001 From: enoch85 Date: Tue, 4 Aug 2026 21:08:59 +0000 Subject: [PATCH 8/9] Add data-handling summary and hardening checklist to Security page Document what Maintainerr actually does to protect data (local-only, no telemetry, verified TLS, log secret-masking, parameterized queries, fail-closed deletes, non-root image) and state plainly that credentials are stored unencrypted at rest. Add an operator hardening checklist. --- docs/Security.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/Security.md b/docs/Security.md index bbb05e28b..98e79ac4f 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -9,6 +9,19 @@ title: Security & Authentication 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. ::: +## How Maintainerr handles your data + +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, `Authorization` headers, 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. +- **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 with `realpath`, 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, 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). + ## What is exposed if you publish it If you do put it on the internet without a login, nothing in the API checks who is calling, so: @@ -206,6 +219,19 @@ authentik is a recommendation, not a requirement. Any of these also work, and fo 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. +## Hardening checklist + +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/download` hands 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. + ## Credential rotation checklist Only needed if your Maintainerr was reachable from the internet without a login. From 47ed99866c2b25b8a28f16c012977dc271e64ce0 Mon Sep 17 00:00:00 2001 From: enoch85 Date: Tue, 4 Aug 2026 21:27:00 +0000 Subject: [PATCH 9/9] Add design/threat-model note; soften container claim on Security page State plainly that no auth, no rate limiting, and no encryption at rest are deliberate trusted-network-appliance choices, with security expected at the network boundary. Reword the container bullet to factual claims only. --- docs/Security.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/Security.md b/docs/Security.md index 98e79ac4f..36767a743 100644 --- a/docs/Security.md +++ b/docs/Security.md @@ -9,6 +9,16 @@ title: Security & Authentication 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. ::: +## Design and threat model + +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. + ## How Maintainerr handles your data Maintainerr is built to keep your data on your own hardware and to be careful with it internally: @@ -18,7 +28,7 @@ Maintainerr is built to keep your data on your own hardware and to be careful wi - **Secrets are kept out of the logs.** Every log line passes through a sanitizer that masks API keys, tokens, `Authorization` headers, 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. - **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 with `realpath`, 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, is built in multiple stages from a digest-pinned base, and pins security-sensitive dependencies. +- **Non-root, pinned container image.** The official image runs as a non-root user, 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).