Skip to content

Security & Encryption

CompassDocs ships with layered protections out of the box. This page explains what the app handles for you, the one key you should look after, and the parts that live at the deployment layer.

  • Passwords are hashed with scrypt (memory-hard) and a per-user random salt — never stored or logged in plain text.
  • Credentials are encrypted at rest (AES-256-GCM): the SMTP password, Anthropic API key, SSO client secret, off-site backup credentials, and any custom TLS private key are sealed with a master key that never touches the database. A stolen database dump yields ciphertext, not your secrets.
  • Session tokens are stored hashed (SHA-256). The raw token exists only in the user’s cookie, so a database leak can’t be replayed as a login. Cookies are httpOnly, SameSite=Lax, and Secure on HTTPS installs, with a sliding idle timeout.
  • API and connector tokens are stored hashed and shown exactly once.
  • Backups are encrypted before they reach disk or an off-site bucket — see Backups & restore.
  • Login throttling: five failed attempts for the same username or email, as typed, from the same address within 15 minutes lock that pair out for 15 minutes (wrong two-factor codes count too); sprays from a single address are locked after 30 failures. Every lockout lands in the audit log as auth.lockout, and every failed attempt, a wrong two-factor code included, is an auth.login_failed entry. The throttle keys on what was typed and is checked before any account is read, so a lockout never shows which account an email belongs to. Since 1.16.0, when an email signs in too, a username and its email therefore each get five attempts per address, within the 30-per-address limit. Each attempt is counted as it starts, so a burst sent at once can’t get past either limit (before 1.16.0 it could). A name signs in only when it matches the account under the same letter-case rules the throttle counts by, so a spelling only the database would fold to the same name (a dotted capital İ for an i) signs nobody in, rather than getting five attempts of its own.
  • Password re-checks are throttled too. Changing your password (Account → Security, or the forced change at a first sign-in or after a reset) and a typed signature on a policy or a training deck check the account’s current password. Their failures count against the username and address, with sign-in failures under the username: five in 15 minutes lock all of them for that account and address for 15 minutes. Signing in with the email is counted on its own. Each wrong password is an auth.login_failed entry (via: change_password for a password change, since 1.16.0), and the lock an auth.lockout.
  • No account oracle on the sign-in page (1.16.0). A sign-in for an unknown, disabled or password-less account spends the same password check as a real one, so the response time doesn’t say whether an account exists, and the identifier is capped at 254 characters. With single sign-on, the email-first step answers an unknown identifier and a password account identically, reads an account only when no SSO setting decides, and is limited to 20 such lookups a minute and 100 an hour per address (auth.lookup_throttled in the audit log when it engages); see Email-first sign-in.
  • The limits count per process, by client address. The throttles read the client’s address the way the audit log does: behind the bundled Caddy as is, behind another proxy too once COMPASSDOCS_TRUSTED_PROXY_HOPS says how many there are (Environment variables). With several app instances, each counts its own share.
  • Security headers on every response: Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, a restrictive Permissions-Policy, and an anti-clickjacking Content-Security-Policy. The Outlook add-in task pane keeps a carve-out so Outlook can embed it — no configuration needed.
  • CSRF protection: cookie-authenticated API requests are rejected when a browser presents a cross-origin Origin (layered on top of the SameSite=Lax cookies), so a malicious page can’t drive the app with a signed-in user’s session.
  • SSRF protection: the server only fetches admin-supplied URLs (quick-link icons, workspace logos, outgoing webhooks) after checking the target — cloud metadata endpoints (169.254.169.254), loopback, and link-local addresses are refused, and every redirect hop is re-validated. Since 0.64.1, intranet (RFC1918) hosts are refused too; set COMPASSDOCS_FETCH_ALLOW_PRIVATE=1 when webhooks, links or a Snipe-IT server need to reach internal tools.
  • Outbound proxy support (0.74.1): on networks that require egress through a proxy, set HTTPS_PROXY (and optionally NO_PROXY) on the container — status polls, outgoing webhooks, AI/embeddings calls, and every other server-side fetch route through it. If the proxy re-signs TLS with a corporate CA, also mount the CA and set NODE_EXTRA_CA_CERTS=/path/to/ca.pem. Settings → System → Diagnostics has an Outbound HTTPS check that verifies egress (direct or proxied) and names the blocker when it fails.
  • Dependency & image scanning run in CI (npm audit, Trivy for vulnerabilities/secrets/misconfig, and Dependabot update PRs), so known-bad dependencies are caught before a release ships.
  • The container runs as a non-root user (uid/gid 1001 as of 0.59.0), so a process escape doesn’t land as root. Only the data directories it needs (/uploads, /backups, custom-TLS certs) are writable.
  • Two-factor auth (authenticator apps + recovery codes) is available on every account — turn it on for admins first. Enterprise installs can move sign-in to single sign-on (Microsoft Entra ID, Google, any OIDC or SAML 2.0 provider) entirely with SSO only. SSO only hides the password form but removes no passwords, so anyone who has one, administrators included, can still sign in at /login?local=1 (or through the API) when the identity provider is down; see Locked out?.
  • Links before sign-in are only the ones you chose (1.17.0). The sign-in page shows the quick links an admin marked Sign-in page, at most eight, and only links visible to Everyone can be marked: their titles, addresses and icons are the only link data anyone can see or fetch without signing in. A link restricted to groups never appears there, and its icon is never served without a session. Public icons are kept out of shared caches, and a browser keeps one for at most an hour.
  • Link clicks are counted without touching the links (1.17.0). The browser follows each link’s real address and sends a same-origin note (a cross-origin one is refused) to count the click. Signed in, a click counts only on a link the person can see, at most 30 a minute per person; signed out, only on a sign-in page link, at most 20 a minute per address. Over the limit the request still succeeds but the click isn’t counted. Clicks are deleted after 90 days.
  • Sign-in artwork is re-encoded, never served as uploaded (1.16.0). The type is read from the file’s bytes (no SVG), the first frame is re-encoded to WebP, which drops camera and location data, and the page serves only those copies, before sign-in, from the uploads volume.

