VPN Server Setup (3x-ui)
name: 3x-ui-setup
by davydenkovm · published 2026-03-22
$ claw add gh:davydenkovm/davydenkovm-3x-ui-vpn-setup---
name: 3x-ui-setup
description: Complete VPN server setup from scratch. Takes a fresh VPS (IP + root + password from hosting provider) through full server hardening and 3x-ui (Xray proxy panel) installation with VLESS Reality or VLESS TLS. Guides user through connecting via Hiddify client. Use when user mentions v2ray, xray, vless, 3x-ui, proxy server, vpn server, or wants to set up encrypted proxy access on a VPS. Designed for beginners — hand-holds through every step.
allowed-tools: Bash,Read,Write,Edit
---
# 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 -- ASK the user for:
1. **Server IP** -- from provider email
2. **Root password** -- from provider email
3. **Desired username** -- for the new non-root account
4. **Server nickname** -- for SSH config (e.g., `myserver`, `vpn1`)
5. **Has domain?** -- if unsure, recommend "no" (Reality path, simpler)
6. **Domain name** (if yes to #5) -- must already point to server IP
Local Mode -- ASK the user for:
1. **Desired username** -- for the new non-root account
2. **Server nickname** -- for future SSH access from user's computer (e.g., `myserver`, `vpn1`)
3. **Has domain?** -- if unsure, recommend "no" (Reality path, simpler)
4. **Domain name** (if yes to #3) -- must already point to server IP
In Local mode, get server IP automatically:
curl -4 -s ifconfig.meIf user pastes the full provider email, extract the data from it.
**Recommend Reality (no domain) for beginners.** Explain:
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.pubStep 2: First Connection as Root
ssh root@{SERVER_IP}Handling forced password change
Many providers force a password change on first login. Signs:
If this happens:
1. Enter the current (provider) password
2. Enter a new strong temporary password (this is temporary -- SSH keys will replace it)
3. 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 -yStep 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}/.sshStep 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}/.sshStep 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 statusStep 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.confStep 11: Time Sync + Base Packages
sudo apt install -y chrony curl wget unzip net-tools
sudo systemctl enable chronyStep 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
EOFTell 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_filterExpected: 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:
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:**
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:
1. **Small unknown sites on neighboring IPs** (e.g., `shop.finn-auto.fi`, `portal.company.de`) -- ideal, not filtered by DPI
2. **Regional/niche services** (e.g., local hosting panels, small business sites) -- low profile
3. **Well-known tech sites** (e.g., `github.com`, `twitch.tv`) -- acceptable but less ideal
**AVOID** these as SNI:
**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 -80If 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:
1. Login with generated credentials
2. Inbounds -> Add Inbound
3. Protocol: VLESS
4. Port: 443
5. Security: Reality
6. Client Flow: xtls-rprx-vision
7. Target & SNI: paste the best SNI from scanner
8. Click "Get New Cert" for keys
9. 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:
1. The raw link (for reference)
2. 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)
После установки:
1. Открой Hiddify
2. Нажми "+" или "Add Profile"
3. Выбери "Add from clipboard" (ссылка уже скопирована)
4. Или отсканируй QR-код (я могу его показать)
5. Нажми кнопку подключения (большая кнопка в центре)
6. Готово! Проверь IP на сайте: https://2ip.ruStep 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.pubTell 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 fail2banLock 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 sshdVerify:
grep -E "PermitRootLogin|PasswordAuthentication" /etc/ssh/sshd_config
sudo systemctl status fail2ban --no-pagerExpected: `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.mdTell user: **Методичка удалена с сервера. Убедись, что она сохранена на твоём компьютере.**
---
Guide File Template
Generate this file using the **Write tool**, substituting all `{variables}` with actual values collected during setup.
~~~markdown
# Методичка 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-сервера**
Данные сервера:
Шаги:
**Часть А: Установка sshpass (нужен для автоматической передачи пароля)**
1. Определи ОС и установи sshpass:
- macOS: `brew install hudochenkov/sshpass/sshpass`
- Linux (Debian/Ubuntu): `sudo apt install -y sshpass`
**Часть Б: Создание и установка 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}
```
4. Проверь вход по ключу (должен работать БЕЗ пароля):
```
ssh -i ~/.ssh/{nickname}_key {username}@{SERVER_IP} "echo 'SSH key OK'"
```
5. Добавь SSH-конфиг:
```
cat >> ~/.ssh/config << 'EOF'
Host {nickname}
HostName {SERVER_IP}
User {username}
IdentityFile ~/.ssh/{nickname}_key
IdentitiesOnly yes
EOF
```
6. Проверь подключение через конфиг: `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'
```
8. Заблокируй парольный вход и 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'
```
9. Проверь что SSH-ключ всё ещё работает после блокировки:
```
ssh {nickname} "grep -E 'PermitRootLogin|PasswordAuthentication' /etc/ssh/sshd_config && sudo systemctl status fail2ban --no-pager && echo 'Сервер полностью защищён!'"
```
10. Скажи пользователю: "Готово! 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)
1. **NEVER skip Step 6** (test login) -- user can be locked out permanently
2. **NEVER disable root before confirming new user works**
3. **NEVER store passwords in files** -- only display once to user
4. **If connection drops** after password change -- reconnect, this is normal
5. **If Step 6 fails** -- fix it before proceeding, keep root session open
6. **Generate SSH key BEFORE first connection** -- more efficient workflow
7. **All operations after Step 6 use sudo** -- not root
8. **Steps 7 and 9 are DEFERRED** -- SSH lockdown and fail2ban are installed at the very end (Step 22)
Part 2 (VPN)
9. **NEVER expose panel to internet** -- access only via SSH tunnel
10. **NEVER skip firewall configuration** -- only open needed ports
11. **ALWAYS save panel credentials** -- show them once, clearly
12. **ALWAYS verify connection works** before declaring success
13. **Ask before every destructive or irreversible action**
14. **ALWAYS generate guide file** (Step 22) -- the user's single source of truth
15. **Lock SSH + install fail2ban LAST** (Step 22) -- only after SSH key access is verified in BOTH modes
16. **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 versionMore tools from the same signal band
Order food/drinks (点餐) on an Android device paired as an OpenClaw node. Uses in-app menu and cart; add goods, view cart, submit order (demo, no real payment).
Sign plugins, rotate agent credentials without losing identity, and publicly attest to plugin behavior with verifiable claims and authenticated transfers.
The philosophical layer for AI agents. Maps behavior to Spinoza's 48 affects, calculates persistence scores, and generates geometric self-reports. Give your...