Security
TelegramOS security: tenant isolation, authentication, encryption at rest, audit trail and outbound-request controls.
The platform implements security controls at multiple layers to protect tenant data, prevent unauthorized access, and ensure safe operation. This page describes what is actually enforced in the running code.
Tenant isolation
| Layer | Mechanism | What it prevents |
|---|---|---|
| Tenant boundary | Every resource row (account, proxy, bot, conversation, wallet) carries the owning user's ID. Read and write queries are scoped to the caller's own tenant plus any team tenants that grant the specific permission (for example inbox:read, accounts:delete); on a lookup failure the scope falls back to owner-only. A foreign or malformed ID returns 404, not 403. | One tenant reading or changing another tenant's data through the API or UI. |
| Per-account runtime | Each active account runs as its own child process inside the worker manager, with its own decrypted session, its own MTProto connection, its own device fingerprint and its own proxy. No connection is multiplexed across accounts. All workers share the worker-manager container's CPU and memory limits; there is no per-process sandbox. | One account's Telegram session being observed or reused by another account's worker. |
| Per-account proxy | An account is bound to exactly one proxy; assigning several accounts to the same proxy is refused. Platform-rented proxies are provisioned as a per-account provider sub-user and torn down when the account is released. Rotation is per account. | Telegram linking accounts through a shared exit IP. |
| Rented-account recycling | A tenant only receives an account that is currently unallocated in the pool. When a rental ends (subscription or bundle expiry plus a 5-day grace period, or an explicit return), the reclamation worker deletes every chat on both sides, clears the tenant binding and returns the account to the pool with its session intact, so a later tenant can receive it. Accounts whose session died are quarantined and never re-sold. A paid permanent delete ($1) logs the session out, wipes the encrypted session blob and retires the account for good. | A new tenant inheriting a previous tenant's conversations, and a previous tenant keeping a way back into the account. |
| Active-session enforcement | Every 6 hours, for each rented account, the platform lists the account's active Telegram sessions. Unknown sessions raise a security notification with two actions, "It's me" and "Terminate". Nothing is terminated until you decide, except sessions approved by a previous tenant, which are revoked when the account changes hands. | Unauthorized access to a rented account through a session opened outside the platform. |
| Hidden service peers | Conversations with Telegram's system account (peer 777000, plus any IDs in the hidden_telegram_peers table) are filtered out by the inbox API for rented accounts only. Owned accounts always see them. The filter is applied on read; the rows themselves are stored. | A rental holder reading login codes for the underlying phone number. |
Authentication
Password requirements
| Requirement | Value |
|---|---|
| Minimum length | 12 characters |
| Maximum length | 72 bytes (bcrypt limit); non-English letters and emoji take more than one byte |
| Character mix | No composition rules; the password may not equal your email or name |
| Breached passwords | New passwords are checked against the Have I Been Pwned list; only the first 5 characters of the SHA-1 hash are sent, and the check is skipped if that service does not answer within 2 seconds |
| Existing passwords | Passwords set under the earlier rules keep working until they are changed |
| Storage | bcrypt, cost factor 12 |
| Social-login accounts | May have no password until one is set in Settings |
A failed login takes the same time whether the email exists or not, so the login endpoint cannot be used to enumerate accounts.
Sessions and tokens
| Item | Value |
|---|---|
| Access token | RS256 JWT, 15 minutes |
| Refresh token | Random 512-bit token, 7 days, rotated on every use with a 30-second reuse grace |
| Cookies | HttpOnly, Secure, SameSite=None; the refresh cookie is scoped to /auth |
| Server-side session records | Expired records are pruned after 7 days |
| API keys | tgp_ prefix, stored as SHA-256 hash, default expiry 90 days, at most 20 per user |
| Deactivated users | A user with is_active = false is refused at login, refresh and API-key authentication |
Two-factor authentication (TOTP)
| Setting | Value |
|---|---|
| Algorithm | TOTP (RFC 6238), 6 digits, 30-second step, one step of clock drift accepted |
| Replay protection | A code that was already accepted is rejected for the rest of its window |
| Backup codes | 8 codes, each valid once, stored as SHA-256 hashes |
| Secret storage | Fernet-encrypted at rest when a TOTP encryption key is configured |
| Attempt limit | 10 failed attempts per account, then a 15-minute lock regardless of source IP |
Rate limiting
| Endpoint | Limit |
|---|---|
| POST /auth/login | 10 requests per minute per IP |
| POST /auth/register | 5 requests per minute per IP, plus the email-send cap below |
| POST /auth/forgot-password, /resend-verification | 5 emails per hour per IP; per address: one send per 60 seconds, 10 per day |
| POST /auth/reset-password, /verify-email | 30 requests per minute per IP; each code is valid 10 minutes and dies after 5 wrong attempts |
| Team mutations (invite, update, remove) | Per-user mutation limiter |
Limits use an atomic sliding window in Redis. When a limit is exceeded the platform returns HTTP 429 with a Retry-After header equal to the window length. There is no application-level lockout after repeated 429s. At the edge, the platform sits behind Cloudflare, nginx terminates TLS 1.2/1.3 with HSTS, and fail2ban bans hosts that generate malformed requests.
Encryption
At rest
| Data | Encryption |
|---|---|
| Telegram session strings | Fernet (AES-128-CBC + HMAC-SHA256) under the platform session key. Optional envelope format: a per-account data key wrapped either by the platform key (standard) or by an Argon2id key derived from the owner's passphrase (vault mode, see below). |
| Proxy credentials | Fernet, same platform key |
| Bot API tokens | AES-256-GCM with a key derived via HKDF-SHA256 from the platform master secret |
| Outbound webhook signing secrets | Fernet |
| TOTP secrets | Fernet, when a TOTP encryption key is configured |
| Passwords, backup codes, refresh tokens, API keys | Stored only as hashes (bcrypt for passwords, SHA-256 for the rest) |
| Message bodies, conversation metadata, contacts | Not encrypted at the application layer; plain rows in Postgres |
| Attachments (MinIO, 20 MB per file) | Not encrypted server-side; access goes through a tenant-scoped streaming endpoint |
| Database backups | Not encrypted; stored on local volumes on the database host, with no off-site copy |
There is no external KMS and no automated key rotation. Encryption keys are operator-managed environment secrets, kept outside the database. Rotating a key is a manual operation.
Vault mode (passphrase-encrypted sessions)
For owned accounts you can re-wrap the session under a passphrase (minimum 10 characters). The wrapping key is derived with Argon2id (t=3, m=64 MiB, p=4); the platform key can no longer decrypt the session, so a database dump is useless without the passphrase. The trade-off: automation only runs while an unlock window is open (default 1 hour, maximum 24 hours). The derived key is handed to the worker in memory and never persisted. Session export is refused while vault mode is on. Rented accounts cannot use vault mode.
In transit
All public traffic is HTTPS (TLS 1.2 or 1.3, HSTS with preload) behind Cloudflare. Telegram traffic leaves through the account's proxy as MTProto, which is encrypted end-to-server by Telegram's own protocol.
Connection and session monitoring
- Every Telegram connect attempt per account is written to
connection_audit_logwith the proxy host and port, API ID, device model, connection type, success or error, duration and whether the connection came from a server IP. - Rented accounts get the 6-hour active-session scan described above; pending prompts appear under Notifications with type
security. - There is no geolocation-based alerting, no device-fingerprint drift detection and no automatic session kill based on health score.
Audit trail
| Log | What is recorded | Who can read it | Retention |
|---|---|---|---|
| Team audit log | team.create, team.member.invite, team.member.update, team.member.remove, team.member.leave with actor, IP address, user agent and a before/after snapshot | Owner and admin (audit:logs:read); every user can read their own actions | Kept indefinitely |
| Platform audit log | session_export, proxy lifecycle (proxy_created, proxy_updated, proxy_deleted, proxy_assigned, proxy_unassigned, proxy_rotated), session enforcement (enforcement_check, prompt_emitted, check_failed) | Operators | Kept indefinitely |
| Connection audit | Per-connect record described above | Operators | Kept indefinitely |
| Account health log | Health-score inputs per account | Operators | 90 days |
| Webhook deliveries | Event type, payload, HTTP status, first 400 bytes of the response, attempts | Workspace | 7 days for delivered, 30 days for failed |
The team audit log is available at GET /audit/logs (filter by team_id, action, resource_type). See API.
SSRF protections
The platform makes outbound HTTP requests on your behalf in four places. Each has its own guard:
| Caller | Guard |
|---|---|
| Event webhooks (inbox) | New URLs must be https://. The host is resolved at registration and again before every delivery, and the delivery is sent to that vetted address; loopback, private, link-local, carrier-NAT, reserved and multicast addresses (IPv4 and IPv6), localhost and *.local are refused. A stored endpoint that now resolves to a refused address fails with a clear error instead of being delivered. Deliveries do not follow redirects, time out after 10 seconds, retry up to 6 times over about 8 hours, and the endpoint is switched off after 5 consecutive final failures. |
| Bot webhook URL | Resolved via DNS at registration; refused if any resolved address is private, loopback, link-local, multicast, unspecified or reserved. |
| Flow HTTP-request node | Resolved at request time and the connection is pinned to the validated public IP (closes DNS rebinding); 15-second total and 5-second connect timeout. Requests are not signed. |
| API connectors | http:// or https:// only. The host is resolved when the connector is saved and tested, and the request is sent to that vetted address; loopback, private, link-local, carrier-NAT, reserved and multicast addresses (IPv4 and IPv6, including IPv4-mapped and numeric encodings), localhost and *.local are refused. Redirects are not followed. |
| Checkout redirect URLs | Resolved and refused if they land on a private or loopback address. |
Plain http:// webhook targets are accepted; use HTTPS wherever you can.
Bot webhook verification
Inbound bot updates are verified with the per-bot X-Telegram-Bot-Api-Secret-Token secret (see Bots). Outbound event webhooks are signed: every delivery carries X-TGP-Signature, an HMAC-SHA256 of the timestamp and raw body under the endpoint's secret, plus X-TGP-Timestamp and X-TGP-Delivery — see API › Webhooks. Requests made by a flow's HTTP-request node are not signed; protect those endpoints with a secret path or an allowlist.
Reporting a vulnerability
Report through in-app Support (the Support bubble in the dashboard), or via @TelOsNotif_Bot if you do not have an account. There is no security mailbox. Include reproduction steps and the affected endpoint. Do not test against accounts or workspaces you do not own. Scope, out-of-scope items and the response targets (acknowledgement within 24 hours, verified high-severity issues targeted for remediation within 14 days) are on /security. See also Support.