Everything encrypted at rest is sealed under one 32-byte master key. It is resolved in this order:

  1. COMPASSDOCS_SECRET_KEY environment variable — 64 hex characters or 44 base64 characters. Recommended for production; generate one with:

    Terminal window
    openssl rand -hex 32
  2. A key file, auto-generated on first boot with 0600 permissions at $COMPASSDOCS_UPLOAD_DIR/.secret.key (inside the uploads volume, so it survives container recreates). Set COMPASSDOCS_KEY_FILE to move it.

Upgrading to 0.41+ migrates existing plaintext credentials and session rows automatically on first boot — nobody is signed out, and nothing needs re-entering.

The app cannot encrypt the disk it runs on. For full encryption at rest of content (documents, attachments, the directory), use the layer below:

  • Encrypted volumes or disks — LUKS on your own hardware, or the encrypted-disk checkbox every major cloud offers. This covers Postgres data, uploads, and local backups wholesale.
  • Managed Postgres (Azure Database, RDS, Fly Postgres, Neon, …) — all encrypt storage at rest by default and handle TLS to the database. For an external database, add ?sslmode=require to DATABASE_URL.
  • HTTPS — use the built-in custom domain & HTTPS stack (Caddy with automatic Let’s Encrypt) or your own reverse proxy. Never serve a workspace over plain HTTP beyond localhost.
  • Host basics — keep the Docker host patched, restrict SSH, and don’t expose Postgres’s port publicly (the compose files don’t).
  • Outbound traffic: if you restrict it, the helpdesk’s push notifications (1.19.0) need the browsers’ push services; see Outbound hosts for push. Push keys are encrypted under the master key like every other secret.
  1. Set COMPASSDOCS_SECRET_KEY (or copy the auto-generated key file) into your password manager.
  2. Serve over HTTPS with the built-in Caddy stack or your proxy.
  3. Turn on 2FA for every admin account.
  4. Enable scheduled backups with an off-site destination — and test a restore.
  5. Use encrypted disks (or managed Postgres) so document content is covered.
  6. Skim the audit log for auth.lockout and failed sign-ins now and then.