Skip to content

Security & Authentication

This page describes how Assimilate authenticates users and agents, protects credentials at rest, and enforces access control.

Authentication Mechanisms

Assimilate supports three authentication methods:

Method Transport Use case
Session cookie Cookie: session=<id> Browser UI
API token Authorization: Bearer <token> Scripts, CI, REST API clients
Agent token WebSocket Hello message Agent binary connecting to server

All three methods resolve to an authenticated identity before any handler runs. Bearer tokens and session cookies are interchangeable for API access.

Session Security

When a user logs in via the UI, the server creates a session record in the database and sets a cookie:

Set-Cookie: session=<uuid>; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400

Key properties:

  • HttpOnly — the cookie is not accessible from JavaScript.
  • SameSite=Lax — mitigates CSRF for cross-site navigations.
  • Max-Age=86400 — sessions expire after 24 hours.
  • The session ID is a random UUID stored in the database. Logging out deletes the session record immediately.

API Tokens

API tokens allow programmatic access without a browser session.

  • Tokens are created through the UI or the API (POST /api/tokens).
  • The plaintext token is shown once at creation time and never stored.
  • The server stores only the SHA-256 hash of the token.
  • Each use updates a last_used timestamp on the token record.
  • Admins can view and delete all tokens. Regular users can only manage their own.

To authenticate with a token:

Authorization: Bearer <plaintext-token>

Agent Tokens

Each agent registered in Assimilate has a unique agent token.

  • Tokens are generated with 32 bytes of cryptographic randomness.
  • The server stores the token as a bcrypt hash.
  • The agent presents its token in the WebSocket Hello handshake when connecting.
  • The same token is used to authenticate the SSH agent forwarding WebSocket endpoint.

Agent tokens are scoped to a single agent. Revoking an agent removes its token.

Role-Based Access Control

Assimilate uses a single RBAC layer: users are assigned one or more roles via the user_roles table. Each role bundles system-wide capabilities (e.g. create agents, manage schedules). A user's effective permissions are the union of all capabilities across every role they hold. See Access Control for the full capability matrix.

The admin bypass is determined by the can_delete_repo capability — a user with this permission (from any role they hold) passes all permission checks. The built-in admin role grants this capability; custom roles can also include it.

Beyond roles, per-repository permissions control what a user can do on individual repositories.

Capability With can_delete_repo Without can_delete_repo
Manage users and roles ✓ ✗
Create and delete agents ✓ depends on other RBAC capabilities
View and manage all repositories ✓ per-repo permission
Manage API tokens (all users) ✓ own tokens only
Configure SSH tunnels ✓ depends on other RBAC capabilities
View system information ✓ ✗
Trigger backups ✓ per-repo permission
Browse archives ✓ per-repo permission

Per-Repository Permissions

Users with can_view_all_repos can see all repositories. For others, fine-grained access to individual repositories is granted by admins:

Permission Effect
can_view User can see the repository and its archives
can_backup User can trigger a backup run
can_modify_schedules User can create and edit backup schedules
can_extract User can browse and extract archive contents
can_delete User can delete archives

Permissions are managed by admins under Settings → Access Control.

Session Idle Timeout

An idle timeout can be configured under System Settings. Sessions that have no API activity for longer than the configured duration are automatically revoked on the next request.

  • Default: 480 minutes (8 hours).
  • When idle timeout is exceeded, the session is deleted from the database and the user receives a 401 Unauthorized response with the message "session expired due to inactivity".
  • The idle window slides forward on real activity, not just /api/auth/me and /api/auth/refresh: AuthUser::from_request_parts (which runs before every protected handler) writes last_seen_at on any authenticated request, throttled to at most once per 60 seconds per session to avoid a DB write on every single API call.
  • This is enforced in AuthUser::from_request_parts, which runs before every protected handler, so there is no gap between the timeout elapsing and access being denied.
  • "Remember Me" sessions are exempt. A session created by checking "Remember Me" at login is not subject to the idle timeout — it stays valid until its absolute 7-day expiry, which the frontend transparently extends by another 7 days as long as the session keeps being used before it lapses. Applying the (much shorter) idle timeout on top of remember_me would defeat the purpose of the checkbox.

Two-Factor Authentication (TOTP)

