TMA CloudTMA Cloud
Concepts

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: attachment and X-Content-Type-Options: nosniff, so a disguised file downloads instead of running in the browser. Stored files also get their own sandbox Content 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 Range requests without reading the whole file
  • Envelope keys: Each newly encrypted object version receives a random data key (DEK); FILE_ENCRYPTION_KEY is 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_KEY or a file named by FILE_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 rotate compare every key in the keyring with its stored check value. On a mismatch they stop with a message naming the version, for example FILE_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: secretAccessKey is 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 as 169.254.169.254 are 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=require in DATABASE_URL
  • Credentials: In .env or *_FILE secret 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 Secure flag unless FORCE_INSECURE_COOKIES is set
  • Headers: See Security Headers
  • S3: Bucket policy can enforce HTTPS-only access; run tma-server s3 policy-https or tma-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 /metrics remain 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_log table and audit_activity view 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-all against 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

Last updated on

On this page