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.
- Optional proxy entry display seeded from
DEFAULT_PROXY_*environment variables. The bot does not install or manage Dante by itself. - Ownership checks so users can only view and manage their own keys unless they are admins.
- 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
- 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 # systemd unit template
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.lock
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=
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.DEFAULT_PROXY_*seeds one proxy entry only when the proxy table is empty.
The supplied systemd unit expects the project in /opt/vpn-service. If you deploy elsewhere, update deploy/vpn-bot.service before installing it.
Short order:
- Clone the repository.
- Create a virtual environment.
- Install
requirements.txt. - Copy
.env.exampleto.env. - Fill
.env. - Initialize the database.
- Run the bot manually.
- Install the systemd service.
sudo mkdir -p /opt/vpn-service
sudo chown -R "$USER":"$USER" /opt/vpn-service
git clone https://github.com/Egor051/vpnbot.git /opt/vpn-service
cd /opt/vpn-service
python3 -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt -c constraints.txt
cp .env.example .env
nano .env
python init_db.py
python main.pyInstall and start the systemd service:
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-botUpdate from GitHub:
cd /opt/vpn-service
git pull --ff-only
.venv/bin/pip install -r requirements.txt -c constraints.txt
.venv/bin/python init_db.py
sudo systemctl restart vpn-botCheck 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 systemd unit is installed and matches the paths in
.env. - 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 and
.venvare not writable by untrusted users.
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
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
. /opt/vpn-service/.venv/bin/activate
cd /opt/vpn-service
python init_db.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 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.
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.
- Block an approved test user and confirm bot access is denied even if a backend revoke error is simulated on staging.
- 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_entriesaudit_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.