Security Model
Overall security architecture and practices in TMA Cloud.
Overall security architecture and practices in TMA Cloud.
Authentication Security
Password Security
- Hashing: bcrypt, 10 rounds, computed off the request thread so a hash does not delay other requests
- Storage: Hashed passwords only
- Validation: 8–128 characters and at most 72 bytes in UTF-8, enforced by the server and mirrored in the client. bcrypt reads only the first 72 bytes, so a longer password would match any other password that starts with the same 72 bytes
- Unknown accounts: A login for an email with no password hash still runs a bcrypt comparison, so response time does not reveal which emails have accounts
Token Security
- JWT Tokens: Signed and verified
- httpOnly Cookies: Page scripts cannot read the session token, so an injected script cannot steal it
- Expiration: Tokens expire after the idle window and are reissued while in use
- Versioning: Signing out everywhere or changing the password invalidates every older token
Session Security
- Active Session Management: View device, current reported IP, heartbeat-based online status, and revoke sessions
- Logout All: Invalidate all tokens
- Idle Expiry: Sessions end after an inactivity window (30 days by default)
- CSRF: State-changing requests must carry
X-Requested-With: XMLHttpRequest. See API Overview
Authorization Security
Access Control
- Account Isolation: A login can only reach its own account's files
- Role-Based: Admin vs regular user permissions
- Per-Permission Grants: Sub-users hold an explicit set of permissions; anything not granted is refused
- API Authorization: Endpoint-level checks, run before request bodies are read
- Share Link Security: Token-based access. A link can only be created for items in the caller's own account
Input Validation
Request bodies are read into typed Rust structs, so a field with the wrong type is refused before the route runs. Fields the route does not use are ignored. Routes then check names, IDs and limits themselves.
- Type and format checks: Request fields have type, length, and format limits where required
- Database queries: Values are passed through parameterized queries
- File paths: Storage keys reject separators and traversal sequences
- MIME types: Stored types come from file content (magic bytes), not client headers
Data Security
File Storage
- Account Isolation: Every file row belongs to one account, and every query filters by it
- Path Validation: Storage keys reject separators and traversal sequences
- MIME Type Detection: Stored type is detected from content (magic bytes), not the filename; a content/extension mismatch is never used to reject an upload
- Safe Serving: Downloads use
Content-Disposition: attachmentandX-Content-Type-Options: nosniff, so a disguised file downloads instead of running in the browser. Stored files also get their ownsandboxContent Security Policy, so an uploaded HTML or SVG file cannot run script on the site even if it is opened directly
File Encryption
- Algorithm: AES-256-GCM in Google Tink's AES-GCM-HKDF-STREAMING format (
AES256_GCM_HKDF_1MB) - Segmented: Objects are split into 1 MB segments, each authenticated on its own. This lets downloads serve HTTP
Rangerequests without reading the whole file - Envelope keys: Each newly encrypted object version receives a random data key (DEK);
FILE_ENCRYPTION_KEYis the key-encryption key (KEK) that wraps it. A server-side logical copy reuses unchanged ciphertext and its wrapped DEK. Rotating the KEK rewraps stored DEKs without re-encrypting objects - Scope: Stored file contents in the S3-compatible bucket. File names and metadata live in the database
- Key Management: The KEK comes from
FILE_ENCRYPTION_KEYor a file named byFILE_ENCRYPTION_KEY_FILE, never from the database. Production requires a random 32-byte key - Stream Processing: Files are processed in segments instead of being buffered in full
- Automatic: Encryption on upload, decryption on download
Key loss means data loss
The KEK is read from the environment or a secret file. If you lose it — or change it without rotating (see CLI Commands) — previously stored files cannot be decrypted and are effectively unrecoverable. Back the key up somewhere safe (password manager, secrets vault) before taking the deployment to production.
Key Placement
The KEK is kept outside the database on purpose. The database holds every wrapped DEK and the encrypted bucket secret, so a KEK stored next to them would let anyone with a database dump or backup decrypt all files. With the KEK held separately, a leaked database alone exposes no file contents and no credentials.
To keep the key out of docker inspect output and the process environment, it is read from a file named by FILE_ENCRYPTION_KEY_FILE. docker-compose.yml does this with secrets/file_encryption_key, which setup.sh generates. See Docker Deployment.
Key Check
The kek_checks table holds one check value per KEK version: an HMAC-SHA256 of a fixed label under a subkey derived from the KEK with HKDF. It confirms that a key is the right one without revealing anything about the key.
- At startup the API, the worker, and
tma-server rotatecompare every key in the keyring with its stored check value. On a mismatch they stop with a message naming the version, for exampleFILE_ENCRYPTION_KEY (key version 2) does not match the key this deployment's data was encrypted with. - They also stop when a file key or the bucket secret is wrapped under a version the keyring lacks, for example after an old key line was removed too early.
- The first time a key version is seen, it must open a stored DEK or the bucket secret already sealed under that version before its check value is recorded.
Without this check, a wrong key would let the server start and then fail every download.
Bucket Credentials
The bucket is configured in Settings → Storage by the first user. See Storage Bucket.
- Encryption: The secret access key is encrypted with AES-256-GCM under a subkey derived from the KEK with HKDF-SHA256 (
tma-cloud/storage-credentials/v1), using a random 12-byte nonce per save. The access key ID is the associated data, so a ciphertext cannot be moved to another key's row - Never returned: The API never returns the secret; the access key ID is shown masked
- Memory only: Each process holds the decrypted secret in memory and never writes it to Redis
- Redaction:
secretAccessKeyis redacted from request logs and audit metadata - Verified before saving: A bucket is saved only after a live connection check passes
- Endpoint limits: Plain
http://is accepted only for private network addresses, and link-local addresses such as169.254.169.254are rejected - Rotation: A master key rotation rewraps the secret under the new key version. See Key Rotation
Database Security
- Parameterized Queries: Values never become part of the SQL text
- Connection Security: TLS with
?sslmode=requireinDATABASE_URL - Credentials: In
.envor*_FILEsecret files, never in the database
Network Security
HTTPS
- Recommended: Serve TMA Cloud over HTTPS through a reverse proxy in production
- Cookie Security: In production the session cookie carries the
Secureflag unlessFORCE_INSECURE_COOKIESis set - Headers: See Security Headers
- S3: Bucket policy can enforce HTTPS-only access; run
tma-server s3 policy-httpsortma-server s3 protect-all(see Storage Management).
Rate Limiting
Separate limits apply to sign-in, uploads and the general API. See Rate Limits.
Forwarded Client IPs
The backend ignores forwarded client IP headers unless the connecting reverse proxy is listed under Settings → Administration → Known proxies. This prevents a direct client from supplying its own X-Forwarded-For value.
Desktop App Only Access (Optional)
- Admins can place an instance into a mode where the backend requires a custom HTTP header that only the desktop app sends
- The middleware checks this header for all main app routes and blocks requests that do not include it
- Share links (
/s/*),/health, and/metricsremain reachable without the header - The OnlyOffice file and callback endpoints (
/api/onlyoffice/file/*,/api/onlyoffice/callback) also skip the header check. The Document Server calls them itself and does not send the header; both are authenticated by the OnlyOffice JWT. When the Document Server reports a saved document, the server fetches it only from the Document Server's own host or from a public address. It refuses hostnames that resolve to private or internal addresses, and gives up on a download that stalls for a minute - This setting is an access policy, not client attestation. A client that reproduces the header can pass the check, so authentication and authorization remain required
Audit & Logging
Audit Trail
- Recorded events: Authentication, file, sharing, document, admin, and account actions
- Writing: each process batches events in memory and writes them about every half second; see Audit Logs
- Retention: Rows older than 30 days are removed by the scheduled cleanup job
- Queries: The
audit_logtable andaudit_activityview expose stored events
Logging Security
- Secret Masking: Passwords, tokens and keys are redacted from request logs and audit metadata
- Structured Logs: One JSON object per line with
LOG_FORMAT=json - Request Tracking: Every log line carries the request ID, which is echoed in
X-Request-ID
Security Headers
- CSP: Content Security Policy. See Content Security Policy
- X-Frame-Options:
DENY, prevents clickjacking - X-Content-Type-Options:
nosniff, prevents MIME type sniffing - X-XSS-Protection:
0, turns off the legacy browser XSS filter, as OWASP recommends. CSP replaces it - Referrer-Policy:
strict-origin-when-cross-origin - Permissions-Policy: Camera, microphone, and geolocation disabled
Content Security Policy
script-src does not include 'unsafe-inline' or 'unsafe-eval', so an injected script does not run. It includes 'wasm-unsafe-eval', which lets the browser compile the web app's WebAssembly and does not allow eval(). Scripts are allowed when they come from:
- The app's own origin
- The configured OnlyOffice Document Server origin, when OnlyOffice is set up
- A per-request nonce. Server-rendered pages (share folder listings, the OnlyOffice viewer) add the nonce to their inline scripts
- A SHA-256 hash of each inline script in the built web app's
index.html
The server computes the hashes at startup and again whenever index.html changes, so a new web build is allowed without a restart.
The policy also sets object-src 'none', frame-ancestors 'none', base-uri 'self', and form-action 'self'. style-src keeps 'unsafe-inline' for the inline styles of server-rendered pages.
Values placed into server-rendered HTML are HTML-escaped. JSON embedded in an inline script has <, >, &, U+2028, and U+2029 escaped, so a value such as a display name containing </script> cannot close the script element.
Before Going to Production
- Serve the app over HTTPS, and add the reverse proxy under Settings → Administration → Known proxies
- Back up the encryption key apart from database backups. See Backups
- Run
tma-server s3 protect-allagainst the bucket. See Storage Management - Create your own account first, then turn Allow sign-up off unless others should register
- Turn on two-factor authentication for the administrator account
Related Topics
- Authentication - Authentication system
- Authorization - Access control
- Audit Logs - Audit system
- Error Codes - Error responses
Last updated on