VPN Server Setup (3x-ui)
Complete setup: fresh VPS from provider → secured server → working VPN with Hiddify client.
Workflow Overview
ЧАСТЬ 1: Настройка сервера Fresh VPS (IP + root + password) → Determine execution mode (remote or local) → Generate SSH key / setup access → Connect as root → Update system → Create non-root user + sudo → Install SSH key → TEST new user login (critical!) → Firewall (ufw) → Kernel hardening → Time sync + packages → Configure local ~/.ssh/config → ✅ Server secured
ЧАСТЬ 2: Установка VPN (3x-ui) → Install 3x-ui panel → Enable BBR (TCP optimization) → Disable ICMP (stealth) → Reality: scanner → create inbound → get link → Install Hiddify client → Verify connection → Generate guide file (credentials + instructions) → Install fail2ban + lock SSH (after key verified) → ✅ VPN working
PART 1: Server Hardening
Secure a fresh server from provider credentials to production-ready state.
Step 0: Collect Information
First, determine execution mode:
Где запущен Claude Code?
-
На локальном компьютере (Remote mode) -- настраиваем удалённый сервер через SSH
-
На самом сервере (Local mode) -- настраиваем этот же сервер напрямую
Remote Mode -- ASK the user for:
-
Server IP -- from provider email
-
Root password -- from provider email
-
Desired username -- for the new non-root account
-
Server nickname -- for SSH config (e.g., myserver , vpn1 )
-
Has domain? -- if unsure, recommend "no" (Reality path, simpler)
-
Domain name (if yes to #5) -- must already point to server IP
Local Mode -- ASK the user for:
-
Desired username -- for the new non-root account
-
Server nickname -- for future SSH access from user's computer (e.g., myserver , vpn1 )
-
Has domain? -- if unsure, recommend "no" (Reality path, simpler)
-
Domain name (if yes to #3) -- must already point to server IP
In Local mode, get server IP automatically:
curl -4 -s ifconfig.me
If user pastes the full provider email, extract the data from it.
Recommend Reality (no domain) for beginners. Explain:
-
Reality: works without domain, free, simpler setup, great performance
-
TLS: needs domain purchase (~$10/year), more traditional, allows fallback site
Execution Modes
All commands in this skill are written for Remote mode (via SSH). For Local mode, adapt as follows:
Step Remote Mode (default) Local Mode
Step 1 Generate SSH key on LOCAL machine SKIP -- user creates key on laptop later (Step 22)
Step 2 ssh root@{SERVER_IP}
Already on server. If not root: sudo su -
Steps 3-4 Run on server via root SSH Run directly (already on server)
Step 5 Install local public key on server SKIP -- user sends .pub via SCP later (Step 22)
Step 6 SSH test from LOCAL: ssh -i ... user@IP
Switch user: su - {username} , then sudo whoami
Step 7 SKIP -- lockdown deferred to Step 22 SKIP -- lockdown deferred to Step 22
Steps 8-11 sudo on server via SSH sudo directly (no SSH prefix)
Step 12 Write ~/.ssh/config on LOCAL SKIP -- user does this from guide file (Step 22)
Step 13 Verify via ssh {nickname}
Run audit directly, skip SSH lockdown checks
Part 2 ssh {nickname} "sudo ..."
sudo ... directly (no SSH prefix)
Step 17A Scanner via ssh {nickname} '...'
Scanner runs directly (no SSH wrapper) -- see Step 17A for both commands
Panel access Via SSH tunnel Direct: https://127.0.0.1:{panel_port}/{web_base_path}
Step 22 Generate guide + fail2ban + lock SSH Generate guide → SCP download → SSH key setup → fail2ban + lock SSH
IMPORTANT: In both modes, the end result is the same -- user has SSH key access to the server from their local computer via ssh {nickname} , password auth disabled, root login disabled.
Step 1: Generate SSH Key (LOCAL)
Run on the user's LOCAL machine BEFORE connecting to the server:
ssh-keygen -t ed25519 -C "{username}@{nickname}" -f ~/.ssh/{nickname}_key -N ""
Save the public key content for later:
cat ~/.ssh/{nickname}_key.pub
Step 2: First Connection as Root
ssh root@{SERVER_IP}
Handling forced password change
Many providers force a password change on first login. Signs:
-
Prompt: "You are required to change your password immediately"
-
Prompt: "Current password:" followed by "New password:"
-
Prompt: "WARNING: Your password has expired"
If this happens:
-
Enter the current (provider) password
-
Enter a new strong temporary password (this is temporary -- SSH keys will replace it)
-
You may be disconnected -- reconnect with the new password
If connection drops after password change -- this is normal. Reconnect:
ssh root@{SERVER_IP}
Step 3: System Update (as root on server)
apt update && DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt upgrade -y
Step 4: Create Non-Root User
useradd -m -s /bin/bash {username} echo "{username}:{GENERATE_STRONG_PASSWORD}" | chpasswd usermod -aG sudo {username}
Generate a strong random password. Tell the user to save it (needed for sudo). Then:
Verify
groups {username}
Step 5: Install SSH Key for New User
mkdir -p /home/{username}/.ssh echo "{PUBLIC_KEY_CONTENT}" > /home/{username}/.ssh/authorized_keys chmod 700 /home/{username}/.ssh chmod 600 /home/{username}/.ssh/authorized_keys chown -R {username}:{username} /home/{username}/.ssh
Step 6: TEST New User Login -- CRITICAL CHECKPOINT
DO NOT proceed without successful test!
Open a NEW connection (keep root session alive):
ssh -i ~/.ssh/{nickname}_key {username}@{SERVER_IP}
Verify sudo works:
sudo whoami
Must output: root
If this fails -- debug permissions, do NOT disable root login:
Check on server as root:
ls -la /home/{username}/.ssh/ cat /home/{username}/.ssh/authorized_keys
Fix ownership:
chown -R {username}:{username} /home/{username}/.ssh
Step 7: Lock Down SSH — DEFERRED
Оба режима: ПРОПУСКАЕМ. Блокировка SSH и установка fail2ban выполняются в самом конце (Step 22), после того как SSH-ключ проверен. Это предотвращает случайную блокировку доступа во время настройки.
Step 8: Firewall
sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow ssh sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw --force enable sudo ufw status
Step 9: fail2ban — DEFERRED
Пропущен. fail2ban устанавливается в конце настройки (Step 22) вместе с блокировкой SSH, чтобы не заблокировать пользователя во время настройки.
Step 10: Kernel Hardening
sudo tee /etc/sysctl.d/99-security.conf << 'EOF' net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.tcp_syncookies = 1 net.ipv4.conf.all.log_martians = 1 net.ipv4.conf.default.log_martians = 1 net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 EOF sudo sysctl -p /etc/sysctl.d/99-security.conf
Step 11: Time Sync + Base Packages
sudo apt install -y chrony curl wget unzip net-tools sudo systemctl enable chrony
Step 12: Configure Local SSH Config
On the user's LOCAL machine:
cat >> ~/.ssh/config << 'EOF'
Host {nickname} HostName {SERVER_IP} User {username} IdentityFile ~/.ssh/{nickname}_key IdentitiesOnly yes EOF
Tell user: Теперь подключайся командой ssh {nickname} -- без пароля и IP.
Step 13: Final Verification
Connect as new user and run quick audit:
ssh {nickname}
Then on server:
sudo ufw status sudo sysctl net.ipv4.conf.all.rp_filter
Expected: ufw active, rp_filter = 1.
Note: SSH lockdown и fail2ban проверяются в конце (Step 22) после подтверждения работы SSH-ключа.
Часть 1 завершена. Базовая настройка сервера готова. Переходим к установке VPN.
PART 2: VPN Installation (3x-ui)
All commands from here use ssh {nickname} -- the shortcut configured in Part 1.
Step 14: Install 3x-ui
3x-ui install script requires root. Run with sudo:
ssh {nickname} "curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh -o /tmp/3x-ui-install.sh && echo 'n' | sudo bash /tmp/3x-ui-install.sh"
The echo 'n' answers "no" to port customization prompt -- a random port and credentials will be generated.
Note: Do NOT use sudo bash <(curl ...) -- process substitution does not work with sudo (file descriptors are not inherited).
IMPORTANT: Capture the output! It contains:
-
Generated username
-
Generated password
-
Panel port
-
Panel web base path
Extract and save these values. Show them to the user:
Данные панели 3x-ui (СОХРАНИ!): Username: {panel_username} Password: {panel_password} Port: {panel_port} Path: {web_base_path} URL: https://127.0.0.1:{panel_port}/{web_base_path} (через SSH-туннель)
Verify 3x-ui is running:
ssh {nickname} "sudo x-ui status"
If not running: ssh {nickname} "sudo x-ui start"
Panel port is NOT opened in firewall intentionally -- access panel only via SSH tunnel for security.
Step 14b: Enable BBR
BBR (Bottleneck Bandwidth and RTT) dramatically improves TCP throughput, especially on lossy links -- critical for VPN performance.
ssh {nickname} 'current=$(sysctl -n net.ipv4.tcp_congestion_control); echo "Current: $current"; if [ "$current" != "bbr" ]; then echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf && echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf && sudo sysctl -p && echo "BBR enabled"; else echo "BBR already active"; fi'
Verify:
ssh {nickname} "sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc"
Expected: net.ipv4.tcp_congestion_control = bbr , net.core.default_qdisc = fq .
Step 15: Disable ICMP (Stealth)
Makes server invisible to ping scans:
ssh {nickname} "sudo sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/' /etc/ufw/before.rules && sudo sed -i 's/-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-forward -p icmp --icmp-type echo-request -j DROP/' /etc/ufw/before.rules && sudo ufw reload"
Verify:
ping -c 2 -W 2 {SERVER_IP}
Expected: no response (timeout).
Step 16: Branch -- Reality or TLS
Path A: VLESS Reality (NO domain needed) -- RECOMMENDED
Go to Step 17A.
Path B: VLESS TLS (domain required)
Go to references/vless-tls.md .
Step 17A: Find Best SNI with Reality Scanner
Scan the server's /24 subnet to find real websites on neighboring IPs that support TLS 1.3, H2 (HTTP/2), and X25519 -- the exact stack Reality needs to mimic a genuine TLS handshake. The found domain becomes the masquerade target (SNI/dest), making VPN traffic indistinguishable from regular HTTPS to a neighboring site on the same hosting.
Why subnet scanning matters:
-
Reality reproduces a real TLS 1.3 handshake with the dest server -- the dest must support TLS 1.3 + H2 + X25519, or Reality won't work
-
RealiTLScanner (from the XTLS project) checks exactly this -- it only outputs servers compatible with Reality
-
DPI sees the SNI in TLS ClientHello and can probe the IP to verify the domain actually lives there
-
Popular domains (microsoft.com, google.com) are often on CDN IPs far from the VPS -- active probing catches this
-
A small unknown site on a neighboring IP (e.g., shop.finn-auto.fi ) is ideal -- nobody filters it, and it's in the same subnet
-
Do NOT manually pick an SNI without the scanner -- a random domain may not support TLS 1.3 or may be on a different IP range
Download and run Reality Scanner against the /24 subnet:
Remote mode (Claude Code on user's laptop):
ssh {nickname} 'ARCH=$(dpkg --print-architecture); case "$ARCH" in amd64) SA="64";; arm64|aarch64) SA="arm64-v8a";; ) SA="$ARCH";; esac && curl -sL "https://github.com/XTLS/RealiTLScanner/releases/latest/download/RealiTLScanner-linux-${SA}" -o /tmp/scanner && chmod +x /tmp/scanner && file /tmp/scanner | grep -q ELF || { echo "ERROR: scanner binary not valid for this architecture"; exit 1; }; MY_IP=$(curl -4 -s ifconfig.me); SUBNET=$(echo $MY_IP | sed "s/.[0-9]$/.0/24/"); echo "Scanning subnet: $SUBNET"; timeout 120 /tmp/scanner --addr "$SUBNET" 2>&1 | head -80'
Local mode (Claude Code on the VPS itself):
ARCH=$(dpkg --print-architecture); case "$ARCH" in amd64) SA="64";; arm64|aarch64) SA="arm64-v8a";; ) SA="$ARCH";; esac && curl -sL "https://github.com/XTLS/RealiTLScanner/releases/latest/download/RealiTLScanner-linux-${SA}" -o /tmp/scanner && chmod +x /tmp/scanner && file /tmp/scanner | grep -q ELF || { echo "ERROR: scanner binary not valid for this architecture"; exit 1; }; MY_IP=$(curl -4 -s ifconfig.me); SUBNET=$(echo $MY_IP | sed "s/.[0-9]$/.0/24/"); echo "Scanning subnet: $SUBNET"; timeout 120 /tmp/scanner --addr "$SUBNET" 2>&1 | head -80
Note: The commands are identical — Local mode simply runs without the ssh {nickname} wrapper since Claude Code is already on the VPS. GitHub releases use non-standard arch names (64 instead of amd64 , arm64-v8a instead of arm64 ). The case block maps them. The file | grep ELF check ensures the download is a real binary, not a 404 HTML page. Timeout is 120s because scanning 254 IPs takes longer than a single IP.
Choosing the best SNI from scan results
Every domain in the scanner output already supports TLS 1.3 + H2 + X25519 (the scanner filters for this). From those results, prefer domains in this order:
-
Small unknown sites on neighboring IPs (e.g., shop.finn-auto.fi , portal.company.de ) -- ideal, not filtered by DPI
-
Regional/niche services (e.g., local hosting panels, small business sites) -- low profile
-
Well-known tech sites (e.g., github.com , twitch.tv ) -- acceptable but less ideal
AVOID these as SNI:
-
www.google.com , www.microsoft.com , googletagmanager.com -- commonly blacklisted by DPI, people in Amnezia chats report these stop working
-
Any domain behind a CDN (Cloudflare, Akamai, Fastly) -- the IP won't match the CDN edge, active probing detects this
-
Domains that resolve to a completely different IP range than the VPS
How to verify a candidate SNI: The scanner output shows which IP responded with which domain. Pick a domain where the responding IP is in the same /24 as the VPS.
If scanner finds nothing or times out -- some providers (e.g., OVH) have sparse subnets. Try scanning a wider range /23 (512 IPs):
Remote mode:
ssh {nickname} 'MY_IP=$(curl -4 -s ifconfig.me); SUBNET=$(echo $MY_IP | sed "s/.[0-9]*$/.0/23/"); timeout 180 /tmp/scanner --addr "$SUBNET" 2>&1 | head -80'
Local mode:
MY_IP=$(curl -4 -s ifconfig.me); SUBNET=$(echo $MY_IP | sed "s/.[0-9]*$/.0/23/"); timeout 180 /tmp/scanner --addr "$SUBNET" 2>&1 | head -80
If still nothing, use www.yahoo.com as a last-resort fallback -- it supports TLS 1.3 and resolves to many IPs globally, and is less commonly filtered than google/microsoft. But always prefer a real neighbor from the scan -- a neighbor is guaranteed to be in the same subnet and verified by the scanner for TLS 1.3 + H2 + X25519 compatibility.
Save the best SNI for the next step.
Step 18A: Create VLESS Reality Inbound via API
Pre-check: Verify port 443 is not occupied by another service (some providers pre-install apache2/nginx):
ssh {nickname} "ss -tlnp | grep ':443 '"
If something is listening on 443, stop and disable it first (e.g., sudo systemctl stop apache2 && sudo systemctl disable apache2 ). Otherwise the VLESS inbound will silently fail to bind.
3x-ui has an API. Since v2.8+, the installer auto-configures SSL, so the panel runs on HTTPS. Use -k to skip certificate verification (self-signed cert on localhost).
First, get session cookie:
ssh {nickname} 'PANEL_PORT={panel_port}; curl -sk -c /tmp/3x-cookie -b /tmp/3x-cookie -X POST "https://127.0.0.1:${PANEL_PORT}/{web_base_path}/login" -H "Content-Type: application/x-www-form-urlencoded" -d "username={panel_username}&password={panel_password}"'
Generate keys for Reality:
ssh {nickname} "sudo /usr/local/x-ui/bin/xray-linux-* x25519"
This outputs two lines: PrivateKey = private key, Password = public key (confusing naming by xray). Save both.
Generate UUID for the client:
ssh {nickname} "sudo /usr/local/x-ui/bin/xray-linux-* uuid"
Generate random Short ID:
ssh {nickname} "openssl rand -hex 8"
Create the inbound:
ssh {nickname} 'PANEL_PORT={panel_port}; curl -sk -c /tmp/3x-cookie -b /tmp/3x-cookie -X POST "https://127.0.0.1:${PANEL_PORT}/{web_base_path}/panel/api/inbounds/add" -H "Content-Type: application/json" -d '"'"'{ "up": 0, "down": 0, "total": 0, "remark": "vless-reality", "enable": true, "expiryTime": 0, "listen": "", "port": 443, "protocol": "vless", "settings": "{"clients":[{"id":"{CLIENT_UUID}","flow":"xtls-rprx-vision","email":"user1","limitIp":0,"totalGB":0,"expiryTime":0,"enable":true}],"decryption":"none","fallbacks":[]}", "streamSettings": "{"network":"tcp","security":"reality","externalProxy":[],"realitySettings":{"show":false,"xver":0,"dest":"{BEST_SNI}:443","serverNames":["{BEST_SNI}"],"privateKey":"{PRIVATE_KEY}","minClient":"","maxClient":"","maxTimediff":0,"shortIds":["{SHORT_ID}"],"settings":{"publicKey":"{PUBLIC_KEY}","fingerprint":"chrome","serverName":"","spiderX":"/"}},"tcpSettings":{"acceptProxyProtocol":false,"header":{"type":"none"}}}", "sniffing": "{"enabled":true,"destOverride":["http","tls","quic","fakedns"],"metadataOnly":false,"routeOnly":false}", "allocate": "{"strategy":"always","refresh":5,"concurrency":3}" }'"'"''
If API approach fails -- tell user to access panel via SSH tunnel (Step 18A-alt).
Step 18A-alt: SSH Tunnel to Panel (manual fallback)
If API fails, user can access panel in browser:
ssh -L {panel_port}:127.0.0.1:{panel_port} {nickname}
Then open in browser: https://127.0.0.1:{panel_port}/{web_base_path} (browser will warn about self-signed cert -- accept it)
Guide user through the UI:
-
Login with generated credentials
-
Inbounds -> Add Inbound
-
Protocol: VLESS
-
Port: 443
-
Security: Reality
-
Client Flow: xtls-rprx-vision
-
Target & SNI: paste the best SNI from scanner
-
Click "Get New Cert" for keys
-
Create
Step 19: Get Connection Link
Get the client connection link from 3x-ui API:
ssh {nickname} 'PANEL_PORT={panel_port}; curl -sk -b /tmp/3x-cookie "https://127.0.0.1:${PANEL_PORT}/{web_base_path}/panel/api/inbounds/list" | python3 -c " import json,sys data = json.load(sys.stdin) for inb in data.get("obj", []): if inb.get("protocol") == "vless": settings = json.loads(inb["settings"]) stream = json.loads(inb["streamSettings"]) client = settings["clients"][0] uuid = client["id"] port = inb["port"] security = stream.get("security", "none") if security == "reality": rs = stream["realitySettings"] sni = rs["serverNames"][0] pbk = rs["settings"]["publicKey"] sid = rs["shortIds"][0] fp = rs["settings"].get("fingerprint", "chrome") flow = client.get("flow", "") link = f"vless://{uuid}@$(curl -4 -s ifconfig.me):{port}?type=tcp&security=reality&pbk={pbk}&fp={fp}&sni={sni}&sid={sid}&spx=%2F&flow={flow}#vless-reality" print(link) break "'
Show the link to the user. This is what they'll paste into Hiddify.
IMPORTANT: Terminal line-wrap fix. Long VLESS links break when copied from terminal. ALWAYS provide the link in TWO formats:
-
The raw link (for reference)
-
A ready-to-copy block with LLM cleanup prompt:
Скопируй всё ниже и вставь в любой LLM (ChatGPT, Claude) чтобы получить чистую ссылку:
Убери все переносы строк и лишние пробелы из этой ссылки, выдай одной строкой:
{VLESS_LINK}
Also save the link to a file for easy access:
ssh {nickname} "echo '{VLESS_LINK}' > ~/vpn-link.txt"
Tell the user: Ссылка также сохранена в файл ~/vpn-link.txt
Cleanup session cookie:
ssh {nickname} "rm -f /tmp/3x-cookie"
Step 20: Guide User -- Install Hiddify Client
Tell the user:
Теперь установи клиент Hiddify на своё устройство:
Android: Google Play -> "Hiddify" или https://github.com/hiddify/hiddify-app/releases iOS: App Store -> "Hiddify" Windows: https://github.com/hiddify/hiddify-app/releases (скачай .exe) macOS: https://github.com/hiddify/hiddify-app/releases (скачай .dmg) Linux: https://github.com/hiddify/hiddify-app/releases (.deb или .AppImage)
После установки:
- Открой Hiddify
- Нажми "+" или "Add Profile"
- Выбери "Add from clipboard" (ссылка уже скопирована)
- Или отсканируй QR-код (я могу его показать)
- Нажми кнопку подключения (большая кнопка в центре)
- Готово! Проверь IP на сайте: https://2ip.ru
Step 21: Verify Connection Works
After user connects via Hiddify, verify:
ssh {nickname} "sudo x-ui status && ss -tlnp | grep -E '443|{panel_port}'"
Step 22: Generate Guide File & Finalize SSH Access
This step generates a comprehensive guide file with all credentials and instructions, then finalizes SSH key-based access.
Remote Mode
22R-1: Generate guide file locally
Use the Write tool to create ~/vpn-{nickname}-guide.md on the user's local machine. Use the Guide File Template below, substituting all {variables} with actual values.
Tell user: Методичка сохранена в ~/vpn-{nickname}-guide.md — там все пароли, доступы и инструкции.
22R-2: Final lockdown — fail2ban + SSH
Verify SSH key access works:
ssh {nickname} "echo 'SSH key access OK'"
If successful, install fail2ban and lock SSH:
ssh {nickname} 'sudo apt install -y fail2ban && sudo tee /etc/fail2ban/jail.local << JAILEOF [DEFAULT] bantime = 1h findtime = 10m maxretry = 5
[sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 24h JAILEOF sudo systemctl enable fail2ban && sudo systemctl restart fail2ban'
ssh {nickname} 'sudo sed -i "s/^#?PermitRootLogin./PermitRootLogin no/" /etc/ssh/sshd_config && sudo sed -i "s/^#?PasswordAuthentication./PasswordAuthentication no/" /etc/ssh/sshd_config && sudo systemctl restart sshd'
Verify lockdown + SSH still works:
ssh {nickname} "grep -E 'PermitRootLogin|PasswordAuthentication' /etc/ssh/sshd_config && sudo systemctl status fail2ban --no-pager -l && echo 'Lockdown OK'"
Local Mode
In Local mode, Claude Code runs on the server. SSH lockdown was skipped (Step 7), so password auth still works. The flow:
22L-1: Generate guide file on server
Use the Write tool to create /home/{username}/vpn-guide.md on the server. Use the Guide File Template below, substituting all {variables} with actual values.
22L-2: User downloads guide via SCP
Tell the user:
Методичка готова! Скачай её на свой компьютер. Открой НОВЫЙ терминал на своём ноутбуке и выполни:
scp {username}@{SERVER_IP}:~/vpn-guide.md ./
Пароль: {sudo_password}
Файл сохранится в текущую папку. Открой его -- там все пароли и инструкции.
Fallback: If SCP doesn't work (Windows without OpenSSH, network issues), show the full guide content directly in chat.
22L-3: User creates SSH key on their laptop
Tell the user:
Теперь создай SSH-ключ на своём компьютере. Есть два варианта:
Вариант А: Следуй инструкциям из раздела "SSH Key Setup" в методичке.
Вариант Б (автоматический): Установи Claude Code на ноутбуке (https://claude.ai/download) и скинь ему файл vpn-guide.md -- он сам всё настроит по инструкциям из раздела "Instructions for Claude Code".
После создания ключа отправь публичный ключ на сервер (следующий шаг).
22L-4: User sends public key to server via SCP
Tell the user:
Отправь публичный ключ на сервер (из терминала на ноутбуке):
scp /.ssh/{nickname}_key.pub {username}@{SERVER_IP}:/
Пароль: {sudo_password}
Wait for user confirmation before proceeding.
22L-5: Install key + verify
mkdir -p /home/{username}/.ssh cat /home/{username}/{nickname}_key.pub >> /home/{username}/.ssh/authorized_keys chmod 700 /home/{username}/.ssh chmod 600 /home/{username}/.ssh/authorized_keys chown -R {username}:{username} /home/{username}/.ssh rm -f /home/{username}/{nickname}_key.pub
Tell user to test from their laptop:
Проверь подключение с ноутбука: ssh -i ~/.ssh/{nickname}_key {username}@{SERVER_IP}
Должно подключиться без пароля.
Wait for user confirmation that SSH key works before proceeding!
22L-6: Final lockdown — fail2ban + SSH
Only after user confirms key-based login works!
Install fail2ban:
sudo apt install -y fail2ban sudo tee /etc/fail2ban/jail.local << 'EOF' [DEFAULT] bantime = 1h findtime = 10m maxretry = 5
[sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 24h EOF sudo systemctl enable fail2ban sudo systemctl restart fail2ban
Lock SSH:
sudo sed -i 's/^#?PermitRootLogin./PermitRootLogin no/' /etc/ssh/sshd_config sudo sed -i 's/^#?PasswordAuthentication./PasswordAuthentication no/' /etc/ssh/sshd_config sudo systemctl restart sshd
Verify:
grep -E "PermitRootLogin|PasswordAuthentication" /etc/ssh/sshd_config sudo systemctl status fail2ban --no-pager
Expected: PermitRootLogin no , PasswordAuthentication no , fail2ban active.
Tell user to verify SSH still works from laptop:
Проверь, что SSH-ключ всё ещё работает: ssh {nickname} Если подключился — всё настроено!
22L-7: User configures SSH config
Tell the user:
Последний шаг! Добавь на ноутбуке в файл ~/.ssh/config:
Host {nickname} HostName {SERVER_IP} User {username} IdentityFile ~/.ssh/{nickname}_key IdentitiesOnly yes
Теперь подключайся просто: ssh {nickname}
22L-8: Delete guide file from server
rm -f /home/{username}/vpn-guide.md
Tell user: Методичка удалена с сервера. Убедись, что она сохранена на твоём компьютере.
Guide File Template
Generate this file using the Write tool, substituting all {variables} with actual values collected during setup.
Методичка VPN-сервера — {nickname}
Дата создания: {current_date}
1. Подключение к серверу
| Параметр | Значение |
|---|---|
| IP | {SERVER_IP} |
| Пользователь | {username} |
| Пароль sudo | {sudo_password} |
| SSH-ключ | ~/.ssh/{nickname}_key |
| Быстрое подключение | ssh {nickname} |
2. Панель 3x-ui
| Параметр | Значение |
|---|---|
| URL | https://127.0.0.1:{panel_port}/{web_base_path} |
| Логин | {panel_username} |
| Пароль | {panel_password} |
Доступ через SSH-туннель:
ssh -L {panel_port}:127.0.0.1:{panel_port} {nickname}
Затем открой: https://127.0.0.1:{panel_port}/{web_base_path}
3. VPN-подключение
| Параметр | Значение |
|---|---|
| Протокол | VLESS Reality |
| Порт | 443 |
| SNI | {best_sni} |
| Клиент | Hiddify |
Ссылка VLESS:
{VLESS_LINK}
4. Настройка SSH-ключа
Если у тебя ещё нет SSH-ключа, следуй инструкциям для своей ОС:
macOS / Linux
# Создать ключ
ssh-keygen -t ed25519 -C "{username}@{nickname}" -f ~/.ssh/{nickname}_key -N ""
# Отправить публичный ключ на сервер
scp ~/.ssh/{nickname}_key.pub {username}@{SERVER_IP}:~/
# Установить права
chmod 600 ~/.ssh/{nickname}_key
# Добавить в SSH-конфиг
cat >> ~/.ssh/config << 'SSHEOF'
Host {nickname}
HostName {SERVER_IP}
User {username}
IdentityFile ~/.ssh/{nickname}_key
IdentitiesOnly yes
SSHEOF
# Проверить подключение
ssh {nickname}
Windows (PowerShell)
# Создать ключ
ssh-keygen -t ed25519 -C "{username}@{nickname}" -f $HOME\.ssh\{nickname}_key -N '""'
# Отправить публичный ключ на сервер
scp $HOME\.ssh\{nickname}_key.pub {username}@{SERVER_IP}:~/
# Добавить в SSH-конфиг
Add-Content $HOME\.ssh\config @"
Host {nickname}
HostName {SERVER_IP}
User {username}
IdentityFile ~/.ssh/{nickname}_key
IdentitiesOnly yes
"@
# Проверить подключение
ssh {nickname}
Примечание: chmod не нужен на Windows. SSH использует ACL автоматически.
5. Частые команды
ssh {nickname} # подключиться к серверу
ssh {nickname} "sudo x-ui status" # статус панели
ssh {nickname} "sudo x-ui restart" # перезапустить панель
ssh {nickname} "sudo x-ui log" # логи
ssh {nickname} "sudo x-ui setting -reset" # сбросить пароль панели
Добавить нового VPN-клиента: Открой панель через SSH-туннель → Inbounds → ... → Add Client → отправь ссылку/QR.
6. Статус безопасности
| Параметр | Статус |
|---|---|
| Вход под root | Отключён |
| Вход по паролю | Отключён |
| Файрвол UFW | Включён (SSH, 80, 443) |
| fail2ban | Включён (3 попытки → бан 24ч) |
| Усиление ядра | Включено (sysctl) |
| BBR | Включён |
| ICMP (ping) | Отключён |
7. Решение проблем
| Проблема | Решение |
|---|---|
| Connection refused | ssh {nickname} "sudo x-ui status" — перезапусти если остановлен |
| Permission denied (publickey) | Проверь путь и права ключа: ls -la ~/.ssh/{nickname}_key |
| Host key verification failed | ssh-keygen -R {SERVER_IP} и переподключись |
| Панель недоступна | Используй SSH-туннель (см. раздел 2) |
| VPN не подключается | Неверный SNI или сервер лежит — проверь sudo x-ui log |
| Забыл пароль панели | ssh {nickname} "sudo x-ui setting -reset" |
8. Инструкции для Claude Code
Этот раздел для автоматической настройки через Claude Code на твоём ноутбуке. Скопируй всё ниже и вставь в Claude Code на ноутбуке.
ЗАДАЧА: Настроить SSH-ключ и завершить настройку безопасности VPN-сервера
Данные сервера:
- SERVER_IP: {SERVER_IP}
- USERNAME: {username}
- PASSWORD: {sudo_password}
- NICKNAME: {nickname}
Шаги:
Часть А: Установка sshpass (нужен для автоматической передачи пароля)
- Определи ОС и установи sshpass:
- macOS:
brew install hudochenkov/sshpass/sshpass - Linux (Debian/Ubuntu):
sudo apt install -y sshpass
- macOS:
Часть Б: Создание и установка SSH-ключа
2. Создай SSH-ключ: ssh-keygen -t ed25519 -C "{username}@{nickname}" -f ~/.ssh/{nickname}_key -N ""
3. Установи ключ на сервере одной командой:
sshpass -p '{sudo_password}' ssh-copy-id -i ~/.ssh/{nickname}_key.pub -o StrictHostKeyChecking=accept-new {username}@{SERVER_IP}
- Проверь вход по ключу (должен работать БЕЗ пароля):
ssh -i ~/.ssh/{nickname}_key {username}@{SERVER_IP} "echo 'SSH key OK'" - Добавь SSH-конфиг:
cat >> ~/.ssh/config << 'EOF' Host {nickname} HostName {SERVER_IP} User {username} IdentityFile ~/.ssh/{nickname}_key IdentitiesOnly yes EOF - Проверь подключение через конфиг:
ssh {nickname} "echo 'Config OK'"
Часть В: Финальная защита сервера (fail2ban + блокировка пароля) 7. Установи fail2ban:
ssh {nickname} 'sudo apt install -y fail2ban && sudo tee /etc/fail2ban/jail.local << JAILEOF
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 24h
JAILEOF
sudo systemctl enable fail2ban && sudo systemctl restart fail2ban'
- Заблокируй парольный вход и root:
ssh {nickname} 'sudo sed -i "s/^#\?PermitRootLogin.*/PermitRootLogin no/" /etc/ssh/sshd_config && sudo sed -i "s/^#\?PasswordAuthentication.*/PasswordAuthentication no/" /etc/ssh/sshd_config && sudo systemctl restart sshd' - Проверь что SSH-ключ всё ещё работает после блокировки:
ssh {nickname} "grep -E 'PermitRootLogin|PasswordAuthentication' /etc/ssh/sshd_config && sudo systemctl status fail2ban --no-pager && echo 'Сервер полностью защищён!'" - Скажи пользователю: "Готово! SSH-ключ настроен, fail2ban установлен, парольный вход отключён. Подключайся: ssh {nickname}"
Completion Summary
Print this summary for the user:
VPN-сервер полностью настроен и работает!
Подключение к серверу: Команда: ssh {nickname} IP: {SERVER_IP} Пользователь: {username} SSH-ключ: ~/.ssh/{nickname}_key Пароль sudo: {sudo_password}
Безопасность сервера: Root-вход отключён Парольный вход отключён Файрвол включён (порты: SSH, 80, 443) fail2ban защищает от брутфорса Ядро усилено (sysctl) BBR включён (TCP-оптимизация) ICMP отключён (сервер не пингуется)
Панель 3x-ui: URL: https://127.0.0.1:{panel_port}/{web_base_path} (через SSH-туннель) Login: {panel_username} Password: {panel_password}
VPN-подключение: Протокол: VLESS Reality Порт: 443 SNI: {best_sni}
Клиент: Hiddify -- ссылка добавлена
Управление (через SSH): ssh {nickname} # подключиться к серверу ssh {nickname} "sudo x-ui status" # статус панели ssh {nickname} "sudo x-ui restart" # перезапустить панель ssh {nickname} "sudo x-ui log" # логи
SSH-туннель к админке: ssh -L {panel_port}:127.0.0.1:{panel_port} {nickname} Затем открыть: https://127.0.0.1:{panel_port}/{web_base_path}
Добавить нового клиента: Открой админку -> Inbounds -> ... -> Add Client Скинь ссылку или QR-код другому человеку
Методичка: ~/vpn-{nickname}-guide.md Все пароли, инструкции и команды в одном файле
Critical Rules
Part 1 (Server)
-
NEVER skip Step 6 (test login) -- user can be locked out permanently
-
NEVER disable root before confirming new user works
-
NEVER store passwords in files -- only display once to user
-
If connection drops after password change -- reconnect, this is normal
-
If Step 6 fails -- fix it before proceeding, keep root session open
-
Generate SSH key BEFORE first connection -- more efficient workflow
-
All operations after Step 6 use sudo -- not root
-
Steps 7 and 9 are DEFERRED -- SSH lockdown and fail2ban are installed at the very end (Step 22)
Part 2 (VPN)
-
NEVER expose panel to internet -- access only via SSH tunnel
-
NEVER skip firewall configuration -- only open needed ports
-
ALWAYS save panel credentials -- show them once, clearly
-
ALWAYS verify connection works before declaring success
-
Ask before every destructive or irreversible action
-
ALWAYS generate guide file (Step 22) -- the user's single source of truth
-
Lock SSH + install fail2ban LAST (Step 22) -- only after SSH key access is verified in BOTH modes
-
NEVER leave password auth enabled after setup is complete
Troubleshooting
Problem Solution
Connection drops after password change Normal -- reconnect with new password
Permission denied (publickey) Check key path and permissions (700/600)
Host key verification failed ssh-keygen -R {SERVER_IP} then reconnect
x-ui install fails sudo apt update && sudo apt install -y curl tar
Panel not accessible Use SSH tunnel: ssh -L {panel_port}:127.0.0.1:{panel_port} {nickname}
Reality not connecting Wrong SNI -- re-run scanner, try different domain
Hiddify shows error Update Hiddify to latest version, re-add link
"connection refused" Check x-ui is running: sudo x-ui status
Forgot panel password sudo x-ui setting -reset
SCP fails (Windows) Install OpenSSH: Settings → Apps → Optional Features → OpenSSH Client
SCP fails (connection refused) Check UFW allows SSH: sudo ufw status , verify sshd running
BBR not active after reboot Re-check: sysctl net.ipv4.tcp_congestion_control -- re-apply if needed
x-ui CLI Reference
x-ui start # start panel x-ui stop # stop panel x-ui restart # restart panel x-ui status # check status x-ui setting -reset # reset username/password x-ui log # view logs x-ui cert # manage SSL certificates x-ui update # update to latest version