Access Control And 2FA¶
This document describes the authorization path implemented in code.
Source of truth:
pytmbot/middleware/access_control.pypytmbot/middleware/session_manager.pypytmbot/middleware/session_wrapper.pypytmbot/utils/totp.py
Request Pipeline¶
For class middlewares, the runtime order is:
UpdateDedupAccessControlRateLimitCallbackAcknowledger(answers callback queries that handlers left unanswered)
The middleware chain is assembled in pytmbot/pytmbot_instance.py.
AccessControl Middleware¶
AccessControl protects all message and callback-query updates.
Current rules:
- Allowed users are read from
access_control.allowed_user_ids. - Unauthorized users accumulate attempts.
- After
3failed attempts, a user is blocked for3600seconds. - Cleanup of expired block state runs in a background thread every
3600seconds. - Admin notifications for repeated unauthorized access are suppressed for
300seconds per user.
Bootstrap exception:
/getmyidis intentionally allowed for unauthorized users.- This supports initial setup and ID discovery before the allowlist is complete.
Session Management¶
SessionManager stores per-user authentication state for privileged flows.
States:
unauthenticatedprocessingauthenticatedblocked
Default timing:
- session timeout:
10minutes - cleanup interval:
600seconds - user-facing invalid TOTP attempts before a block:
3 - short-window TOTP burst limit: at least
5attempts per60seconds - temporary TOTP block duration:
10minutes
Implementation notes:
SessionManageris a named singleton with weak-reference instance storage.- Expired sessions are removed by a background cleanup thread: authenticated sessions by login time, pending
(
processing) sessions after10minutes without activity, and blocked sessions only once the block has ended. - Session statistics are exposed to the health subsystem.
Two-Factor Authentication¶
2FA is built in. It is not implemented as a plugin.
Current behavior:
- Sensitive handlers use
two_factor_auth_requiredfrompytmbot/middleware/session_wrapper.py. - TOTP codes are 6-digit codes with a 30-second interval.
- QR codes are generated from the per-user TOTP secret.
- The secret is derived from the user ID, the Telegram username, and the configured salt. Users without a username
get a secret derived from the user ID and salt only. Adding or removing a username changes the secret, so the user
must scan the QR code again (
/qrcode). - In group chats, buttons are bound to the user who opened the screen; a press by anyone else is answered with "This button belongs to another user." before any 2FA check and never deletes the owner's message.
- 2FA happens only in a private chat with the bot. The QR code (
/qrcode, "Get 2FA QR code") and code entry ("Enter 2FA code", the code itself) are never handled in a group. A protected action in a group posts a short notice with an "Open private chat" button (https://t.me/<bot>?start=2fa) instead of the 2FA keyboard; the notice holds no codes. A code sent to a group during 2FA is deleted when the bot has the right to, is not checked, and does not count as an attempt. /start 2fain the private chat opens the 2FA screen for an admin, or says that 2FA is already confirmed. The 2FA session is per user, so after confirming in private the user repeats the action in the group.- After a successful code, the "Return" button opens the screen the user was on; for container actions it opens the container's Manage screen and never repeats Start/Stop/Restart.
- Successful verification transitions the user session to
authenticatedand resets the failed-attempt counter. - QR code messages are deleted automatically after 60 seconds. If too many messages are already waiting for auto-deletion, the QR code is not sent; if its deletion cannot be scheduled, it is removed right away.
- Replayed or invalid codes are rejected by
pytmbot/utils/totp.pyand count as failed attempts. - Replay state is persisted in the runtime state directory when it is writable. If writing fails, the error is logged and the outdated state file is removed.
- Restart barrier: when the replay state cannot be loaded at startup (for example, the state directory is on tmpfs and was wiped), codes that were valid before the process started are rejected. After a restart the user may need to wait for the next code (up to about a minute); this rejection does not count as a failed attempt.
What Is Protected¶
The bot uses allowlists for all access and applies 2FA only to selected sensitive operations.
Examples of 2FA-protected flows in the current codebase:
- Docker container management actions
- runtime container details
- log access
The exact protected handlers are the ones decorated with two_factor_auth_required.
Observability¶
Access and authentication subsystems emit structured logs for:
- unauthorized access attempts
- blocks and block expiry
- session lifecycle
- TOTP failures
- privileged action denial
See also: