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_usedtimestamp 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
Hellohandshake 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 Unauthorizedresponse with the message "session expired due to inactivity". - The idle window slides forward on real activity, not just
/api/auth/meand/api/auth/refresh:AuthUser::from_request_parts(which runs before every protected handler) writeslast_seen_aton 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_mewould 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:
- Password step — the user authenticates with their username and password. The server returns a
totp_required: trueflag and a short-livedtemp_token. - 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
sessionstable but withpending_totp = true. TheAuthUsermiddleware 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 with400 Bad Requestwhile 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 Requestslockout 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_untiland further attempts return401 Unauthorizedregardless 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 with429 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/loginand/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. WhenASSIMILATE_TRUSTED_PROXIESis configured, this per-IP resolution honorsX-Forwarded-Foronly 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_OKenvironment 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:
- Default credentials — the server creates an
adminaccount with passwordadmin. - Forced password change — the UI requires you to set a new password before you can use the application.
- Set
ASSIMILATE_SECRET_KEY— generate a strong random value and keep it stable. Without it, passphrase encryption cannot function. - Enable TOTP/2FA — enroll two-factor authentication on the Profile page for all administrative accounts.
- Configure idle timeout — set a session idle timeout under System Settings to automatically revoke inactive sessions.
- Review user accounts — create per-user accounts with the least privilege needed. Avoid sharing the admin account.
- 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.