{"id":50134476,"url":"https://github.com/egor051/vpnbot","last_synced_at":"2026-05-23T21:01:40.429Z","repository":{"id":352742777,"uuid":"1216432197","full_name":"Egor051/vpnbot","owner":"Egor051","description":"Telegram VPN bot for self-hosted Ubuntu VDS. Manages Xray VLESS Reality, AmneziaWG, SOCKS5/Dante, and MTProto Proxy with user approval, audit logging, and traffic statistics.","archived":false,"fork":false,"pushed_at":"2026-05-16T23:28:41.000Z","size":791,"stargazers_count":1,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-16T23:47:59.589Z","etag":null,"topics":["aiogram","amneziawg","mtproto","proxy","python","selfhosted","socks5","sqlite","systemd","telegram-bot","ubuntu","user-management","vless-reality","vpn-bot","wireguard","xray"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Egor051.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-20T22:41:42.000Z","updated_at":"2026-05-16T23:28:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Egor051/vpnbot","commit_stats":null,"previous_names":["egor051/vpnbot"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Egor051/vpnbot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Egor051%2Fvpnbot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Egor051%2Fvpnbot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Egor051%2Fvpnbot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Egor051%2Fvpnbot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Egor051","download_url":"https://codeload.github.com/Egor051/vpnbot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Egor051%2Fvpnbot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33412082,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T18:09:33.147Z","status":"ssl_error","status_checked_at":"2026-05-23T18:09:31.380Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aiogram","amneziawg","mtproto","proxy","python","selfhosted","socks5","sqlite","systemd","telegram-bot","ubuntu","user-management","vless-reality","vpn-bot","wireguard","xray"],"created_at":"2026-05-23T21:01:39.143Z","updated_at":"2026-05-23T21:01:40.416Z","avatar_url":"https://github.com/Egor051.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# VPN Telegram Bot\n\nTelegram bot for self-hosted VPN access management on an Ubuntu VDS. The bot manages users, access approval, Xray VLESS Reality keys, AmneziaWG keys, key revocation/deletion, audit records, and basic traffic statistics.\n\nThis project is designed for a single-server deployment without Docker, Redis, PostgreSQL, or a heavy ORM.\n\n## Features\n\n- Telegram user registration and access approval flow.\n- Admin panel for pending requests, users, key issuance, audit, stats, and announcements.\n- Xray VLESS Reality key creation, config delivery, revocation, deletion, and startup reconciliation.\n- AmneziaWG key creation, client config delivery, revocation, deletion, IP allocation, and startup reconciliation.\n- Separate one-page Telegram section \"Прокси\" for SOCKS5/Dante auto-issue and Telegram MTProto Proxy links.\n- MTProto supports `static` compatibility mode and `managed` mode with per-user secrets, safe apply, and rollback.\n- Optional legacy proxy entry table seeded from `DEFAULT_PROXY_*` remains internal/compatibility storage; the user-facing proxy UX uses `proxy_accesses`.\n- Ownership checks so users can view their own configs/stats; destructive VPN and proxy lifecycle actions are admin-only.\n- Audit log with recursive masking for sensitive values.\n- SQLite storage with migrations from `db/schema.sql`.\n- Rotating local logs in `LOG_DIR`.\n- systemd deployment using `deploy/vpn-bot.service`.\n- Intended target: Ubuntu VDS with existing Xray and/or AmneziaWG installation.\n\n## Stack\n\n- Python 3.12+\n- aiogram 3\n- SQLite via aiosqlite\n- python-dotenv\n- systemd\n- Xray VLESS Reality\n- AmneziaWG / WireGuard-compatible tooling\n- Ubuntu / Linux VDS\n\n## Repository Layout\n\n```text\nmain.py                    # Bot entry point\ninit_db.py                 # SQLite schema bootstrap/migration entry point\nrequirements.txt           # Runtime dependencies\nconstraints.txt            # Pinned production dependency constraints\n.env.example               # Environment variable template\ndb/schema.sql              # Database schema\ndeploy/vpn-bot.service     # vpn-bot systemd unit template\ndeploy/run-mtproxy-managed # MTProxy managed-mode wrapper installed during deploy\ndeploy/mtproxy-vpnbot-managed.conf # MTProxy drop-in installed during deploy\nbot/                       # Telegram handlers, keyboards, FSM, formatting\nservices/                  # Business workflows and permissions\nrepositories/              # SQLite access layer\nadapters/                  # Xray, AWG, systemctl, backups, shell adapters\nconfig/settings.py         # Environment parsing and validation\ntests/                     # Regression and hardening tests\n```\n\n## Security Warning\n\nThis project handles operational VPN and Telegram secrets. Never commit or publish:\n\n- `.env` files.\n- Telegram bot tokens.\n- Private keys or preshared keys.\n- Real Xray Reality server/client configuration.\n- Real AmneziaWG server/client configuration.\n- Full VPN client configs.\n- SQLite databases or database dumps.\n- Server IP addresses combined with credentials.\n- SSH, panel, hosting, or other server credentials.\n- Recommended BotFather setting: disable adding this bot to groups. The bot is designed to work in private chats only; group chats may expose user data, admin actions, or sensitive operational messages.\n\nUse `.env.example` only as a template. Keep production configuration on the server and outside Git history.\n\n## Environment Variables\n\nCopy `.env.example` to `.env` and replace placeholders with values for your server. `BOT_TOKEN` and `ADMIN_IDS` are required for startup. Fill the relevant Xray or AWG values before issuing that key type.\n\n```dotenv\nBOT_TOKEN=\u003ctelegram_bot_token\u003e\nADMIN_IDS=\u003ctelegram_user_id\u003e,\u003ctelegram_user_id\u003e\n\nDB_PATH=/opt/vpn-service/data/vpn.db\nSQLITE_SYNCHRONOUS=FULL\nLOG_DIR=/opt/vpn-service/logs\nBOT_LOCK_PATH=/run/vpn-bot/vpn-bot.lock\n\n# Root+api mode (default): PRIVILEGE_HELPERS_ENABLED=false or omit. Non-root helper mode: set true with helper paths below.\nPRIVILEGE_HELPERS_ENABLED=false\nHELPER_STAGING_ROOT=/run/vpn-bot\nSOCKS5_USER_HELPER_PATH=/usr/local/sbin/vpnbot-socks5-user\nXRAY_APPLY_HELPER_PATH=/usr/local/sbin/vpnbot-xray-apply\nAWG_APPLY_HELPER_PATH=/usr/local/sbin/vpnbot-awg-apply\nMTPROTO_APPLY_HELPER_PATH=/usr/local/sbin/vpnbot-mtproxy-apply\n\nXRAY_CONFIG_PATH=/usr/local/etc/xray/config.json\nXRAY_SERVICE_NAME=xray\nXRAY_APPLY_MODE=api\nXRAY_INBOUND_TAG=vless-in\nXRAY_PUBLIC_HOST=\u003cvpn_public_host\u003e\nXRAY_PUBLIC_PORT=443\nXRAY_REALITY_PUBLIC_KEY=\u003cxray_reality_public_key\u003e\nXRAY_SNI=\u003cxray_reality_sni\u003e\nXRAY_FLOW=xtls-rprx-vision\nXRAY_FINGERPRINT=chrome\nXRAY_NETWORK_TYPE=tcp\nXRAY_SHORT_ID=\u003cxray_short_id\u003e\nXRAY_MANAGE_SHORT_IDS=false\nXRAY_ALLOW_RESTART_ON_ROLLBACK=false\nXRAY_STATS_SERVER=127.0.0.1:10085\n\nAWG_CONFIG_PATH=/etc/amnezia/amneziawg/awg0.conf\nAWG_INTERFACE=awg0\nAWG_NETWORK=10.0.0.0/24\nAWG_SERVER_ADDRESS=10.0.0.1\nAWG_ENDPOINT_HOST=\u003cawg_endpoint_host\u003e\nAWG_ENDPOINT_PORT=\u003cawg_endpoint_port\u003e\nAWG_SERVER_PUBLIC_KEY=\u003cawg_server_public_key\u003e\nAWG_DNS=1.1.1.1\nAWG_MTU=\nAWG_ALLOWED_IPS=0.0.0.0/0, ::/0\nAWG_PERSISTENT_KEEPALIVE=25\nAWG_USE_PRESHARED_KEY=true\n\nDEFAULT_PROXY_TYPE=\nDEFAULT_PROXY_HOST=\nDEFAULT_PROXY_PORT=\nDEFAULT_PROXY_LOGIN=\nDEFAULT_PROXY_PASSWORD=\nDEFAULT_PROXY_NOTE=\n\nSOCKS5_ENABLED=false\nSOCKS5_HOST=\nSOCKS5_PORT=31337\nSOCKS5_LOGIN_PREFIX=vpn_socks_\nSOCKS5_SYSTEM_USER_SHELL=/usr/sbin/nologin\nSOCKS5_SERVICE_NAME=danted\nSOCKS5_PUBLIC_NAME=SOCKS5 Proxy\nSOCKS5_NOTE=SOCKS5 Dante proxy on VDS\n\nMTPROTO_ENABLED=false\nMTPROTO_MODE=static\nMTPROTO_HOST=\nMTPROTO_PORT=8443\nMTPROTO_SECRET=\nMTPROTO_PUBLIC_NAME=Telegram MTProto Proxy\nMTPROTO_NOTE=MTProto proxy for Telegram\n\n# Managed MTProto per-user secrets mode\nMTPROTO_SERVICE_NAME=mtproxy\nMTPROTO_BINARY_PATH=/usr/local/bin/mtproto-proxy\nMTPROTO_RUN_USER=mtproxy\nMTPROTO_RUN_GROUP=mtproxy\nMTPROTO_CONFIG_DIR=/etc/mtproxy\nMTPROTO_PROXY_SECRET_PATH=/etc/mtproxy/proxy-secret\nMTPROTO_PROXY_MULTI_CONF_PATH=/etc/mtproxy/proxy-multi.conf\nMTPROTO_MANAGED_DIR=/etc/mtproxy/vpnbot\nMTPROTO_MANAGED_SECRETS_PATH=/etc/mtproxy/vpnbot/managed-secrets.json\nMTPROTO_MANAGED_ENV_PATH=/etc/mtproxy/vpnbot/mtproxy.env\nMTPROTO_MANAGED_WRAPPER_PATH=/opt/vpn-service/scripts/run-mtproxy-managed\nMTPROTO_BACKUP_DIR=/etc/mtproxy/vpnbot/backups\nMTPROTO_INTERNAL_STATS_PORT=8888\nMTPROTO_WORKERS=1\nMTPROTO_APPLY_TIMEOUT_SECONDS=10\nMTPROTO_ROLLBACK_ON_APPLY_FAILURE=true\nMTPROTO_KEEP_LAST_BACKUPS=10\nMTPROTO_STATS_URL=\n\nAUDIT_RETENTION_DAYS=180\nCONFIG_BACKUP_KEEP_LAST=20\n```\n\n### Complete Environment Variable Reference\n\nAll variables parsed by `config/settings.py`. Variables marked **Required** must be set before startup; variables not marked are optional with the shown default.\n\n\u003e ⚠️ **Security-sensitive variables** are marked with 🔒. Never commit them; keep them on the server in `.env` (mode `0600`, root-only).\n\n#### Core\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `BOT_TOKEN` | **Yes** | — | Telegram Bot API token from BotFather. 🔒 | `123456:ABC-DEF...` |\n| `ADMIN_IDS` | **Yes** | — | Comma-separated list of Telegram user IDs with full admin access. | `123456,789012` |\n| `DB_PATH` | No | `/opt/vpn-service/data/vpn.db` | Path to the SQLite database file. | `/opt/vpn-service/data/vpn.db` |\n| `SQLITE_SYNCHRONOUS` | No | `FULL` | SQLite synchronous mode: `FULL`, `NORMAL`, or `EXTRA`. `FULL` is safest. | `FULL` |\n| `LOG_DIR` | No | `/opt/vpn-service/logs` | Directory for rotating log files. | `/opt/vpn-service/logs` |\n| `BOT_LOCK_PATH` | No | `/run/vpn-bot.lock` | Path to the single-instance PID lock file. | `/run/vpn-bot/vpn-bot.lock` |\n| `BOT_DROP_PENDING_UPDATES` | No | `false` | Drop queued Telegram updates on startup. Useful after downtime. | `false` |\n| `BOT_LANGUAGE` | No | `ru` | Bot UI language. Supported: `ru`, `en`. | `ru` |\n| `AUDIT_RETENTION_DAYS` | No | `180` | Days to retain audit log entries (0 = forever, max 3650). | `180` |\n| `CONFIG_BACKUP_KEEP_LAST` | No | `20` | Number of config backups to keep per backend (1–500). | `20` |\n\n#### Health Endpoint\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `HEALTH_HOST` | No | `127.0.0.1` | Host for the optional HTTP health endpoint. | `127.0.0.1` |\n| `HEALTH_PORT` | No | _(disabled)_ | Port for the HTTP health endpoint. Omit to disable. | `8080` |\n\n#### Privilege Helpers (non-root deployment)\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `PRIVILEGE_HELPERS_ENABLED` | No | `false` | Enable non-root deployment via sudo helpers. Incompatible with `XRAY_APPLY_MODE=api`. | `false` |\n| `HELPER_STAGING_ROOT` | No | `/run/vpn-bot` | Root directory for staging files passed to sudo helpers. | `/run/vpn-bot` |\n| `SOCKS5_USER_HELPER_PATH` | No | `/usr/local/sbin/vpnbot-socks5-user` | Absolute path to the SOCKS5 user management sudo helper. | `/usr/local/sbin/vpnbot-socks5-user` |\n| `XRAY_APPLY_HELPER_PATH` | No | `/usr/local/sbin/vpnbot-xray-apply` | Absolute path to the Xray config apply sudo helper. | `/usr/local/sbin/vpnbot-xray-apply` |\n| `AWG_APPLY_HELPER_PATH` | No | `/usr/local/sbin/vpnbot-awg-apply` | Absolute path to the AWG config apply sudo helper. | `/usr/local/sbin/vpnbot-awg-apply` |\n| `MTPROTO_APPLY_HELPER_PATH` | No | `/usr/local/sbin/vpnbot-mtproxy-apply` | Absolute path to the MTProto apply sudo helper. | `/usr/local/sbin/vpnbot-mtproxy-apply` |\n| `XRAY_HELPER_STAGING_DIR` | No | `$HELPER_STAGING_ROOT/xray` | Staging directory for Xray helper files. | `/run/vpn-bot/xray` |\n| `AWG_HELPER_STAGING_DIR` | No | `$HELPER_STAGING_ROOT/awg` | Staging directory for AWG helper files. | `/run/vpn-bot/awg` |\n| `MTPROTO_HELPER_STAGING_DIR` | No | `$HELPER_STAGING_ROOT/mtproxy` | Staging directory for MTProto helper files. | `/run/vpn-bot/mtproxy` |\n\n#### Xray VLESS Reality\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `XRAY_CONFIG_PATH` | No | `/usr/local/etc/xray/config.json` | Path to the Xray config file. | `/usr/local/etc/xray/config.json` |\n| `XRAY_SERVICE_NAME` | No | `xray` | systemd service name for Xray. | `xray` |\n| `XRAY_APPLY_MODE` | No | `restart` | How to apply Xray config changes: `restart`, `reload`, or `api`. `api` requires root and is incompatible with helpers. | `api` |\n| `XRAY_INBOUND_TAG` | No* | _(first inbound)_ | Tag of the VLESS inbound in `config.json`. Required for `api` mode. | `vless-in` |\n| `XRAY_PUBLIC_HOST` | No* | — | Public hostname/IP clients use to connect. Required to issue keys. | `vpn.example.com` |\n| `XRAY_PUBLIC_PORT` | No | `443` | Public TCP port for VLESS connections. | `443` |\n| `XRAY_REALITY_PUBLIC_KEY` | No* | — | Xray Reality public key (base64url). Required to issue keys. | `ABC123...` |\n| `XRAY_SNI` | No* | — | SNI (Server Name Indication) for Reality. Required to issue keys. | `www.microsoft.com` |\n| `XRAY_FLOW` | No | `xtls-rprx-vision` | VLESS flow control. | `xtls-rprx-vision` |\n| `XRAY_FINGERPRINT` | No | `chrome` | TLS fingerprint. One of: `chrome`, `firefox`, `safari`, `ios`, `android`, `edge`, `randomized`, `randomizedalpn`, `randomizednoalpn`. | `chrome` |\n| `XRAY_NETWORK_TYPE` | No | `tcp` | Network type: `tcp` or `raw`. | `tcp` |\n| `XRAY_SHORT_ID` | No* | — | Hex short ID (≤16 chars). Required if `XRAY_MANAGE_SHORT_IDS=false`. | `abcd1234` |\n| `XRAY_MANAGE_SHORT_IDS` | No | `false` | Let the bot manage short IDs automatically. | `false` |\n| `XRAY_ALLOW_RESTART_ON_ROLLBACK` | No | `false` | Allow service restart during config rollback. | `false` |\n| `XRAY_STATS_SERVER` | No* | — | Address of the Xray gRPC stats/API server. Required for `api` mode. | `127.0.0.1:10085` |\n| `XRAY_ACCESS_LOG_PATH` | No | _(empty)_ | Path to the Xray access log for anomaly detection. Leave empty to disable. | `/var/log/xray/access.log` |\n\n_Legacy aliases accepted: `XRAY_SERVER_ADDRESS` (= `XRAY_PUBLIC_HOST`), `XRAY_SERVER_PORT` (= `XRAY_PUBLIC_PORT`), `XRAY_PUBLIC_KEY` (= `XRAY_REALITY_PUBLIC_KEY`), `XRAY_SERVER_NAME` (= `XRAY_SNI`)._\n\n#### AmneziaWG\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `AWG_CONFIG_PATH` | No | `/etc/amnezia/amneziawg/awg0.conf` | Path to the AWG server config file. | `/etc/amnezia/amneziawg/awg0.conf` |\n| `AWG_INTERFACE` | No | `awg0` | AWG/WireGuard network interface name. | `awg0` |\n| `AWG_NETWORK` | No | `10.0.0.0/24` | IPv4 subnet for the VPN. | `10.0.0.0/24` |\n| `AWG_SERVER_ADDRESS` | No | `10.0.0.1` | Server's IPv4 address inside the VPN subnet. | `10.0.0.1` |\n| `AWG_ENDPOINT_HOST` | No* | — | Public hostname/IP for AWG endpoint. Required to issue keys. | `vpn.example.com` |\n| `AWG_ENDPOINT_PORT` | No | `0` | Public UDP port for AWG endpoint. | `51820` |\n| `AWG_SERVER_PUBLIC_KEY` | No | _(empty)_ | AWG server public key (base64). Shown in client configs. | `ABC123...` |\n| `AWG_DNS` | No | `1.1.1.1` | DNS server for AWG clients. | `1.1.1.1` |\n| `AWG_MTU` | No | _(auto)_ | MTU for AWG client interface (576–1500). Omit to let client decide. | `1280` |\n| `AWG_ALLOWED_IPS` | No | `0.0.0.0/0, ::/0` | Allowed IPs for AWG client routing (full-tunnel by default). | `0.0.0.0/0, ::/0` |\n| `AWG_PERSISTENT_KEEPALIVE` | No | `25` | Keepalive interval in seconds (0–86400). | `25` |\n| `AWG_USE_PRESHARED_KEY` | No | `true` | Generate and include a preshared key per client. | `true` |\n| `AWG_STATS_INTERVAL` | No | `60` | Background traffic stats sampling interval in seconds (0–3600). | `60` |\n\n_Legacy alias: `AWG_CLIENT_DNS` (= `AWG_DNS`)._\n\n#### SOCKS5 / Dante\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `SOCKS5_ENABLED` | No | `false` | Enable SOCKS5 proxy backend. | `false` |\n| `SOCKS5_HOST` | No* | _(empty)_ | Public host for SOCKS5 (required if `SOCKS5_ENABLED=true`). | `vpn.example.com` |\n| `SOCKS5_PORT` | No | `31337` | Public port for SOCKS5 connections. | `31337` |\n| `SOCKS5_LOGIN_PREFIX` | No | `vpn_socks_` | Prefix for all managed Linux users. Must be unique and non-generic. | `vpn_socks_` |\n| `SOCKS5_SYSTEM_USER_SHELL` | No | `/usr/sbin/nologin` | Shell for managed SOCKS5 Linux users. | `/usr/sbin/nologin` |\n| `SOCKS5_SERVICE_NAME` | No | `danted` | systemd service name for Dante. | `danted` |\n| `SOCKS5_PUBLIC_NAME` | No | `SOCKS5 Proxy` | Display name shown in the bot UI. | `SOCKS5 Proxy` |\n| `SOCKS5_NOTE` | No | `SOCKS5 Dante proxy on VDS` | Description shown in proxy access cards. | `SOCKS5 Dante proxy on VDS` |\n\n#### MTProto Proxy\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `MTPROTO_ENABLED` | No | `false` | Enable MTProto proxy backend. | `false` |\n| `MTPROTO_MODE` | No | `static` | Proxy mode: `static` (shared secret) or `managed` (per-user secrets). | `static` |\n| `MTPROTO_HOST` | No* | _(empty)_ | Public host for MTProto (required if `MTPROTO_ENABLED=true`). | `vpn.example.com` |\n| `MTPROTO_PORT` | No | `8443` | Public port for MTProto connections. | `8443` |\n| `MTPROTO_SECRET` | No* | _(empty)_ | 🔒 Shared MTProto secret (required if `MTPROTO_MODE=static` and enabled). | _(hex string)_ |\n| `MTPROTO_PUBLIC_NAME` | No | `Telegram MTProto Proxy` | Display name shown in the bot UI. | `Telegram MTProto Proxy` |\n| `MTPROTO_NOTE` | No | `MTProto proxy for Telegram` | Description shown in proxy access cards. | `MTProto proxy for Telegram` |\n| `MTPROTO_STATS_URL` | No | _(empty)_ | URL for MTProto statistics endpoint. | `http://127.0.0.1:8888/stats` |\n| `MTPROTO_SERVICE_NAME` | No | `mtproxy` | systemd service name for MTProxy. | `mtproxy` |\n| `MTPROTO_BINARY_PATH` | No | `/usr/local/bin/mtproto-proxy` | Path to the MTProto proxy binary. | `/usr/local/bin/mtproto-proxy` |\n| `MTPROTO_RUN_USER` | No | `mtproxy` | User to run the MTProto proxy process as. | `mtproxy` |\n| `MTPROTO_RUN_GROUP` | No | `mtproxy` | Group to run the MTProto proxy process as. | `mtproxy` |\n| `MTPROTO_CONFIG_DIR` | No | `/etc/mtproxy` | Directory containing MTProxy base config files. | `/etc/mtproxy` |\n| `MTPROTO_PROXY_SECRET_PATH` | No | `/etc/mtproxy/proxy-secret` | Path to the MTProxy `proxy-secret` file. | `/etc/mtproxy/proxy-secret` |\n| `MTPROTO_PROXY_MULTI_CONF_PATH` | No | `/etc/mtproxy/proxy-multi.conf` | Path to the MTProxy `proxy-multi.conf` file. | `/etc/mtproxy/proxy-multi.conf` |\n| `MTPROTO_MANAGED_DIR` | No | `/etc/mtproxy/vpnbot` | Directory for bot-managed MTProto files. | `/etc/mtproxy/vpnbot` |\n| `MTPROTO_MANAGED_SECRETS_PATH` | No | `$MTPROTO_MANAGED_DIR/managed-secrets.json` | 🔒 Path to managed secrets JSON. | `/etc/mtproxy/vpnbot/managed-secrets.json` |\n| `MTPROTO_MANAGED_ENV_PATH` | No | `$MTPROTO_MANAGED_DIR/mtproxy.env` | Path to managed MTProxy env file. | `/etc/mtproxy/vpnbot/mtproxy.env` |\n| `MTPROTO_MANAGED_WRAPPER_PATH` | No | `/opt/vpn-service/scripts/run-mtproxy-managed` | Path to the managed-mode wrapper script. | `/opt/vpn-service/scripts/run-mtproxy-managed` |\n| `MTPROTO_BACKUP_DIR` | No | `$MTPROTO_MANAGED_DIR/backups` | Directory for MTProto managed-file backups. | `/etc/mtproxy/vpnbot/backups` |\n| `MTPROTO_INTERNAL_STATS_PORT` | No | `8888` | Internal MTProxy stats port (1–65535). | `8888` |\n| `MTPROTO_WORKERS` | No | `1` | Number of MTProxy worker processes (1–1024). | `1` |\n| `MTPROTO_APPLY_TIMEOUT_SECONDS` | No | `10` | Timeout in seconds for apply + health check (1–3600). | `10` |\n| `MTPROTO_ROLLBACK_ON_APPLY_FAILURE` | No | `true` | Automatically restore backup on apply failure. | `true` |\n| `MTPROTO_KEEP_LAST_BACKUPS` | No | `10` | Number of managed-file backups to retain (0–1000). | `10` |\n\n#### Key Expiry and Trial Access\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `KEY_EXPIRY_CHECK_INTERVAL` | No | `1800` | How often (seconds) to check for expiring/expired keys (0–86400). | `1800` |\n| `KEY_EXPIRY_NOTIFY_DAYS` | No | _(empty)_ | Comma-separated list of days before expiry to send user notifications. | `7,3,1` |\n| `KEY_MAX_TRIAL_DAYS` | No | `365` | Maximum duration (days) for trial VPN keys (1–3650). | `30` |\n\n#### Off-site Encrypted Backup\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `OFFSITE_BACKUP_ENCRYPTION_KEY` | No | _(disabled)_ | 🔒 Fernet key for encrypting off-site DB backups. Generate with: `python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"`. Leave empty to disable off-site backups. | _(44-char base64url)_ |\n| `OFFSITE_BACKUP_INTERVAL` | No | `604800` | Interval (seconds) between off-site backup uploads (0 = disabled). Default is 7 days. | `604800` |\n\n#### Anomaly Detection\n\n| Variable | Required | Default | Description | Example |\n|---|---|---|---|---|\n| `ANOMALY_CHECK_INTERVAL` | No | `300` | How often (seconds) to run the anomaly detection scan (0–86400). | `300` |\n| `ANOMALY_WINDOW_SECONDS` | No | `3600` | Traffic observation window in seconds (60–86400). | `3600` |\n| `ANOMALY_MIN_UNIQUE_IPS` | No | `3` | Minimum unique source IPs within the window to flag a key (1–1000). | `3` |\n| `ANOMALY_AUTO_REVOKE` | No | `false` | Automatically revoke flagged keys without admin confirmation. | `false` |\n| `ANOMALY_COOLDOWN_SECONDS` | No | `7200` | Cooldown before re-flagging the same key (0–86400). | `7200` |\n| `ANOMALY_CONCURRENT_WINDOW_SECONDS` | No | `600` | Window for concurrent-connection anomaly detection (0–86400). | `600` |\n\n#### Legacy / Compatibility\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `DEFAULT_PROXY_TYPE` | No | _(empty)_ | Legacy proxy entry type (internal use only; does not drive user-facing proxy flow). |\n| `DEFAULT_PROXY_HOST` | No | _(empty)_ | Legacy proxy host. |\n| `DEFAULT_PROXY_PORT` | No | _(empty)_ | Legacy proxy port. |\n| `DEFAULT_PROXY_LOGIN` | No | _(empty)_ | Legacy proxy login. |\n| `DEFAULT_PROXY_PASSWORD` | No | _(empty)_ | 🔒 Legacy proxy password. |\n| `DEFAULT_PROXY_NOTE` | No | _(empty)_ | Legacy proxy note. |\n\nNotes:\n\n- If `XRAY_INBOUND_TAG` is empty, the adapter uses the first inbound with `settings.clients`.\n- If `XRAY_MANAGE_SHORT_IDS=false`, `XRAY_SHORT_ID` must be set.\n- `XRAY_APPLY_MODE=restart` is the default apply mode; use `reload` only when your Xray unit reliably applies reload.\n\n\u003e ⚠️ **IMPORTANT — XRAY_APPLY_MODE=api and root deployment:**\n\u003e - `XRAY_APPLY_MODE=api` is the **only** mode that adds/removes Xray keys without restarting the Xray service. Without it, every key creation or deletion causes a full Xray restart, which drops all active connections.\n\u003e - `XRAY_APPLY_MODE=api` is **INCOMPATIBLE** with `PRIVILEGE_HELPERS_ENABLED=true` — the bot will refuse to start if both are set simultaneously.\n\u003e - To use api mode, the bot **MUST run as root** (`User=root` in the service file) with `PRIVILEGE_HELPERS_ENABLED=false`.\n\u003e - `deploy/vpn-bot.service` in the repo is the **authoritative source** — every deploy overwrites `/etc/systemd/system/vpn-bot.service` from it. Manual edits to the system service file are lost on the next deploy. The repo file must reflect the intended production configuration.\n\u003e - See [Xray API Mode](#xray-api-mode) for required env vars and one-time server setup.\n\n- `XRAY_APPLY_MODE=api` is incompatible with `PRIVILEGE_HELPERS_ENABLED=true`. When privilege helpers are enabled the bot applies Xray config changes through the `vpnbot-xray-apply` sudo helper, which always calls `systemctl restart xray` regardless of `XRAY_APPLY_MODE`. Use `restart` mode with privilege helpers; `reload` and `api` modes are not honoured by the helper.\n- `SQLITE_SYNCHRONOUS=FULL` is the safer default for this control-plane database. `NORMAL` is faster but can lose the last committed transactions on OS or power failure while VPN backend state has already changed.\n- `AWG_CLIENT_DNS` is supported only as a legacy alias; use `AWG_DNS` for new deployments.\n- `AWG_ENDPOINT_HOST` and `AWG_ENDPOINT_PORT` should point to the public AWG endpoint clients will use.\n- `SOCKS5_ENABLED=true` requires `SOCKS5_HOST`, `SOCKS5_PORT`, and a safe `SOCKS5_LOGIN_PREFIX`. Dante must already be installed and listening; the bot only creates/locks/deletes managed Linux users with that prefix.\n- `MTPROTO_ENABLED=true` requires `MTPROTO_HOST`. `MTPROTO_MODE=static` also requires `MTPROTO_SECRET`.\n- `MTPROTO_MODE=static` is compatibility mode: the bot shows a shared MTProto secret and can only deactivate a user's SQLite record. True per-user server-side revoke is impossible in static mode without rotating the shared secret.\n- `MTPROTO_MODE=managed` creates one unique secret per user. In production helper mode the bot stages managed files under `/run/vpn-bot/mtproxy`; `/usr/local/sbin/vpnbot-mtproxy-apply` writes `/etc/mtproxy/vpnbot`, restarts `mtproxy`, verifies service/port health, and rolls back managed files if apply fails. The systemd drop-in and wrapper are installed during deploy, not written by the bot at runtime.\n- `MTPROTO_SECRET`, SOCKS5 passwords, and real production endpoints with credentials must never be committed. `.env.example` intentionally keeps proxy secrets empty.\n- `DEFAULT_PROXY_*` is legacy compatibility storage and does not drive the new user-facing proxy access flow.\n- **Root deployment with api mode** (current `deploy/vpn-bot.service` default): `User=root`, `PRIVILEGE_HELPERS_ENABLED=false`, `XRAY_APPLY_MODE=api`. The bot writes Xray config and applies changes directly via the Xray gRPC API; no sudo helpers are needed. See [Xray API Mode](#xray-api-mode).\n- **Alternative non-root deployment with privilege helpers**: Run the bot as `vpn-bot:vpn-bot` with `PRIVILEGE_HELPERS_ENABLED=true`. Root-only backend changes go through fixed sudo helpers documented in `deploy/helpers/README.md`. Use `XRAY_APPLY_MODE=restart` or `reload` in this model; api mode is not honoured when helpers are enabled.\n- Keep project code, deploy files, `.env`, and `.venv` not writable by the service account. In root mode all paths are accessible; in non-root mode only `/opt/vpn-service/data`, `/opt/vpn-service/logs` if file logs are enabled, and `/run/vpn-bot` should be writable by `vpn-bot`.\n\n## Xray API Mode\n\n\u003e ⚠️ **IMPORTANT — `XRAY_APPLY_MODE=api` requires root and is incompatible with privilege helpers:**\n\u003e - `XRAY_APPLY_MODE=api` is the **only** mode that adds/removes Xray keys without restarting the Xray service. Without it, every key creation or deletion causes a full Xray restart, which drops all active connections.\n\u003e - `XRAY_APPLY_MODE=api` is **INCOMPATIBLE** with `PRIVILEGE_HELPERS_ENABLED=true` — the bot will refuse to start if both are set simultaneously.\n\u003e - To use api mode, the bot **MUST run as root** (`User=root` in the service file) with `PRIVILEGE_HELPERS_ENABLED=false`.\n\u003e - `deploy/vpn-bot.service` in the repo is the **authoritative source** — every deploy overwrites `/etc/systemd/system/vpn-bot.service` from it. Manual edits to the system service file are lost on the next deploy. The repo file must reflect the intended production configuration.\n\n### Required .env variables for api mode\n\n```dotenv\nXRAY_APPLY_MODE=api\nXRAY_INBOUND_TAG=vless-in          # must match the \"tag\" field on the VLESS inbound in config.json\nXRAY_STATS_SERVER=127.0.0.1:10085  # must match the dokodemo-door api inbound port\n```\n\nAlso set `PRIVILEGE_HELPERS_ENABLED=false` (or omit it) when using api mode.\n\n### One-time server preparation\n\nBefore starting the bot in api mode, configure the Xray API inbound and tag the VLESS inbound in `/usr/local/etc/xray/config.json`:\n\n1. Add `\"tag\": \"vless-in\"` to your VLESS inbound object (use whatever tag you set as `XRAY_INBOUND_TAG`):\n\n```json\n{\n  \"inbounds\": [\n    {\n      \"tag\": \"vless-in\",\n      \"port\": 443,\n      \"protocol\": \"vless\",\n      \"...\": \"...\"\n    }\n  ]\n}\n```\n\n2. Ensure the Xray API block and a `dokodemo-door` API inbound are present in `config.json`. The port must match `XRAY_STATS_SERVER`:\n\n```json\n{\n  \"api\": {\n    \"tag\": \"api\",\n    \"services\": [\"HandlerService\", \"StatsService\", \"LoggerService\"]\n  },\n  \"inbounds\": [\n    {\n      \"tag\": \"api-in\",\n      \"listen\": \"127.0.0.1\",\n      \"port\": 10085,\n      \"protocol\": \"dokodemo-door\",\n      \"settings\": { \"address\": \"127.0.0.1\" }\n    }\n  ],\n  \"routing\": {\n    \"rules\": [\n      { \"inboundTag\": [\"api-in\"], \"outboundTag\": \"api\", \"type\": \"field\" }\n    ]\n  }\n}\n```\n\n3. Restart Xray once so the tag takes effect and verify the config:\n\n```bash\nsudo xray run -test -config /usr/local/etc/xray/config.json\nsudo systemctl restart xray\nsudo systemctl status xray --no-pager\n```\n\n4. Install the service file and start the bot:\n\n```bash\nsudo cp deploy/vpn-bot.service /etc/systemd/system/vpn-bot.service\nsudo systemctl daemon-reload\nsudo systemctl enable --now vpn-bot\nsudo systemctl status vpn-bot\n```\n\n`deploy/vpn-bot.service` already contains `User=root`, `ProtectSystem=false`, and no `ReadWritePaths` restrictions — no manual edits to the service file are needed.\n\n## Access Lifecycle Policy\n\n- Approved users may create their own Xray/AWG keys, view their own active configs, view stats, and edit their own key notes.\n- Approved users may issue and view their own SOCKS5/MTProto proxy access when the backend is enabled.\n- Revoke/delete for Xray and AWG keys is admin-only. Normal users do not get revoke/delete buttons, and direct callbacks/service calls are rejected.\n- Revoke/delete for SOCKS5 and MTProto proxy access is admin-only. The user-facing proxy page only issues/shows active access and stats.\n- Blocking a user is an admin action. It blocks bot access and attempts to revoke active/problem VPN keys and SOCKS5/MTProto proxy access.\n- In `MTPROTO_MODE=static`, blocking/revoking only deactivates the bot/SQLite record; a copied shared secret keeps working until the shared secret is rotated.\n- In `MTPROTO_MODE=managed`, admin revoke removes that user's MTProto secret from the managed active list while other users remain active.\n\n## Backend Degraded Mode\n\nThe bot marks a backend DEGRADED when reconciliation or post-apply compensation cannot prove that SQLite and the server runtime are safe to mutate automatically. DEGRADED is backend-specific:\n\n- Xray DEGRADED blocks Xray create/revoke/delete/manual reconcile only.\n- AWG DEGRADED blocks AWG create/revoke/delete/manual reconcile only.\n- SOCKS5 DEGRADED blocks SOCKS5 issue/revoke/delete only.\n- MTProto DEGRADED blocks MTProto issue/revoke/delete only.\n- Other backends continue working unless they are also DEGRADED.\n\nThe admin panel has `Диагностика backend`, which shows `OK` or `DEGRADED` for Xray, AWG, SOCKS5, and MTProto plus a non-secret reason. For full context, check `journalctl -u vpn-bot`, audit rows, SQLite lifecycle statuses, and the backend config/runtime listed in the runbooks below. Recover by fixing the server state from backups or manual inspection, then restart `vpn-bot` so startup reconciliation can re-check the backend.\n\n## Proxy Deployment Notes\n\nThe bot does not install Dante or MTProxy. Prepare them on the VDS first, then enable the relevant env flags.\n\nSOCKS5/Dante expectations:\n\n- Dante listens on the configured public host/port, for example `0.0.0.0:31337`.\n- Authentication is Linux username/password.\n- The bot process does not call account-management tools directly in production. It uses `sudo -n /usr/local/sbin/vpnbot-socks5-user ...`; the helper is the only code allowed to call `getent`, `useradd`, `chpasswd`, `passwd -l`, and `userdel`.\n- The bot refuses to manage Linux users whose login does not start with `SOCKS5_LOGIN_PREFIX`.\n\nMTProto static mode:\n\n- Set `MTPROTO_MODE=static` and provide `MTPROTO_SECRET`.\n- MTProxy is managed outside the bot by its own systemd unit.\n- The bot does not edit MTProxy files in static mode.\n- User output always includes both Telegram links: plain secret first, then the `dd` random-padding variant.\n- Static mode uses a shared secret; blocking one user only deactivates the bot record and does not revoke that user server-side.\n\nMTProto managed mode:\n\n- Set `MTPROTO_MODE=managed`; do not set a shared production secret in `MTPROTO_SECRET` for new users.\n- MTProxy must already be installed and have valid `proxy-secret` and `proxy-multi.conf` files.\n- Install the managed wrapper/drop-in once during deploy. The default model is root-wrapper: wrapper запускается от root; systemd starts the wrapper as root, the wrapper reads root-only managed env/secrets, and the wrapper starts `mtproto-proxy` with `-u mtproxy` from `MTPROTO_RUN_USER` so the proxy process drops privileges internally.\n  ```bash\n  sudo install -m 700 -d /opt/vpn-service/scripts\n  sudo install -m 700 deploy/run-mtproxy-managed /opt/vpn-service/scripts/run-mtproxy-managed\n  sudo install -m 700 -d /etc/systemd/system/mtproxy.service.d\n  sudo install -m 600 deploy/mtproxy-vpnbot-managed.conf /etc/systemd/system/mtproxy.service.d/vpnbot-managed.conf\n  sudo install -m 700 -d /etc/mtproxy/vpnbot /etc/mtproxy/vpnbot/backups\n  sudo chown root:root /opt/vpn-service/scripts/run-mtproxy-managed /etc/mtproxy/vpnbot /etc/mtproxy/vpnbot/backups\n  sudo /opt/vpn-service/.venv/bin/python - \u003c\u003c'PY'\n  import json, secrets\n  from pathlib import Path\n  managed = Path(\"/etc/mtproxy/vpnbot\")\n  placeholder = secrets.token_hex(16)\n  (managed / \"managed-secrets.json\").write_text(json.dumps({\n      \"version\": 1,\n      \"generation\": 0,\n      \"managed_by\": \"vpn-bot\",\n      \"secrets\": [],\n      \"runtime_secrets\": [{\"secret\": placeholder, \"fingerprint\": \"empty-placeholder\", \"purpose\": \"empty-placeholder\"}],\n  }, indent=2, sort_keys=True) + \"\\n\", encoding=\"utf-8\")\n  (managed / \"mtproxy.env\").write_text(\n      \"MTPROTO_BINARY_PATH=/usr/local/bin/mtproto-proxy\\n\"\n      \"MTPROTO_RUN_USER=mtproxy\\n\"\n      \"MTPROTO_RUN_GROUP=mtproxy\\n\"\n      \"MTPROTO_PROXY_SECRET_PATH=/etc/mtproxy/proxy-secret\\n\"\n      \"MTPROTO_PROXY_MULTI_CONF_PATH=/etc/mtproxy/proxy-multi.conf\\n\"\n      \"MTPROTO_MANAGED_SECRETS_PATH=/etc/mtproxy/vpnbot/managed-secrets.json\\n\"\n      \"MTPROTO_PORT=8443\\n\"\n      \"MTPROTO_INTERNAL_STATS_PORT=8888\\n\"\n      \"MTPROTO_WORKERS=1\\n\",\n      encoding=\"utf-8\",\n  )\n  PY\n  sudo chmod 600 /etc/mtproxy/vpnbot/managed-secrets.json /etc/mtproxy/vpnbot/mtproxy.env\n  sudo chown root:root /etc/mtproxy/vpnbot/managed-secrets.json /etc/mtproxy/vpnbot/mtproxy.env\n  sudo systemctl daemon-reload\n  sudo systemctl restart mtproxy\n  sudo systemctl status mtproxy --no-pager\n  sudo ss -tlnp | grep 8443\n  ```\n- The drop-in clears any existing `User=`/`Group=` from `mtproxy.service`; `systemctl show mtproxy -p User -p Group -p ExecStart` should show empty `User`/`Group` and `ExecStart=/opt/vpn-service/scripts/run-mtproxy-managed`.\n- If `MTPROTO_MANAGED_WRAPPER_PATH` or `MTPROTO_MANAGED_ENV_PATH` differs from the defaults, edit the installed wrapper/drop-in during deploy and run `systemctl daemon-reload` manually.\n- Do not set `MTPROTO_MODE=managed` in `vpn-bot` until the placeholder managed baseline above has restarted successfully and `mtproxy` is active/listening. Bot issue/revoke refuses to proceed when `MTPROTO_MANAGED_SECRETS_PATH` or `MTPROTO_MANAGED_ENV_PATH` is missing, so the first helper apply always has known-good files to roll back to.\n- At runtime the non-root bot stages MTProxy candidates under `/run/vpn-bot/mtproxy`. The `/usr/local/sbin/vpnbot-mtproxy-apply` helper validates the staged files, writes `MTPROTO_MANAGED_SECRETS_PATH`, writes `MTPROTO_MANAGED_ENV_PATH`, maintains `MTPROTO_BACKUP_DIR/\u003cbackup-id\u003e/`, restarts `mtproxy`, checks `systemctl is-active`, checks that `MTPROTO_PORT` is listening, and restores the previous managed files on apply failure.\n- Normal issue/revoke does not write `/etc/systemd/system` and does not run `systemctl daemon-reload`; install or update the MTProxy unit/drop-in manually during deploy.\n- Managed mode gives real per-user revoke by removing only that user's secret from the active MTProxy list. Other users' secrets remain in the managed file.\n- Raw MTProto secrets are not shown in admin status, audit, logs, README, or `.env.example`; admin diagnostics use counts and fingerprints only.\n- Managed secrets and env files are root:root `0600`; backup directories are root:root `0700`; backup files that may contain secrets are root:root `0600`; the wrapper is root:root `0700`; the systemd drop-in contains no secrets and can be root:root `0600`.\n\nMTProto managed mode visibility checks:\n\n- `systemctl cat mtproxy` and `systemctl show mtproxy -p User -p Group -p ExecStart -p Environment` should show only the wrapper/env paths, not raw secrets. In the default root-wrapper model, `User` and `Group` are empty at service level.\n- `journalctl -u vpn-bot` and `journalctl -u mtproxy` should not contain raw MTProto secrets; the bot redacts audit/error details and the wrapper does not print secrets. If your MTProxy build logs accepted secrets or generated links, do not use managed mode until that logging is disabled or the binary is replaced.\n- The official `mtproto-proxy` binary accepts client secrets as `-S \u003csecret\u003e` arguments. That means raw secrets can be visible in process argv to root, and to unprivileged users unless `/proc` is hardened. Restrict shell access, consider mounting `/proc` with `hidepid=2`, and do not enable managed mode with this binary if your requirement is \"raw MTProto secrets are never visible to root-level process inspection\".\n\nManual rollback for managed MTProto:\n\n1. Stop `vpn-bot`.\n2. Inspect `MTPROTO_BACKUP_DIR`, default `/etc/mtproxy/vpnbot/backups`.\n3. Restore the previous managed secrets/env files from the latest known-good backup if automatic rollback did not recover.\n4. Run `sudo systemctl restart mtproxy`.\n5. Check `sudo systemctl status mtproxy --no-pager` and `sudo ss -tlnp | grep 8443`.\n\nProxy statistics are lifecycle/accounting stats from SQLite: issued, active, revoked/deactivated, timestamps, status, reason, and error. The bot does not invent per-user traffic for Dante or MTProxy. Without Dante per-login accounting or a safe aggregate MTProxy stats endpoint, traffic is shown as unavailable.\n\n## Deployment Overview\n\n\u003e ⚠️ **IMPORTANT — `deploy/vpn-bot.service` is the authoritative source:**\n\u003e Every deploy copies `deploy/vpn-bot.service` verbatim to `/etc/systemd/system/vpn-bot.service`. Manual edits to the system service file are overwritten on the next deploy. The current repo file runs the bot as `User=root` with `ProtectSystem=false` for `XRAY_APPLY_MODE=api` operation. If you switch deployment models, update `deploy/vpn-bot.service` first — do not edit the system file directly.\n\nThe supplied systemd unit expects the project in `/opt/vpn-service`. If you deploy elsewhere, update `deploy/vpn-bot.service` before installing it.\n\n**Root deployment model (current default — api mode, `User=root`):**\n\nThe repo service file is already configured for root+api mode. See [Xray API Mode](#xray-api-mode) for required `.env` variables and one-time Xray config preparation. There is no need to create a `vpn-bot` system user or install sudo helpers for this model.\n\n**Non-root deployment model (privilege helper mode, `User=vpn-bot`):**\n\nUpdate `deploy/vpn-bot.service` to set `User=vpn-bot`, `Group=vpn-bot`, `ProtectSystem=strict`, and restore `ReadWritePaths` before deploying. Then follow these steps:\n\n1. Keep `/opt/vpn-service`, deploy files, `.env`, and `.venv` owned by root/operator and not writable by `vpn-bot`.\n2. Create the `vpn-bot:vpn-bot` system identity.\n3. Grant `vpn-bot` write access only to runtime state: `/opt/vpn-service/data`, `/opt/vpn-service/logs` if file logs are enabled, and `/run/vpn-bot` created by systemd.\n4. Install fixed helpers under `/usr/local/sbin` and install `/etc/sudoers.d/vpnbot` with only those helper entrypoints.\n5. Enable `PRIVILEGE_HELPERS_ENABLED=true`.\n6. Install `deploy/vpn-bot.service`; it is the non-root unit.\n\nFresh install outline:\n\n```bash\nsudo install -o root -g root -m 0755 -d /opt/vpn-service\nsudo git clone https://github.com/Egor051/vpnbot.git /opt/vpn-service\ncd /opt/vpn-service\n\nsudo python3 -m venv .venv\nsudo /opt/vpn-service/.venv/bin/pip install --upgrade pip\nsudo /opt/vpn-service/.venv/bin/pip install -r requirements.txt -c constraints.txt\n\nsudo deploy/create-vpn-bot-user.sh\nsudo install -o vpn-bot -g vpn-bot -m 0700 -d /opt/vpn-service/data /opt/vpn-service/logs\nsudo install -o root -g root -m 0600 .env.example .env\nsudoedit .env\n```\n\nHelper and sudoers install:\n\n```bash\nsudo install -o root -g root -m 0755 deploy/helpers/vpnbot-socks5-user /usr/local/sbin/vpnbot-socks5-user\nsudo install -o root -g root -m 0755 deploy/helpers/vpnbot-xray-apply /usr/local/sbin/vpnbot-xray-apply\nsudo install -o root -g root -m 0755 deploy/helpers/vpnbot-awg-apply /usr/local/sbin/vpnbot-awg-apply\nsudo install -o root -g root -m 0755 deploy/helpers/vpnbot-mtproxy-apply /usr/local/sbin/vpnbot-mtproxy-apply\nsudo install -o root -g root -m 0440 deploy/sudoers.d/vpnbot.example /etc/sudoers.d/vpnbot\nsudo visudo -cf /etc/sudoers.d/vpnbot\n```\n\nInstall and start the systemd service:\n\n```bash\npython deploy/check-nonroot-helper-mode.py\nsudo cp deploy/vpn-bot.service /etc/systemd/system/vpn-bot.service\nsudo systemctl daemon-reload\nsudo systemctl enable --now vpn-bot\nsudo systemctl status vpn-bot\npython deploy/check-nonroot-helper-mode.py\n```\n\nDo not recursively chown the whole application tree to a login user for production. Do not make the repository checkout, deploy files, or `.venv` writable by `vpn-bot`; a compromised bot process must not be able to rewrite its own code, dependencies, units, or helper source.\n\nIf `MTPROTO_MODE=managed` is enabled, keep `/etc/mtproxy/vpnbot` root-owned and helper-managed. Do not grant `vpn-bot.service` runtime write access to `/etc/systemd/system` or broad write access to `/etc/mtproxy`; install or update the MTProxy drop-in and wrapper manually during deploy, then run `systemctl daemon-reload` outside the bot runtime.\n\nPost-deploy smoke checklist:\n\n1. `python deploy/check-nonroot-helper-mode.py` passes.\n2. `systemctl show vpn-bot -p User -p Group -p RuntimeDirectory -p NoNewPrivileges -p ReadWritePaths` shows `vpn-bot`, `vpn-bot`, `vpn-bot`, no enabled `NoNewPrivileges`, and only the expected writable paths.\n3. `sudo -u vpn-bot test ! -w /opt/vpn-service/.venv \u0026\u0026 sudo -u vpn-bot test ! -w /opt/vpn-service/deploy`.\n4. `sudo visudo -cf /etc/sudoers.d/vpnbot` passes and the file contains no `NOPASSWD: ALL`.\n5. Issue/revoke one staging Xray or AWG key and one enabled proxy backend access, then check `journalctl -u vpn-bot -n 100 --no-pager` for helper errors or secret leakage.\n\n## Local Checks\n\nInstall runtime and development dependencies before running checks:\n\n```bash\npython -m pip install -r requirements.txt -c constraints.txt\npython -m pip install -r requirements-dev.txt\n```\n\nRun the same core gates used by CI:\n\n```bash\npython -m pip_audit -r requirements.txt -r constraints.txt\npython -m ruff check .\npython -m compileall .\npython -m mypy --strict bot/ services/ adapters/ config/ models/ utils/ repositories/\npython -m pytest --cov=. --cov-report=term-missing --cov-fail-under=60\n```\n\n### Updating dependencies\n\nAfter changing `requirements.txt` or `requirements-dev.txt`, regenerate the hashed constraints files and commit them:\n\n```bash\nmake update-hashes\ngit add constraints-hashed.txt constraints-dev-hashed.txt\ngit commit -m \"chore: update hashed constraints\"\n```\n\nCI installs packages with `--require-hashes` and will fail if the committed hashes do not match what PyPI serves, protecting against supply-chain tampering.\n\n## CI Checks\n\nGitHub Actions runs the local gates without production secrets or live services:\n\n- `dependency-audit`: `python -m pip_audit -r requirements.txt -r constraints.txt` — blocks the `tests` job if vulnerabilities are found.\n- `tests` (needs `dependency-audit`): Python 3.12 — install runtime/dev dependencies, `ruff check .` (style + security + bugbear rules), `compileall`, `mypy --strict`, and `pytest ≥60% coverage`.\n\n## Maintenance\n\nUpdate from GitHub:\n\n```bash\ncd /opt/vpn-service\nsudo git pull --ff-only\nsudo /opt/vpn-service/.venv/bin/pip install -r requirements.txt -c constraints.txt\npython deploy/check-nonroot-helper-mode.py\nsudo systemctl restart vpn-bot\npython deploy/check-nonroot-helper-mode.py\n```\n\nDo not run production DB migrations as root against `/opt/vpn-service/data/vpn.db`. The service bootstraps schema/migrations on startup as `vpn-bot`; if you must run `init_db.py` manually, run it with the same non-root identity and environment as the service.\n\nCheck status:\n\n```bash\nsudo systemctl status vpn-bot\n```\n\nRestart the service:\n\n```bash\nsudo systemctl restart vpn-bot\n```\n\nView logs:\n\n```bash\nsudo journalctl -u vpn-bot -f\ntail -f /opt/vpn-service/logs/bot.log\n```\n\n## Production Operations Runbook\n\n### Pre-deploy checklist\n\n- `.env` exists, is not committed, and is readable only by the service operator/root.\n- `DB_PATH` parent and `LOG_DIR` exist and are not world-readable.\n- The installed systemd unit matches `deploy/vpn-bot.service`. In the default root+api configuration: `User=root`, `Group=root`, `ProtectSystem=false`, `RuntimeDirectory=vpn-bot`, `BOT_LOCK_PATH=/run/vpn-bot/vpn-bot.lock`.\n- For root+api mode: `PRIVILEGE_HELPERS_ENABLED=false` (or absent), `XRAY_APPLY_MODE=api`, `XRAY_INBOUND_TAG` set, `XRAY_STATS_SERVER` pointing to the Xray API address. For non-root helper mode: `PRIVILEGE_HELPERS_ENABLED=true`, helper paths point to `/usr/local/sbin/vpnbot-*`, and `/etc/sudoers.d/vpnbot` validates with `visudo -cf`.\n- `python deploy/check-nonroot-helper-mode.py` passes before the service restart.\n- Xray config exists at `XRAY_CONFIG_PATH` and validates before the bot writes to it.\n- AWG config/interface exist if AWG keys will be issued.\n- Firewall rules are known before opening VPN ports.\n- Backup destination exists and backup files are not world-readable.\n- Code, deploy files, and `.venv` are not writable by `vpn-bot` or other untrusted users.\n- If managed MTProto is enabled, `vpn-bot.service` does not have `ReadWritePaths=/etc/systemd/system`; the MTProxy wrapper/drop-in were installed manually and contain no raw secrets.\n- If managed MTProto is enabled, `/etc/mtproxy/vpnbot/managed-secrets.json`, `/etc/mtproxy/vpnbot/mtproxy.env`, and `/etc/mtproxy/vpnbot/backups/*` are readable only by root/service operators.\n\n### General bot health check\n\n```bash\ncd /opt/vpn-service\npython deploy/check-nonroot-helper-mode.py\nsudo systemctl status vpn-bot --no-pager\nsudo journalctl -u vpn-bot -n 100 --no-pager\nsqlite3 /opt/vpn-service/data/vpn.db \"PRAGMA quick_check;\"\n.venv/bin/python -m compileall .\n.venv/bin/python -m pytest\n```\n\n### Package 7 Healthcheck — preflight, postflight, and admin diagnostics\n\n\u003e ⚠️ **Note:** `deploy/check-nonroot-helper-mode.py` is designed for the **non-root privilege-helper deployment model** (`User=vpn-bot` + `PRIVILEGE_HELPERS_ENABLED=true`). If you are running the **root+api mode** (`User=root` + `XRAY_APPLY_MODE=api`), this checker will report `FAIL: User=root` — that is expected and correct for root deployment. Skip this checker in root mode; use `systemctl status vpn-bot` and the bot's admin diagnostics panel instead.\n\n`deploy/check-nonroot-helper-mode.py` is the mandatory preflight and postflight tool for the non-root privilege-separated deployment. Run it before and after every deploy.\n\n**Human-readable output (default):**\n\n```bash\ncd /opt/vpn-service\npython deploy/check-nonroot-helper-mode.py\n```\n\nExit codes:\n- `0` — all checks passed (warnings are informational, not failures)\n- `1` — one or more checks failed; address failures before starting or restarting the service\n\n**Machine-readable JSON output (for automation/CI):**\n\n```bash\npython deploy/check-nonroot-helper-mode.py --json\n```\n\nJSON format: `{\"overall\": \"ok|warning|failed\", \"failures\": N, \"warnings\": N, \"checks\": [{\"status\": \"ok|warning|failed\", \"message\": \"...\"}]}`\n\n**Pre-start mode (default — before `systemctl start vpn-bot`):**\n\n```bash\npython deploy/check-nonroot-helper-mode.py --mode pre-start\n```\n\nIn `pre-start` mode, `/run/vpn-bot` absence is expected (systemd creates the `RuntimeDirectory` when the service starts) and will produce a warning, not a failure.\n\n**Post-start mode (after `systemctl start vpn-bot`):**\n\n```bash\npython deploy/check-nonroot-helper-mode.py --mode post-start\n```\n\nIn `post-start` mode, `/run/vpn-bot` must exist and be writable by `vpn-bot`. Absence is a failure.\n\n**What the checker validates (Package 5D + Package 7):**\n\n- `vpn-bot.service` contains `User=vpn-bot`, `Group=vpn-bot`, `RuntimeDirectory=vpn-bot`, `RuntimeDirectoryMode=0700`, `ProtectSystem=strict`\n- `vpn-bot.service` does not contain `User=root`, `Group=root`, `NoNewPrivileges=true`\n- `/etc/sudoers.d/vpnbot` is root:root 0440, grants only the 4 fixed helpers, no broad grants (`NOPASSWD: ALL`, `ALL=(ALL)`)\n- Helper binaries are root:root 0755\n- `/opt/vpn-service`, `.venv`, `deploy` are not writable by `vpn-bot`\n- `/run/vpn-bot` existence and writability (mode-dependent)\n- `.env` is not world-readable and is readable by `vpn-bot`\n- SQLite `PRAGMA quick_check`\n- Xray config syntax test (`xray run -test -config`)\n- AWG config strip (`awg-quick strip`)\n- MTProxy managed files readable and structurally valid JSON\n- `sudo -n \u003chelper\u003e status` calls succeed (verifies sudoers grants work end-to-end)\n- `systemctl is-active` for: `vpn-bot`, `xray`, `awg-quick@awg0`, `danted`, `mtproxy`\n\n**Admin diagnostics in the bot (on-demand):**\n\nOpen the admin panel in Telegram → *Диагностика backend*. This runs a live read-only health check and shows:\n\n```\nDiagnostics  OK\n2026-05-12 10:30:00 UTC\n\n✓ Non-root OK (uid=1001)\n✓ PRIVILEGE_HELPERS_ENABLED=true\n✓ Xray: OK\n✓ AWG: OK\n✓ SOCKS5: OK\n✓ MTProto: OK\n✓ SQLite PRAGMA quick_check: ok\n✓ vpn-bot: active\n✓ xray: active\n✓ awg-quick@awg0: active\n...\n```\n\nOverall status is `OK / WARNING / DEGRADED / FAILED`. Secrets, tokens, private keys, and raw hex values are never shown — only the sanitised status and reason.\n\n**Expected sudo log entries:**\n\nWhen `PRIVILEGE_HELPERS_ENABLED=true`, every privileged operation (Xray/AWG config apply, SOCKS5 user create/delete, MTProto secret apply) produces a sudo log entry like:\n\n```\nvpn-bot : TTY=... ; PWD=... ; USER=root ; COMMAND=/usr/local/sbin/vpnbot-xray-apply apply ...\n```\n\nThese entries are **expected and normal**. They confirm the least-privilege model is working correctly.\n\n**Signs that require rollback:**\n\n- `FAIL: ... User=root` in checker output — the service is configured to run as root (expected and correct in root+api mode; only a failure in non-root helper mode)\n- `FAIL: ... NOPASSWD: ALL` — broad sudo grant is present\n- `FAIL: ... writable by vpn-bot` on code/venv/deploy directories\n- SQLite `PRAGMA quick_check` returns anything other than `ok`\n- Bot starts, issues one key, but Xray/AWG service is immediately DEGRADED with a config apply error\n- `sudo -n \u003chelper\u003e status` returns permission errors — sudoers file is incorrect\n- Any helper binary not root:root 0755 — must be fixed before the bot can use them\n\nIf rollback is needed, see the \"Rollback after a bad deploy\" section below.\n\n### Backup\n\nBack up at least these files before deploys, migrations, and manual backend edits:\n\n```bash\nsudo install -m 700 -d /root/vpn-service-backups\nsudo tar --xattrs --acls -czf /root/vpn-service-backups/vpn-service-$(date -u +%Y%m%dT%H%M%SZ).tar.gz \\\n  /opt/vpn-service/.env \\\n  /opt/vpn-service/data/vpn.db \\\n  /usr/local/etc/xray/config.json \\\n  /etc/amnezia/amneziawg/awg0.conf \\\n  /etc/mtproxy\nsudo chmod 600 /root/vpn-service-backups/vpn-service-*.tar.gz\n```\n\nInclude `/opt/vpn-service/logs` only if operational logs are needed for incident analysis. Treat all backups as sensitive because they can contain Telegram tokens, VPN keys, Xray UUIDs, AWG private/preshared keys, and server endpoints.\n\n### Restore\n\n```bash\nsudo systemctl stop vpn-bot\nsudo tar -xzf /root/vpn-service-backups/\u003cbackup\u003e.tar.gz -C /\nsudo xray run -test -config /usr/local/etc/xray/config.json\nsudo awg-quick strip /etc/amnezia/amneziawg/awg0.conf \u003e/dev/null\ncd /opt/vpn-service\nsudo install -o vpn-bot -g vpn-bot -m 0700 -d /opt/vpn-service/data /opt/vpn-service/logs\nsudo chown -R vpn-bot:vpn-bot /opt/vpn-service/data /opt/vpn-service/logs\npython deploy/check-nonroot-helper-mode.py\nsudo systemctl start vpn-bot\nsudo systemctl status vpn-bot\nsudo journalctl -u vpn-bot -n 100 --no-pager\n```\n\nIf `awg-quick` is unavailable but `wg-quick` is the intended tool on the server, run the equivalent `wg-quick strip` check. Do not run `awg set`, `wg set`, `systemctl restart xray`, or runtime-changing commands during restore validation until the config files have passed read-only checks.\n\n### Firewall and exposed ports\n\n- Keep SSH open only from trusted sources where possible.\n- Open the public Xray TCP port, usually `443/tcp`.\n- Open the public AWG endpoint UDP port from `AWG_ENDPOINT_PORT` or the AWG config `ListenPort`.\n- Open Dante/SOCKS only if a separate proxy is intentionally deployed and protected.\n- Keep `XRAY_STATS_SERVER` bound to localhost only, for example `127.0.0.1:\u003cport\u003e`. Never expose the Xray stats API to the internet.\n- If UFW default routed policy is `deny`, explicitly allow routed traffic required by AWG clients.\n\nExample read-only checks:\n\n```bash\nsudo ufw status verbose\nsudo ss -tulnp\n```\n\n### Read-only health checks\n\n```bash\nsudo systemctl status vpn-bot --no-pager\nsudo systemctl status xray --no-pager\nsudo systemctl status danted --no-pager\nsudo ss -tlnp | grep 31337\nsudo systemctl status mtproxy --no-pager\nsudo ss -tlnp | grep 8443\nsudo journalctl -u vpn-bot -n 100 --no-pager\nsudo xray run -test -config /usr/local/etc/xray/config.json\nsudo awg show\nsudo awg-quick strip /etc/amnezia/amneziawg/awg0.conf \u003e/dev/null\nsqlite3 /opt/vpn-service/data/vpn.db \"PRAGMA quick_check; SELECT status, key_type, COUNT(*) FROM vpn_keys GROUP BY status, key_type;\"\n```\n\nIf `XRAY_STATS_SERVER` is configured locally, query it only from the server or localhost. Confirm that bot DB status, Xray config clients, AWG config peers, and AWG runtime peers agree after create/revoke/delete operations.\n\n### Xray degraded recovery\n\nXray DEGRADED blocks only Xray create/revoke/delete/manual reconcile. AWG, SOCKS5, and MTProto continue unless separately degraded.\n\n```bash\nsudo systemctl status xray --no-pager\nsudo xray run -test -config /usr/local/etc/xray/config.json\nsudo jq '[.inbounds[]?.settings.clients[]? | {email}]' /usr/local/etc/xray/config.json\nsqlite3 /opt/vpn-service/data/vpn.db \"SELECT status, key_type, COUNT(*) FROM vpn_keys WHERE key_type='xray' GROUP BY status, key_type;\"\nsudo journalctl -u vpn-bot -n 150 --no-pager\n```\n\nCheck for manual clients/orphans, failed pending statuses, and config syntax errors. Restore from backup or remove only confirmed bot-managed drift, then restart `vpn-bot` and re-open admin backend diagnostics.\n\n### AWG degraded recovery\n\nAWG DEGRADED blocks only AWG create/revoke/delete/manual reconcile. Xray, SOCKS5, and MTProto continue unless separately degraded.\n\n```bash\nsudo systemctl status awg-quick@awg0 --no-pager\nsudo awg show\nsudo awk '/^# vpnbot key_id=|^PublicKey =|^AllowedIPs =/{print}' /etc/amnezia/amneziawg/awg0.conf\nsqlite3 /opt/vpn-service/data/vpn.db \"SELECT status, key_type, COUNT(*) FROM vpn_keys WHERE key_type='awg' GROUP BY status, key_type;\"\nsudo journalctl -u vpn-bot -n 150 --no-pager\n```\n\nDo not print AWG private keys or preshared keys into tickets/chat. Compare public keys/client IPs only, fix confirmed drift from backup or manual state, then restart `vpn-bot`.\n\n### SOCKS5 degraded recovery\n\nSOCKS5 DEGRADED blocks only SOCKS5 issue/revoke/delete. Xray, AWG, and MTProto continue unless separately degraded.\n\n```bash\nsudo systemctl status danted --no-pager\ngetent passwd | awk -F: '$1 ~ /^vpn_socks_/ {print $1}'\nsqlite3 /opt/vpn-service/data/vpn.db \"SELECT status, access_type, COUNT(*) FROM proxy_accesses WHERE access_type='socks5' GROUP BY status, access_type;\"\nsudo journalctl -u vpn-bot -n 150 --no-pager\n```\n\nCheck that every managed Linux user starts with `SOCKS5_LOGIN_PREFIX`; do not print SOCKS5 passwords. Lock/delete only confirmed bot-managed stray users, restore SQLite from backup if needed, then restart `vpn-bot`.\n\n### MTProto degraded recovery\n\nMTProto DEGRADED blocks only MTProto issue/revoke/delete. Xray, AWG, and SOCKS5 continue unless separately degraded.\n\n```bash\nsudo systemctl status mtproxy --no-pager\nsudo jq '{secret_count: (.secrets | length), fingerprints: [.secrets[]?.fingerprint]}' /etc/mtproxy/vpnbot/managed-secrets.json\nsqlite3 /opt/vpn-service/data/vpn.db \"SELECT status, access_type, COUNT(*) FROM proxy_accesses WHERE access_type='mtproto' GROUP BY status, access_type;\"\nsudo journalctl -u vpn-bot -n 150 --no-pager\n```\n\nDo not print raw MTProto secrets. In static mode, per-user server-side revoke is impossible; rotate `MTPROTO_SECRET` if a copied shared secret must be invalidated. In managed mode, compare counts/fingerprints, restore managed files from `/etc/mtproxy/vpnbot/backups` if needed, restart `mtproxy`, then restart `vpn-bot`.\n\n### Rollback after a bad deploy\n\n\u003e ⚠️ **Back up first.** Always create a backup before rolling back code (see [Backup](#backup)). A code rollback does not roll back runtime state — SQLite, Xray config, and AWG config need separate restoration if the deploy already modified them.\n\n**Step 1 — stop the service and back up runtime state:**\n\n```bash\nsudo systemctl stop vpn-bot\nsudo tar --xattrs --acls -czf /root/vpn-service-backups/pre-rollback-$(date -u +%Y%m%dT%H%M%SZ).tar.gz \\\n  /opt/vpn-service/.env \\\n  /opt/vpn-service/data/vpn.db \\\n  /usr/local/etc/xray/config.json \\\n  /etc/amnezia/amneziawg/awg0.conf\nsudo chmod 600 /root/vpn-service-backups/pre-rollback-*.tar.gz\n```\n\n**Step 2 — roll back the code:**\n\n```bash\ncd /opt/vpn-service\ngit log --oneline -5\ngit reset --hard \u003cprevious_commit\u003e\n.venv/bin/pip install -r requirements.txt -c constraints.txt\n```\n\n`git reset --hard` discards all local code changes on the server. Only use it when rolling back an unwanted deploy.\n\n\u003e **`init_db.py` is for fresh installs only.** Do NOT run `init_db.py` during rollback — it requires `BOT_TOKEN`/`ADMIN_IDS` and will attempt forward migrations on the existing database. The bot bootstraps the schema on startup; if the previous version is schema-compatible, simply restarting the service is sufficient.\n\n**Step 3 — restore runtime state from backup if the failed deploy modified it:**\n\n```bash\n# Restore SQLite DB if the failed deploy changed DB schema or data\nsudo cp /root/vpn-service-backups/\u003cbackup\u003e.tar.gz /tmp/\nsudo tar -xzf /tmp/\u003cbackup\u003e.tar.gz -C / opt/vpn-service/data/vpn.db\n\n# Restore Xray config if changed\nsudo tar -xzf /tmp/\u003cbackup\u003e.tar.gz -C / usr/local/etc/xray/config.json\nsudo xray run -test -config /usr/local/etc/xray/config.json\n\n# Restore AWG config if changed\nsudo tar -xzf /tmp/\u003cbackup\u003e.tar.gz -C / etc/amnezia/amneziawg/awg0.conf\n```\n\n**Step 4 — restart and verify:**\n\n```bash\nsudo systemctl start vpn-bot\nsudo systemctl status vpn-bot\nsudo journalctl -u vpn-bot -n 100 --no-pager\n```\n\n### Manual VDS verification after fixes\n\nOn a staging user before production use:\n\n1. Create one Xray key, verify it is active in DB and present in Xray config.\n2. Revoke and delete the Xray key, verify DB/config/runtime no longer allow access.\n3. Create one AWG key, verify DB, `awg0.conf`, and `awg show` agree.\n4. Revoke and delete the AWG key, verify peer removal from config and runtime.\n5. Open \"Прокси\" as an approved test user, issue SOCKS5 after confirmation, and verify the message contains Host, Port, Login, Password, and URL.\n6. Issue MTProto after confirmation and verify the plain Telegram link appears before the `dd` link.\n7. In `MTPROTO_MODE=managed`, issue MTProto for test user A and record only the non-secret fingerprint/count from admin status.\n8. Issue MTProto for test user B and confirm admin status shows two active managed MTProto accesses.\n9. Hard-block or admin-revoke test user A, then confirm the managed secrets file no longer contains A's fingerprint while B's fingerprint remains active.\n10. Confirm user B's Telegram MTProto link still works after user A is revoked.\n11. Simulate a failed apply on staging, for example by temporarily pointing `MTPROTO_SERVICE_NAME` to a failing test unit or stopping the listener check path, then revoke/issue and confirm rollback restores the previous managed secrets/env files and `mtproxy` returns to active/listening.\n12. In `MTPROTO_MODE=static`, block the user and confirm MTProto is deactivated only in SQLite.\n13. Check that bot logs and audit output do not contain SOCKS5 passwords, `MTPROTO_SECRET`, or managed raw MTProto secrets.\n14. Check `systemctl cat mtproxy`, `systemctl show mtproxy -p User -p Group -p ExecStart -p Environment`, and `journalctl -u mtproxy -n 100 --no-pager` for absence of raw MTProto secrets.\n15. Check managed file permissions:\n    ```bash\n    sudo stat -c '%U:%G %a %n' /opt/vpn-service/scripts/run-mtproxy-managed /etc/mtproxy/vpnbot/managed-secrets.json /etc/mtproxy/vpnbot/mtproxy.env\n    sudo find /etc/mtproxy/vpnbot/backups -maxdepth 2 -printf '%u:%g %m %p\\n'\n    ```\n16. Send an announcement with approved, pending, and blocked test users; only approved users and superadmins should receive it.\n\n## Database\n\nSQLite is used as the local storage backend. By default the database path is:\n\n```text\n/opt/vpn-service/data/vpn.db\n```\n\n`init_db.py` opens the database and applies schema bootstrap/migrations. The bot also bootstraps the database during app creation.\n\nCurrent schema tables include:\n\n- `users`\n- `access_requests`\n- `vpn_keys`\n- `proxy_entries`\n- `proxy_accesses`\n- `audit_log`\n- `vpn_key_traffic_stats`\n\n## Project Status\n\nEarly self-hosted project. It is usable as a focused VPN management bot, but production use requires careful review, server-specific testing, operational backups, secret handling discipline, and hardening of the surrounding Xray/AWG/server setup.\n\n## License\n\nMIT License. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fegor051%2Fvpnbot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fegor051%2Fvpnbot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fegor051%2Fvpnbot/lists"}