Users can enable TOTP-based two-factor authentication from their Profile page. Once enabled, logging in requires two steps:

  1. Password step — the user authenticates with their username and password. The server returns a totp_required: true flag and a short-lived temp_token.
  2. TOTP step — the user provides the 6-digit code from their authenticator app, along with the temp_token. The server verifies the code and issues a real session cookie.

Security properties:

  • Temp tokens are stored in the same sessions table but with pending_totp = true. The AuthUser middleware rejects pending sessions, so a temp token cannot be used to access any API endpoint except TOTP verification/recovery.
  • TOTP codes use RFC 6238 (SHA-1, 30-second period, 6 digits).
  • Replay protection — each successful login records the exact 30-second time-step its code belonged to. A later login is rejected as "TOTP code already used" only if its code maps to that same step or an earlier one; a different, currently-valid code from a later step (e.g. a second device logging in shortly after the first) is accepted.
  • Recovery codes — 10 codes are generated at enrollment using 8 bytes of cryptographic randomness, formatted as hex groups. They are hashed with bcrypt before storage. Each code is single-use: after verification it is removed from the stored set.
  • Secret encryption — the TOTP secret is encrypted with AES-256-GCM using the same key derivation as repository passphrases.
  • Enrollment can be cancelled; TOTP is only activated after a successful verification code confirms the user can generate valid codes.
  • Setup (/api/auth/totp/setup) is rejected with 400 Bad Request while TOTP is already enabled, so an authenticated-but-compromised session (e.g. via XSS) cannot silently overwrite a legitimate secret; disabling first requires the current password.

Session Management

Users can view and manage their active sessions from the Sessions tab in their Profile page.

  • Each session displays creation time, expiration, last activity, and the "Remember Me" flag.
  • Users can revoke any session except their current one.
  • Session revocation checks ownership at the database layer (DELETE FROM sessions WHERE id = $1 AND user_id = $2), preventing a user from revoking another user's session.
  • Session IDs are stored as SHA-256 hashes; the plaintext UUID is never persisted.

