Security Hardening
The default Compose templates work out of the box without any special security configuration. For users who want additional container-level hardening, the template below adds capability restrictions, privilege escalation prevention, and a noexec tmpfs mount.
What the settings do
Section titled “What the settings do”| Setting | Effect |
|---|---|
security_opt: no-new-privileges:true |
Prevents processes inside the container from gaining additional privileges (e.g., via setuid binaries) |
cap_drop: ALL |
Drops all Linux capabilities by default |
cap_add: [...] |
Re-adds only the capabilities each service actually needs |
tmpfs: /tmp:noexec,nosuid |
Mounts /tmp as a tmpfs with execution and SUID disabled |
Built-in application protections
Section titled “Built-in application protections”These are always on and need no configuration; they are listed so you know what the app does on its own and where the environment variables above fit in.
| Protection | What it does |
|---|---|
| Session revocation | Every request, page or API, checks the session cookie against the admin record. Changing credentials, linking/unlinking SSO, or logging out increments a version counter and every outstanding cookie stops working immediately — on all devices. A stolen cookie is dead the moment you log out. |
| Session lifetime | 30 days, enforced on both the encrypted payload and the cookie’s Max-Age. |
Secure cookie |
Set automatically on HTTPS requests (see COOKIE_SECURE in Installation). |
| CSRF | Every state-changing /api/* request (POST, PUT, PATCH, DELETE) must carry an Origin or Referer on the app’s own host; a request from another site is rejected with 403. Combined with the SameSite=Lax cookie this also blocks a sibling app on the same parent domain. Requests with neither header (curl, scripts) are allowed — they carry no ambient cookie. |
| Rate limiting | Login, Plex PIN, SSO, password-change and first-run setup endpoints allow 10 attempts per 15 minutes per client. Only a sign-in that fails is counted: signing in from ten devices in a row costs nothing. On top of that, a global ceiling of 60 failures per 15 minutes across all clients catches a client that rotates X-Forwarded-For to get a fresh per-client budget on every request — but it never applies to an address you have signed in from (remembered for 30 days, in memory), so sixty wrong passwords from elsewhere cannot lock you out of your own login; an address is only ever refused by its own ten. See TRUST_PROXY_HEADERS. |
| Login timing | The local login spends the same bcrypt cost whether or not the username exists, so response time does not reveal which usernames are real. |
| Forward-auth proxy secret | Forward-auth sign-in requires FORWARD_AUTH_SECRET to be set and the request to carry it in the X-Librariarr-Proxy-Secret header your proxy adds, so a client that reaches the app directly cannot sign in by sending the identity header itself. Without the variable, forward-auth cannot be turned on and is not offered on the login page. |
| OIDC over TLS | The OIDC issuer must be an https:// URL. The client relies on TLS to authenticate the identity provider (it verifies no ID token signature), so over plain HTTP whoever sits on the path to the IdP could sign in as you. http:// is accepted only on a loopback host (localhost, *.localhost, 127.x.x.x, ::1), or anywhere with OIDC_ALLOW_INSECURE_HTTP=true. |
| Session cookie contents | The sealed cookie carries only who is signed in and whether that sign-in is still honoured — never a credential. The Plex account token lives in the database only, so a copy of the session secret cannot be turned into a Plex token that signs in on its own. |
| Recent sign-in | Anything that adds a lasting way in needs proof beyond the session cookie: changing the password needs the current one, and setting a first password, linking a Plex account, linking an SSO identity, or creating an API key without a password needs a sign-in from the last 15 minutes. Each of these outlasts logout, so a stolen cookie alone cannot turn itself into permanent access through them. The same applies to anything that would hand over a Plex token, since a Plex token signs in to Librariarr on its own: creating, downloading or restoring a backup, adding a Plex server from your Plex account, and pointing a Plex server at a new address or turning off its certificate checks — and the same for a Jellyfin or Emby server, whose API key is an admin key that can delete media. The same goes for pointing a Sonarr, Radarr, Lidarr, Seerr or Tracearr instance, or the AI provider, at a new address with its saved API key, whether to save it or just to test the connection, since the key would go to that address; entering the key again along with the new address needs no extra sign-in. Turning SSO on, switching its mode, or changing the header forward-auth reads the user from needs one too, since those decide which identity signs in; so does changing the forward-auth email or name header, since each sign-in copies them into the account, where the app shows them back (and none of the three may name a header that carries a credential, such as the proxy secret or Authorization). Server discovery never sends a server’s token to the browser, and the stream artwork proxy only ever asks your media server for artwork. When the sign-in is older, the app asks you to confirm it’s you in place — your password, or signing in again with Plex (only while Plex login is allowed) or SSO — rather than making you sign out. Wrong passwords there share one limit with the API-key password check and the current-password check on a credential change, so none adds guesses beyond the others; each attempt counts from the moment it starts, so a burst of parallel guesses gets no more than a slow one, and only a signed-in request is ever counted, so nobody can use the limit to lock you out. While password sign-in is off (local login off, or SSO in use) the password is accepted for nothing — not to sign in, confirm it’s you, create an API key or change your credentials — and turning it back on needs a recent sign-in by another method, as does turning Plex login back on. |
| Secret masking | API keys, tokens, the OIDC client secret, the AI key and the Discord webhook URL are never returned by the API once saved; the UI shows a placeholder and saving the placeholder keeps the stored value. |
| Public API keys | Keys for the public API are shown once and stored only as a salted scrypt hash. Each is limited to the scopes it was issued with (read-only keys cannot change anything), can expire, and stops working on the next request once deleted. Keys are sent in a header only (a key in the URL is refused), work only on /api/v1, are never written to backups, and are all revoked by a restore. They are independent of your login: a password change or logout does not revoke them, so delete keys you no longer trust. Creating one needs your password (or, on a Plex/SSO-only account, a sign-in from the last 15 minutes), and every creation and deletion is posted to Discord when a webhook is configured. |
| Content Security Policy | Every page is sent with a fresh random nonce, and only scripts carrying it run ('strict-dynamic' lets those load the rest of the app), so a script injected into a page does not run. frame-ancestors 'none' and X-Frame-Options: DENY prevent clickjacking; scripts are restricted to the app itself, and connections to the app and plex.tv. The production build does not permit eval, and object-src 'none', base-uri 'self' and form-action 'self' stop injected plugins, base URLs and cross-origin form posts. Artwork the app proxies from a media server is served under default-src 'none'; sandbox, so nothing a server returns there can run on the app’s origin, and is cached only by the browser. No X-Powered-By header is sent. |
| Backup files | Backups hold every integration’s API key and the Plex token (encrypted only when a passphrase is set). They are written readable by their owner only, a download is never cached, and creating, downloading or restoring one needs a sign-in from the last 15 minutes. |
| Password length | New passwords must be 8 characters to 72 bytes. bcrypt ignores everything after the 72nd byte, so a longer one would be silently shortened. |
| Forced logout | Signing out from a link on another site is refused, so another site cannot sign you out of every device. |
Hardened Compose template
Section titled “Hardened Compose template”services: librariarr: image: ahembree/librariarr:latest container_name: librariarr ports: - "${LIBRARIARR_PORT:-3000}:3000" environment: - DATABASE_URL=postgresql://librariarr:${DB_PASSWORD}@librariarr-db:5432/librariarr - SESSION_SECRET=${SESSION_SECRET:-} - TZ=${TZ:-UTC} - PUID=${PUID:-1000} - PGID=${PGID:-1000} volumes: - ./config:/config security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - SETUID - SETGID - FOWNER tmpfs: - /tmp:noexec,nosuid healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s timeout: 5s start_period: 60s retries: 3 depends_on: librariarr-db: condition: service_healthy restart: unless-stopped
librariarr-db: image: postgres:18-alpine container_name: librariarr-db environment: POSTGRES_USER: librariarr POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: librariarr volumes: - librariarr-db:/var/lib/postgresql security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - DAC_OVERRIDE - FOWNER - SETUID - SETGID tmpfs: - /tmp:noexec,nosuid healthcheck: test: ["CMD-SHELL", "pg_isready -U librariarr"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped
volumes: librariarr-db:Applying to an external database setup
Section titled “Applying to an external database setup”If you use an existing PostgreSQL server, add the security settings to only the librariarr service:
services: librariarr: image: ahembree/librariarr:latest container_name: librariarr # ... your existing config ... security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - SETUID - SETGID - FOWNER tmpfs: - /tmp:noexec,nosuidRequired capabilities
Section titled “Required capabilities”Librariarr container — CHOWN, SETUID, SETGID, FOWNER: needed to set up file ownership for the PUID/PGID user mapping and write to the /config volume.
PostgreSQL container — CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID: needed by the PostgreSQL entrypoint to initialize the data directory and switch to the postgres user.