3x-ui-setup

SKILL.md

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:

  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.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:

  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 -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:

  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:

  • 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:

  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.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

```bash
# Создать ключ
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)

```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. Частые команды

```bash
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 (нужен для автоматической передачи пароля)**
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)

  1. NEVER expose panel to internet -- access only via SSH tunnel
  2. NEVER skip firewall configuration -- only open needed ports
  3. ALWAYS save panel credentials -- show them once, clearly
  4. ALWAYS verify connection works before declaring success
  5. Ask before every destructive or irreversible action
  6. ALWAYS generate guide file (Step 22) -- the user's single source of truth
  7. Lock SSH + install fail2ban LAST (Step 22) -- only after SSH key access is verified in BOTH modes
  8. 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
Weekly Installs
6
GitHub Stars
21
First Seen
Feb 22, 2026
Installed on
opencode6
gemini-cli6
github-copilot6
amp6
codex6
kimi-cli6