Brute-Force Protection

  • Password login additionally tracks failed attempts per username and client IP address in the database: 5 failed attempts within a 15-minute window trigger a 429 Too Many Requests lockout for that username/IP pair, and all attempts (successful and failed) are recorded.
  • Account-wide lockout, independent of source IP: after 10 consecutive failed login attempts for a username (since its last successful login, unbounded window), the account itself is locked via users.locked_until and further attempts return 401 Unauthorized regardless of whether the password is later correct. This closes the gap the per-(username, IP) limiter above leaves open — an attacker who rotates source IPs can't bypass a per-account limit the way they can bypass a per-IP one.
  • Lockout duration escalates with each consecutive lockout cycle (a threshold-crossing failure while the account isn't already locked): 1 minute → 5 minutes → 15 minutes → 1 hour → 24 hours, capped at 24 hours, tracked via users.lockout_escalation_level.
  • A successful login clears the lockout and resets the escalation counter back to the shortest tier; a lockout that merely expires with time (no successful login in between) does not reset it, so an attacker can't reset the escalation clock just by waiting out each lockout.
  • Login response timing is uniform across the nonexistent-username, wrong-password, and locked-account cases (a dummy bcrypt hash is used when the username doesn't exist, and the locked-account branch performs the same database work as the wrong-password branch), preventing both username enumeration and lockout-state detection by response time.
  • TOTP code and recovery-code verification (/api/auth/totp/verify-login, /api/auth/totp/recovery, and /api/auth/totp/disable) track failed attempts per user account the same way: 5 failed attempts within a 15-minute window lock out further attempts for that account with 429 Too Many Requests, regardless of source IP. This closes the gap an IP-only cap leaves open — an attacker who already has a valid password (or a hijacked authenticated session, for the disable endpoint) could otherwise distribute code/recovery-code/password guesses across many source IPs.
  • /api/auth/login and /api/auth/totp/verify-login + /api/auth/totp/recovery (which share a bucket with each other, separate from plain login) are each capped at 30 requests per minute per IP, as a coarser backstop on top of the per-account lockouts above. When ASSIMILATE_TRUSTED_PROXIES is configured, this per-IP resolution honors X-Forwarded-For only from those trusted proxies; otherwise the direct peer address is used.
  • Per-user API rate limiting: once authenticated, mutating requests (POST/PUT/PATCH/DELETE) to any authenticated route are capped at 60 requests per minute per user, independent of the login-specific limits above. Read requests (GET/HEAD/OPTIONS) are not throttled, so UI polling and automated test suites aren't affected.
  • API token and agent token authentication is not subject to either of the above; invalid tokens are rejected immediately.

Passphrase Encryption

Borg repository passphrases are encrypted at rest using AES-256-GCM.

The encryption key is derived from ASSIMILATE_SECRET_KEY using HKDF-SHA256. A random 96-bit nonce is generated for each encryption operation, so identical passphrases produce different ciphertexts.

The same scheme protects the other secrets Assimilate stores: TOTP secrets, the SMTP password of an email notification channel, and every header value of a webhook notification channel. Both notification secrets are kept apart from the channel's configuration, are decrypted only when the server sends the notification, and are never returned by the API. The API reports only has_password and, for webhook headers, each header's name with a has_value flag. A saved secret is only ever sent to the server it was entered for: changing an email channel's SMTP host, port or security mode, or a webhook URL's scheme, host or port, requires entering it again.

Security

If you change or lose ASSIMILATE_SECRET_KEY, all stored passphrases, SMTP passwords and webhook header values become unrecoverable. Back up this value and keep it stable for the lifetime of your deployment.

Generate a strong key before first run:

openssl rand -hex 32

See Configuration for how to set this environment variable.

Password Policy

  • Passwords must be at least 8 characters.
  • Passwords are hashed with bcrypt before storage. The plaintext is never persisted.
  • The API rejects change-password requests that do not meet the minimum length.

Repository Relocation Protection

Borg verifies that a repository has not been moved or swapped since the last access. If a malicious actor replaces the remote repository with a different one, borg refuses to operate on it unless explicitly told to accept the relocation.

Assimilate enforces a one-shot relocation acceptance model:

  • The BORG_RELOCATED_REPO_ACCESS_IS_OK environment variable is only set when an admin has explicitly changed the repository's path, SSH host, or SSH port.
  • The acceptance applies to a single backup run. Once that backup succeeds, the flag is cleared.
  • At all other times, borg will reject unexpected repository relocations, protecting against silent data-store substitution.

See Repositories — Relocation Safety for operational details.

Response Security Headers

Every HTTP response from the server carries a standard set of hardening headers, applied by a single middleware layer ahead of all routes:

Header Value Purpose
Content-Security-Policy default-src 'self'; ...; connect-src 'self'; ... Restricts script/style/image/font/connect sources to the app's own origin. The API docs route (/api/docs) uses a relaxed variant that additionally allows https://cdn.jsdelivr.net for the Scalar UI. connect-src is same-origin only — it does not allow WebSocket connections to arbitrary hosts.
X-Content-Type-Options nosniff Stops browsers from MIME-sniffing responses (e.g. archive/file content served via export) into an executable type.
Referrer-Policy no-referrer Prevents the app's URL/path from leaking to webhook or other external targets via the Referer header.
Permissions-Policy geolocation=(), camera=(), microphone=() Disables browser features the app never uses.
Strict-Transport-Security max-age=63072000; includeSubDomains Sent only when ASSIMILATE_SECURE_COOKIES is enabled (the default), since that setting already signals a TLS deployment. See Configuration.

Sensitive Data Handling

  • Repository passphrases are never logged or transmitted in plaintext. They are decrypted in memory only when passed to the borg subprocess.
  • API tokens and agent tokens are stored as hashes. The plaintext is never written to the database or logs.
  • Debug and trace output uses [REDACTED] placeholders wherever sensitive values would otherwise appear.

First-Run Security Checklist

When you start Assimilate for the first time:

  1. Default credentials — the server creates an admin account with password admin.
  2. Forced password change — the UI requires you to set a new password before you can use the application.
  3. Set ASSIMILATE_SECRET_KEY — generate a strong random value and keep it stable. Without it, passphrase encryption cannot function.
  4. Enable TOTP/2FA — enroll two-factor authentication on the Profile page for all administrative accounts.
  5. Configure idle timeout — set a session idle timeout under System Settings to automatically revoke inactive sessions.
  6. Review user accounts — create per-user accounts with the least privilege needed. Avoid sharing the admin account.
  7. Rotate agent tokens — if an agent token is ever exposed, delete the agent and recreate it to issue a new token.

See Getting Started for the full setup walkthrough.