Telegram 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.
This project is designed for a single-server deployment without Docker, Redis, PostgreSQL, or a heavy ORM.
- Telegram user registration and access approval flow.
- Admin panel for pending requests, users, key issuance, audit, stats, and announcements.
- Xray VLESS Reality key creation, config delivery, revocation, deletion, and startup reconciliation.
- AmneziaWG key creation, client config delivery, revocation, deletion, IP allocation, and startup reconciliation.
- Separate one-page Telegram section "Прокси" for SOCKS5/Dante auto-issue and Telegram MTProto Proxy links.
- MTProto supports
staticcompatibility mode andmanagedmode with per-user secrets, safe apply, and rollback. - Optional legacy proxy entry table seeded from
DEFAULT_PROXY_*remains internal/compatibility storage; the user-facing proxy UX usesproxy_accesses. - Ownership checks so users can view their own configs/stats; destructive VPN and proxy lifecycle actions are admin-only.
- Audit log with recursive masking for sensitive values.
- SQLite storage with migrations from
db/schema.sql. - Rotating local logs in
LOG_DIR. - systemd deployment using
deploy/vpn-bot.service. - Intended target: Ubuntu VDS with existing Xray and/or AmneziaWG installation.
- Python 3.12+
- aiogram 3
- SQLite via aiosqlite
- python-dotenv
- systemd
- Xray VLESS Reality
- AmneziaWG / WireGuard-compatible tooling
- Ubuntu / Linux VDS
main.py # Bot entry point
init_db.py # SQLite schema bootstrap/migration entry point
requirements.txt # Runtime dependencies
constraints.txt # Pinned production dependency constraints
.env.example # Environment variable template
db/schema.sql # Database schema
deploy/vpn-bot.service # vpn-bot systemd unit template
deploy/run-mtproxy-managed # MTProxy managed-mode wrapper installed during deploy
deploy/mtproxy-vpnbot-managed.conf # MTProxy drop-in installed during deploy
bot/ # Telegram handlers, keyboards, FSM, formatting
services/ # Business workflows and permissions
repositories/ # SQLite access layer
adapters/ # Xray, AWG, systemctl, backups, shell adapters
config/settings.py # Environment parsing and validation
tests/ # Regression and hardening tests
This project handles operational VPN and Telegram secrets. Never commit or publish:
.envfiles.- Telegram bot tokens.
- Private keys or preshared keys.
- Real Xray Reality server/client configuration.
- Real AmneziaWG server/client configuration.
- Full VPN client configs.
- SQLite databases or database dumps.
- Server IP addresses combined with credentials.
- SSH, panel, hosting, or other server credentials.
- 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.
Use .env.example only as a template. Keep production configuration on the server and outside Git history.
Copy .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.
BOT_TOKEN=<telegram_bot_token>
ADMIN_IDS=<telegram_user_id>,<telegram_user_id>
DB_PATH=/opt/vpn-service/data/vpn.db
SQLITE_SYNCHRONOUS=FULL
LOG_DIR=/opt/vpn-service/logs
BOT_LOCK_PATH=/run/vpn-bot/vpn-bot.lock
# Production non-root helper mode
PRIVILEGE_HELPERS_ENABLED=true
HELPER_STAGING_ROOT=/run/vpn-bot
SOCKS5_USER_HELPER_PATH=/usr/local/sbin/vpnbot-socks5-user
XRAY_APPLY_HELPER_PATH=/usr/local/sbin/vpnbot-xray-apply
AWG_APPLY_HELPER_PATH=/usr/local/sbin/vpnbot-awg-apply
MTPROTO_APPLY_HELPER_PATH=/usr/local/sbin/vpnbot-mtproxy-apply
XRAY_CONFIG_PATH=/usr/local/etc/xray/config.json
XRAY_SERVICE_NAME=xray
XRAY_INBOUND_TAG=
XRAY_PUBLIC_HOST=<vpn_public_host>
XRAY_PUBLIC_PORT=443
XRAY_REALITY_PUBLIC_KEY=<xray_reality_public_key>
XRAY_SNI=<xray_reality_sni>
XRAY_FLOW=xtls-rprx-vision
XRAY_FINGERPRINT=chrome
XRAY_NETWORK_TYPE=tcp
XRAY_SHORT_ID=<xray_short_id>
XRAY_MANAGE_SHORT_IDS=false
XRAY_ALLOW_RESTART_ON_ROLLBACK=false
XRAY_STATS_SERVER=
AWG_CONFIG_PATH=/etc/amnezia/amneziawg/awg0.conf
AWG_INTERFACE=awg0
AWG_NETWORK=10.0.0.0/24
AWG_SERVER_ADDRESS=10.0.0.1
AWG_ENDPOINT_HOST=<awg_endpoint_host>
AWG_ENDPOINT_PORT=<awg_endpoint_port>
AWG_SERVER_PUBLIC_KEY=<awg_server_public_key>
AWG_DNS=1.1.1.1
AWG_MTU=
AWG_ALLOWED_IPS=0.0.0.0/0, ::/0
AWG_PERSISTENT_KEEPALIVE=25
AWG_USE_PRESHARED_KEY=true
DEFAULT_PROXY_TYPE=
DEFAULT_PROXY_HOST=
DEFAULT_PROXY_PORT=
DEFAULT_PROXY_LOGIN=
DEFAULT_PROXY_PASSWORD=
DEFAULT_PROXY_NOTE=
SOCKS5_ENABLED=false
SOCKS5_HOST=
SOCKS5_PORT=31337
SOCKS5_LOGIN_PREFIX=vpn_socks_
SOCKS5_SYSTEM_USER_SHELL=/usr/sbin/nologin
SOCKS5_SERVICE_NAME=danted
SOCKS5_PUBLIC_NAME=SOCKS5 Proxy
SOCKS5_NOTE=SOCKS5 Dante proxy on VDS
MTPROTO_ENABLED=false
MTPROTO_MODE=static
MTPROTO_HOST=
MTPROTO_PORT=8443
MTPROTO_SECRET=
MTPROTO_PUBLIC_NAME=Telegram MTProto Proxy
MTPROTO_NOTE=MTProto proxy for Telegram
# Managed MTProto per-user secrets mode
MTPROTO_SERVICE_NAME=mtproxy
MTPROTO_BINARY_PATH=/usr/local/bin/mtproto-proxy
MTPROTO_RUN_USER=mtproxy
MTPROTO_RUN_GROUP=mtproxy
MTPROTO_CONFIG_DIR=/etc/mtproxy
MTPROTO_PROXY_SECRET_PATH=/etc/mtproxy/proxy-secret
MTPROTO_PROXY_MULTI_CONF_PATH=/etc/mtproxy/proxy-multi.conf
MTPROTO_MANAGED_DIR=/etc/mtproxy/vpnbot
MTPROTO_MANAGED_SECRETS_PATH=/etc/mtproxy/vpnbot/managed-secrets.json
MTPROTO_MANAGED_ENV_PATH=/etc/mtproxy/vpnbot/mtproxy.env
MTPROTO_MANAGED_WRAPPER_PATH=/opt/vpn-service/scripts/run-mtproxy-managed
MTPROTO_BACKUP_DIR=/etc/mtproxy/vpnbot/backups
MTPROTO_INTERNAL_STATS_PORT=8888
MTPROTO_WORKERS=1
MTPROTO_APPLY_TIMEOUT_SECONDS=10
MTPROTO_ROLLBACK_ON_APPLY_FAILURE=true
MTPROTO_KEEP_LAST_BACKUPS=10
MTPROTO_STATS_URL=
AUDIT_RETENTION_DAYS=180
CONFIG_BACKUP_KEEP_LAST=20Notes:
- If
XRAY_INBOUND_TAGis empty, the adapter uses the first inbound withsettings.clients. - If
XRAY_MANAGE_SHORT_IDS=false,XRAY_SHORT_IDmust be set. XRAY_APPLY_MODE=restartis the default production apply mode; usereloadonly when your Xray unit reliably applies reload.SQLITE_SYNCHRONOUS=FULLis the safer default for this control-plane database.NORMALis faster but can lose the last committed transactions on OS or power failure while VPN backend state has already changed.AWG_CLIENT_DNSis supported only as a legacy alias; useAWG_DNSfor new deployments.AWG_ENDPOINT_HOSTandAWG_ENDPOINT_PORTshould point to the public AWG endpoint clients will use.SOCKS5_ENABLED=truerequiresSOCKS5_HOST,SOCKS5_PORT, and a safeSOCKS5_LOGIN_PREFIX. Dante must already be installed and listening; the bot only creates/locks/deletes managed Linux users with that prefix.MTPROTO_ENABLED=truerequiresMTPROTO_HOST.MTPROTO_MODE=staticalso requiresMTPROTO_SECRET.MTPROTO_MODE=staticis 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.MTPROTO_MODE=managedcreates one unique secret per user. In production helper mode the bot stages managed files under/run/vpn-bot/mtproxy;/usr/local/sbin/vpnbot-mtproxy-applywrites/etc/mtproxy/vpnbot, restartsmtproxy, 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.MTPROTO_SECRET, SOCKS5 passwords, and real production endpoints with credentials must never be committed..env.exampleintentionally keeps proxy secrets empty.DEFAULT_PROXY_*is legacy compatibility storage and does not drive the new user-facing proxy access flow.- Production deployment runs the bot as
vpn-bot:vpn-botwithPRIVILEGE_HELPERS_ENABLED=true. Root-only backend changes go through the fixed sudo helpers documented indeploy/helpers/README.md. - Keep project code, deploy files,
.env, and.venvoutsidevpn-botwrite access. Only/opt/vpn-service/data,/opt/vpn-service/logsif file logs are enabled, and/run/vpn-botshould be writable by the service user.
- Approved users may create their own Xray/AWG keys, view their own active configs, view stats, and edit their own key notes.
- Approved users may issue and view their own SOCKS5/MTProto proxy access when the backend is enabled.
- 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.
- Revoke/delete for SOCKS5 and MTProto proxy access is admin-only. The user-facing proxy page only issues/shows active access and stats.
- Blocking a user is an admin action. It blocks bot access and attempts to revoke active/problem VPN keys and SOCKS5/MTProto proxy access.
- In
MTPROTO_MODE=static, blocking/revoking only deactivates the bot/SQLite record; a copied shared secret keeps working until the shared secret is rotated. - In
MTPROTO_MODE=managed, admin revoke removes that user's MTProto secret from the managed active list while other users remain active.
The 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:
- Xray DEGRADED blocks Xray create/revoke/delete/manual reconcile only.
- AWG DEGRADED blocks AWG create/revoke/delete/manual reconcile only.
- SOCKS5 DEGRADED blocks SOCKS5 issue/revoke/delete only.
- MTProto DEGRADED blocks MTProto issue/revoke/delete only.
- Other backends continue working unless they are also DEGRADED.
The 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.
The bot does not install Dante or MTProxy. Prepare them on the VDS first, then enable the relevant env flags.
SOCKS5/Dante expectations:
- Dante listens on the configured public host/port, for example
0.0.0.0:31337. - Authentication is Linux username/password.
- 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 callgetent,useradd,chpasswd,passwd -l, anduserdel. - The bot refuses to manage Linux users whose login does not start with
SOCKS5_LOGIN_PREFIX.
MTProto static mode:
- Set
MTPROTO_MODE=staticand provideMTPROTO_SECRET. - MTProxy is managed outside the bot by its own systemd unit.
- The bot does not edit MTProxy files in static mode.
- User output always includes both Telegram links: plain secret first, then the
ddrandom-padding variant. - Static mode uses a shared secret; blocking one user only deactivates the bot record and does not revoke that user server-side.
MTProto managed mode:
- Set
MTPROTO_MODE=managed; do not set a shared production secret inMTPROTO_SECRETfor new users. - MTProxy must already be installed and have valid
proxy-secretandproxy-multi.conffiles. - 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-proxywith-u mtproxyfromMTPROTO_RUN_USERso the proxy process drops privileges internally.sudo install -m 700 -d /opt/vpn-service/scripts sudo install -m 700 deploy/run-mtproxy-managed /opt/vpn-service/scripts/run-mtproxy-managed sudo install -m 700 -d /etc/systemd/system/mtproxy.service.d sudo install -m 600 deploy/mtproxy-vpnbot-managed.conf /etc/systemd/system/mtproxy.service.d/vpnbot-managed.conf sudo install -m 700 -d /etc/mtproxy/vpnbot /etc/mtproxy/vpnbot/backups sudo chown root:root /opt/vpn-service/scripts/run-mtproxy-managed /etc/mtproxy/vpnbot /etc/mtproxy/vpnbot/backups sudo /opt/vpn-service/.venv/bin/python - <<'PY' import json, secrets from pathlib import Path managed = Path("/etc/mtproxy/vpnbot") placeholder = secrets.token_hex(16) (managed / "managed-secrets.json").write_text(json.dumps({ "version": 1, "generation": 0, "managed_by": "vpn-bot", "secrets": [], "runtime_secrets": [{"secret": placeholder, "fingerprint": "empty-placeholder", "purpose": "empty-placeholder"}], }, indent=2, sort_keys=True) + "\n", encoding="utf-8") (managed / "mtproxy.env").write_text( "MTPROTO_BINARY_PATH=/usr/local/bin/mtproto-proxy\n" "MTPROTO_RUN_USER=mtproxy\n" "MTPROTO_RUN_GROUP=mtproxy\n" "MTPROTO_PROXY_SECRET_PATH=/etc/mtproxy/proxy-secret\n" "MTPROTO_PROXY_MULTI_CONF_PATH=/etc/mtproxy/proxy-multi.conf\n" "MTPROTO_MANAGED_SECRETS_PATH=/etc/mtproxy/vpnbot/managed-secrets.json\n" "MTPROTO_PORT=8443\n" "MTPROTO_INTERNAL_STATS_PORT=8888\n" "MTPROTO_WORKERS=1\n", encoding="utf-8", ) PY sudo chmod 600 /etc/mtproxy/vpnbot/managed-secrets.json /etc/mtproxy/vpnbot/mtproxy.env sudo chown root:root /etc/mtproxy/vpnbot/managed-secrets.json /etc/mtproxy/vpnbot/mtproxy.env sudo systemctl daemon-reload sudo systemctl restart mtproxy sudo systemctl status mtproxy --no-pager sudo ss -tlnp | grep 8443
- The drop-in clears any existing
User=/Group=frommtproxy.service;systemctl show mtproxy -p User -p Group -p ExecStartshould show emptyUser/GroupandExecStart=/opt/vpn-service/scripts/run-mtproxy-managed. - If
MTPROTO_MANAGED_WRAPPER_PATHorMTPROTO_MANAGED_ENV_PATHdiffers from the defaults, edit the installed wrapper/drop-in during deploy and runsystemctl daemon-reloadmanually. - Do not set
MTPROTO_MODE=managedinvpn-botuntil the placeholder managed baseline above has restarted successfully andmtproxyis active/listening. Bot issue/revoke refuses to proceed whenMTPROTO_MANAGED_SECRETS_PATHorMTPROTO_MANAGED_ENV_PATHis missing, so the first helper apply always has known-good files to roll back to. - At runtime the non-root bot stages MTProxy candidates under
/run/vpn-bot/mtproxy. The/usr/local/sbin/vpnbot-mtproxy-applyhelper validates the staged files, writesMTPROTO_MANAGED_SECRETS_PATH, writesMTPROTO_MANAGED_ENV_PATH, maintainsMTPROTO_BACKUP_DIR/<backup-id>/, restartsmtproxy, checkssystemctl is-active, checks thatMTPROTO_PORTis listening, and restores the previous managed files on apply failure. - Normal issue/revoke does not write
/etc/systemd/systemand does not runsystemctl daemon-reload; install or update the MTProxy unit/drop-in manually during deploy. - 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.
- Raw MTProto secrets are not shown in admin status, audit, logs, README, or
.env.example; admin diagnostics use counts and fingerprints only. - Managed secrets and env files are root:root
0600; backup directories are root:root0700; backup files that may contain secrets are root:root0600; the wrapper is root:root0700; the systemd drop-in contains no secrets and can be root:root0600.
MTProto managed mode visibility checks:
systemctl cat mtproxyandsystemctl show mtproxy -p User -p Group -p ExecStart -p Environmentshould show only the wrapper/env paths, not raw secrets. In the default root-wrapper model,UserandGroupare empty at service level.journalctl -u vpn-botandjournalctl -u mtproxyshould 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.- The official
mtproto-proxybinary accepts client secrets as-S <secret>arguments. That means raw secrets can be visible in process argv to root, and to unprivileged users unless/procis hardened. Restrict shell access, consider mounting/procwithhidepid=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".
Manual rollback for managed MTProto:
- Stop
vpn-bot. - Inspect
MTPROTO_BACKUP_DIR, default/etc/mtproxy/vpnbot/backups. - Restore the previous managed secrets/env files from the latest known-good backup if automatic rollback did not recover.
- Run
sudo systemctl restart mtproxy. - Check
sudo systemctl status mtproxy --no-pagerandsudo ss -tlnp | grep 8443.
Proxy 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.
The supplied systemd unit expects the project in /opt/vpn-service. If you deploy elsewhere, update deploy/vpn-bot.service before installing it.
Production deployment model:
- Keep
/opt/vpn-service, deploy files,.env, and.venvowned by root/operator and not writable byvpn-bot. - Create the
vpn-bot:vpn-botsystem identity. - Grant
vpn-botwrite access only to runtime state:/opt/vpn-service/data,/opt/vpn-service/logsif file logs are enabled, and/run/vpn-botcreated by systemd. - Install fixed helpers under
/usr/local/sbinand install/etc/sudoers.d/vpnbotwith only those helper entrypoints. - Enable
PRIVILEGE_HELPERS_ENABLED=true. - Install
deploy/vpn-bot.service; it is the production non-root unit.
Fresh install outline:
sudo install -o root -g root -m 0755 -d /opt/vpn-service
sudo git clone https://github.com/Egor051/vpnbot.git /opt/vpn-service
cd /opt/vpn-service
sudo python3 -m venv .venv
sudo /opt/vpn-service/.venv/bin/pip install --upgrade pip
sudo /opt/vpn-service/.venv/bin/pip install -r requirements.txt -c constraints.txt
sudo deploy/create-vpn-bot-user.sh
sudo install -o vpn-bot -g vpn-bot -m 0700 -d /opt/vpn-service/data /opt/vpn-service/logs
sudo install -o root -g root -m 0600 .env.example .env
sudoedit .envHelper and sudoers install:
sudo install -o root -g root -m 0755 deploy/helpers/vpnbot-socks5-user /usr/local/sbin/vpnbot-socks5-user
sudo install -o root -g root -m 0755 deploy/helpers/vpnbot-xray-apply /usr/local/sbin/vpnbot-xray-apply
sudo install -o root -g root -m 0755 deploy/helpers/vpnbot-awg-apply /usr/local/sbin/vpnbot-awg-apply
sudo install -o root -g root -m 0755 deploy/helpers/vpnbot-mtproxy-apply /usr/local/sbin/vpnbot-mtproxy-apply
sudo install -o root -g root -m 0440 deploy/sudoers.d/vpnbot.example /etc/sudoers.d/vpnbot
sudo visudo -cf /etc/sudoers.d/vpnbotInstall and start the systemd service:
python deploy/check-nonroot-helper-mode.py
sudo cp deploy/vpn-bot.service /etc/systemd/system/vpn-bot.service
sudo systemctl daemon-reload
sudo systemctl enable --now vpn-bot
sudo systemctl status vpn-bot
python deploy/check-nonroot-helper-mode.pyDo 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.
If 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.
Post-deploy smoke checklist:
python deploy/check-nonroot-helper-mode.pypasses.systemctl show vpn-bot -p User -p Group -p RuntimeDirectory -p NoNewPrivileges -p ReadWritePathsshowsvpn-bot,vpn-bot,vpn-bot, no enabledNoNewPrivileges, and only the expected writable paths.sudo -u vpn-bot test ! -w /opt/vpn-service/.venv && sudo -u vpn-bot test ! -w /opt/vpn-service/deploy.sudo visudo -cf /etc/sudoers.d/vpnbotpasses and the file contains noNOPASSWD: ALL.- Issue/revoke one staging Xray or AWG key and one enabled proxy backend access, then check
journalctl -u vpn-bot -n 100 --no-pagerfor helper errors or secret leakage.
Install runtime and development dependencies before running checks:
python -m pip install -r requirements.txt -c constraints.txt
python -m pip install -r requirements-dev.txtRun the same core gates used by CI:
python -m ruff check . --select=E9,F63,F7,F82
python -m compileall .
python -m pytest
python -m pip_audit -r requirements.txt -r constraints.txt --no-depsGitHub Actions runs the local gates without production secrets or live services:
- Python 3.12: install runtime/dev dependencies,
python -m ruff check . --select=E9,F63,F7,F82,python -m compileall .,python -m mypy, andpython -m pytest. - Dependency audit on Python 3.12:
python -m pip_audit -r requirements.txt -r constraints.txt --no-deps.
Update from GitHub:
cd /opt/vpn-service
sudo git pull --ff-only
sudo /opt/vpn-service/.venv/bin/pip install -r requirements.txt -c constraints.txt
python deploy/check-nonroot-helper-mode.py
sudo systemctl restart vpn-bot
python deploy/check-nonroot-helper-mode.pyDo 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.
Check status:
sudo systemctl status vpn-botRestart the service:
sudo systemctl restart vpn-botView logs:
sudo journalctl -u vpn-bot -f
tail -f /opt/vpn-service/logs/bot.log.envexists, is not committed, and is readable only by the service operator/root.DB_PATHparent andLOG_DIRexist and are not world-readable.- The installed systemd unit matches
deploy/vpn-bot.service, runs asUser=vpn-botandGroup=vpn-bot, usesRuntimeDirectory=vpn-bot, and setsBOT_LOCK_PATH=/run/vpn-bot/vpn-bot.lock. PRIVILEGE_HELPERS_ENABLED=true, helper paths point to/usr/local/sbin/vpnbot-*, and/etc/sudoers.d/vpnbotvalidates withvisudo -cf.python deploy/check-nonroot-helper-mode.pypasses before the service restart.- Xray config exists at
XRAY_CONFIG_PATHand validates before the bot writes to it. - AWG config/interface exist if AWG keys will be issued.
- Firewall rules are known before opening VPN ports.
- Backup destination exists and backup files are not world-readable.
- Code, deploy files, and
.venvare not writable byvpn-botor other untrusted users. - If managed MTProto is enabled,
vpn-bot.servicedoes not haveReadWritePaths=/etc/systemd/system; the MTProxy wrapper/drop-in were installed manually and contain no raw secrets. - 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.
cd /opt/vpn-service
python deploy/check-nonroot-helper-mode.py
sudo systemctl status vpn-bot --no-pager
sudo journalctl -u vpn-bot -n 100 --no-pager
sqlite3 /opt/vpn-service/data/vpn.db "PRAGMA quick_check;"
.venv/bin/python -m compileall .
.venv/bin/python -m pytestdeploy/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.
Human-readable output (default):
cd /opt/vpn-service
python deploy/check-nonroot-helper-mode.pyExit codes:
0— all checks passed (warnings are informational, not failures)1— one or more checks failed; address failures before starting or restarting the service
Machine-readable JSON output (for automation/CI):
python deploy/check-nonroot-helper-mode.py --jsonJSON format: {"overall": "ok|warning|failed", "failures": N, "warnings": N, "checks": [{"status": "ok|warning|failed", "message": "..."}]}
Pre-start mode (default — before systemctl start vpn-bot):
python deploy/check-nonroot-helper-mode.py --mode pre-startIn 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.
Post-start mode (after systemctl start vpn-bot):
python deploy/check-nonroot-helper-mode.py --mode post-startIn post-start mode, /run/vpn-bot must exist and be writable by vpn-bot. Absence is a failure.
What the checker validates (Package 5D + Package 7):
vpn-bot.servicecontainsUser=vpn-bot,Group=vpn-bot,RuntimeDirectory=vpn-bot,RuntimeDirectoryMode=0700,ProtectSystem=strictvpn-bot.servicedoes not containUser=root,Group=root,NoNewPrivileges=true/etc/sudoers.d/vpnbotis root:root 0440, grants only the 4 fixed helpers, no broad grants (NOPASSWD: ALL,ALL=(ALL))- Helper binaries are root:root 0755
/opt/vpn-service,.venv,deployare not writable byvpn-bot/run/vpn-botexistence and writability (mode-dependent).envis not world-readable and is readable byvpn-bot- SQLite
PRAGMA quick_check - Xray config syntax test (
xray run -test -config) - AWG config strip (
awg-quick strip) - MTProxy managed files readable and structurally valid JSON
sudo -n <helper> statuscalls succeed (verifies sudoers grants work end-to-end)systemctl is-activefor:vpn-bot,xray,awg-quick@awg0,danted,mtproxy
Admin diagnostics in the bot (on-demand):
Open the admin panel in Telegram → Диагностика backend. This runs a live read-only health check and shows:
Diagnostics OK
2026-05-12 10:30:00 UTC
✓ Non-root OK (uid=1001)
✓ PRIVILEGE_HELPERS_ENABLED=true
✓ Xray: OK
✓ AWG: OK
✓ SOCKS5: OK
✓ MTProto: OK
✓ SQLite PRAGMA quick_check: ok
✓ vpn-bot: active
✓ xray: active
✓ awg-quick@awg0: active
...
Overall status is OK / WARNING / DEGRADED / FAILED. Secrets, tokens, private keys, and raw hex values are never shown — only the sanitised status and reason.
Expected sudo log entries:
When PRIVILEGE_HELPERS_ENABLED=true, every privileged operation (Xray/AWG config apply, SOCKS5 user create/delete, MTProto secret apply) produces a sudo log entry like:
vpn-bot : TTY=... ; PWD=... ; USER=root ; COMMAND=/usr/local/sbin/vpnbot-xray-apply apply ...
These entries are expected and normal. They confirm the least-privilege model is working correctly.
Signs that require rollback:
FAIL: ... User=rootin checker output — the service is configured to run as rootFAIL: ... NOPASSWD: ALL— broad sudo grant is presentFAIL: ... writable by vpn-boton code/venv/deploy directories- SQLite
PRAGMA quick_checkreturns anything other thanok - Bot starts, issues one key, but Xray/AWG service is immediately DEGRADED with a config apply error
sudo -n <helper> statusreturns permission errors — sudoers file is incorrect- Any helper binary not root:root 0755 — must be fixed before the bot can use them
If rollback is needed, see the "Rollback after a bad deploy" section below.
Back up at least these files before deploys, migrations, and manual backend edits:
sudo install -m 700 -d /root/vpn-service-backups
sudo tar --xattrs --acls -czf /root/vpn-service-backups/vpn-service-$(date -u +%Y%m%dT%H%M%SZ).tar.gz \
/opt/vpn-service/.env \
/opt/vpn-service/data/vpn.db \
/usr/local/etc/xray/config.json \
/etc/amnezia/amneziawg/awg0.conf \
/etc/mtproxy
sudo chmod 600 /root/vpn-service-backups/vpn-service-*.tar.gzInclude /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.
sudo systemctl stop vpn-bot
sudo tar -xzf /root/vpn-service-backups/<backup>.tar.gz -C /
sudo xray run -test -config /usr/local/etc/xray/config.json
sudo awg-quick strip /etc/amnezia/amneziawg/awg0.conf >/dev/null
cd /opt/vpn-service
sudo install -o vpn-bot -g vpn-bot -m 0700 -d /opt/vpn-service/data /opt/vpn-service/logs
sudo chown -R vpn-bot:vpn-bot /opt/vpn-service/data /opt/vpn-service/logs
python deploy/check-nonroot-helper-mode.py
sudo systemctl start vpn-bot
sudo systemctl status vpn-bot
sudo journalctl -u vpn-bot -n 100 --no-pagerIf 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.
- Keep SSH open only from trusted sources where possible.
- Open the public Xray TCP port, usually
443/tcp. - Open the public AWG endpoint UDP port from
AWG_ENDPOINT_PORTor the AWG configListenPort. - Open Dante/SOCKS only if a separate proxy is intentionally deployed and protected.
- Keep
XRAY_STATS_SERVERbound to localhost only, for example127.0.0.1:<port>. Never expose the Xray stats API to the internet. - If UFW default routed policy is
deny, explicitly allow routed traffic required by AWG clients.
Example read-only checks:
sudo ufw status verbose
sudo ss -tulnpsudo systemctl status vpn-bot --no-pager
sudo systemctl status xray --no-pager
sudo systemctl status danted --no-pager
sudo ss -tlnp | grep 31337
sudo systemctl status mtproxy --no-pager
sudo ss -tlnp | grep 8443
sudo journalctl -u vpn-bot -n 100 --no-pager
sudo xray run -test -config /usr/local/etc/xray/config.json
sudo awg show
sudo awg-quick strip /etc/amnezia/amneziawg/awg0.conf >/dev/null
sqlite3 /opt/vpn-service/data/vpn.db "PRAGMA quick_check; SELECT status, key_type, COUNT(*) FROM vpn_keys GROUP BY status, key_type;"If 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.
Xray DEGRADED blocks only Xray create/revoke/delete/manual reconcile. AWG, SOCKS5, and MTProto continue unless separately degraded.
sudo systemctl status xray --no-pager
sudo xray run -test -config /usr/local/etc/xray/config.json
sudo jq '[.inbounds[]?.settings.clients[]? | {email}]' /usr/local/etc/xray/config.json
sqlite3 /opt/vpn-service/data/vpn.db "SELECT status, key_type, COUNT(*) FROM vpn_keys WHERE key_type='xray' GROUP BY status, key_type;"
sudo journalctl -u vpn-bot -n 150 --no-pagerCheck 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.
AWG DEGRADED blocks only AWG create/revoke/delete/manual reconcile. Xray, SOCKS5, and MTProto continue unless separately degraded.
sudo systemctl status awg-quick@awg0 --no-pager
sudo awg show
sudo awk '/^# vpnbot key_id=|^PublicKey =|^AllowedIPs =/{print}' /etc/amnezia/amneziawg/awg0.conf
sqlite3 /opt/vpn-service/data/vpn.db "SELECT status, key_type, COUNT(*) FROM vpn_keys WHERE key_type='awg' GROUP BY status, key_type;"
sudo journalctl -u vpn-bot -n 150 --no-pagerDo 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.
SOCKS5 DEGRADED blocks only SOCKS5 issue/revoke/delete. Xray, AWG, and MTProto continue unless separately degraded.
sudo systemctl status danted --no-pager
getent passwd | awk -F: '$1 ~ /^vpn_socks_/ {print $1}'
sqlite3 /opt/vpn-service/data/vpn.db "SELECT status, access_type, COUNT(*) FROM proxy_accesses WHERE access_type='socks5' GROUP BY status, access_type;"
sudo journalctl -u vpn-bot -n 150 --no-pagerCheck 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.
MTProto DEGRADED blocks only MTProto issue/revoke/delete. Xray, AWG, and SOCKS5 continue unless separately degraded.
sudo systemctl status mtproxy --no-pager
sudo jq '{secret_count: (.secrets | length), fingerprints: [.secrets[]?.fingerprint]}' /etc/mtproxy/vpnbot/managed-secrets.json
sqlite3 /opt/vpn-service/data/vpn.db "SELECT status, access_type, COUNT(*) FROM proxy_accesses WHERE access_type='mtproto' GROUP BY status, access_type;"
sudo journalctl -u vpn-bot -n 150 --no-pagerDo 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.
cd /opt/vpn-service
git log --oneline -5
git reset --hard <previous_commit>
.venv/bin/pip install -r requirements.txt -c constraints.txt
.venv/bin/python init_db.py
sudo systemctl restart vpn-bot
sudo journalctl -u vpn-bot -n 100 --no-pagerOnly use git reset --hard when you intentionally discard local code changes on the server. Restore .env, SQLite DB, Xray config, and AWG config from backup if the failed deploy changed runtime state.
On a staging user before production use:
- Create one Xray key, verify it is active in DB and present in Xray config.
- Revoke and delete the Xray key, verify DB/config/runtime no longer allow access.
- Create one AWG key, verify DB,
awg0.conf, andawg showagree. - Revoke and delete the AWG key, verify peer removal from config and runtime.
- Open "Прокси" as an approved test user, issue SOCKS5 after confirmation, and verify the message contains Host, Port, Login, Password, and URL.
- Issue MTProto after confirmation and verify the plain Telegram link appears before the
ddlink. - In
MTPROTO_MODE=managed, issue MTProto for test user A and record only the non-secret fingerprint/count from admin status. - Issue MTProto for test user B and confirm admin status shows two active managed MTProto accesses.
- 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.
- Confirm user B's Telegram MTProto link still works after user A is revoked.
- Simulate a failed apply on staging, for example by temporarily pointing
MTPROTO_SERVICE_NAMEto a failing test unit or stopping the listener check path, then revoke/issue and confirm rollback restores the previous managed secrets/env files andmtproxyreturns to active/listening. - In
MTPROTO_MODE=static, block the user and confirm MTProto is deactivated only in SQLite. - Check that bot logs and audit output do not contain SOCKS5 passwords,
MTPROTO_SECRET, or managed raw MTProto secrets. - Check
systemctl cat mtproxy,systemctl show mtproxy -p User -p Group -p ExecStart -p Environment, andjournalctl -u mtproxy -n 100 --no-pagerfor absence of raw MTProto secrets. - Check managed file permissions:
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 sudo find /etc/mtproxy/vpnbot/backups -maxdepth 2 -printf '%u:%g %m %p\n'
- Send an announcement with approved, pending, and blocked test users; only approved users and superadmins should receive it.
SQLite is used as the local storage backend. By default the database path is:
/opt/vpn-service/data/vpn.db
init_db.py opens the database and applies schema bootstrap/migrations. The bot also bootstraps the database during app creation.
Current schema tables include:
usersaccess_requestsvpn_keysproxy_entriesproxy_accessesaudit_logvpn_key_traffic_stats
Early 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.
MIT License. See LICENSE.