140 lines
5.9 KiB
Markdown
140 lines
5.9 KiB
Markdown
|
|
# Эксплуатационный runbook
|
|||
|
|
|
|||
|
|
Версия: **2026-05-18**
|
|||
|
|
|
|||
|
|
## Добавление нового VPN-узла
|
|||
|
|
|
|||
|
|
### 1. Подготовка сервера
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# Минимум Ubuntu 22.04+, root, IPv4 публичный, открытые :22 :443 :8444
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Получить секреты у admin
|
|||
|
|
|
|||
|
|
- `VPN_PRIVKEY` — Reality privateKey (общий для всех узлов)
|
|||
|
|
- `TOKEN` — admin token orchestrator
|
|||
|
|
|
|||
|
|
Передача — через зашифрованный канал (encrypted age/gpg, signal, encrypted email). НЕ через github/telegram cleartext.
|
|||
|
|
|
|||
|
|
### 3. Запустить join.sh
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ROLE=vpn-backend \
|
|||
|
|
TOKEN=<admin_token> \
|
|||
|
|
VPN_PRIVKEY=<reality_privatekey> \
|
|||
|
|
ALIAS=cologne LABEL=Köln COUNTRY=DE HOSTING=Hetzner \
|
|||
|
|
COORDS="50.94,6.96" \
|
|||
|
|
bash <(curl -sL https://hub.montana.quest/efir369999/montana/raw/branch/main/Node/join.sh)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Скрипт:
|
|||
|
|
1. ставит зависимости, fail2ban
|
|||
|
|
2. ufw: 22, 80, 443, 8444 → ALLOW; остальное DENY
|
|||
|
|
3. ставит pinned версию xray, dedicated user `xray:xray`
|
|||
|
|
4. пишет config.json с UUID/PBK/SID = универсальными, privateKey из env (никогда в скрипт)
|
|||
|
|
5. drop-in `Restart=always StartLimitBurst=10`
|
|||
|
|
6. `ExecStopPost=/usr/local/bin/montana-vpn-deregister` — при штатной остановке узла дёргает `/vpn/node/deregister`
|
|||
|
|
7. ставит montana-node :8444 (TimeChain p2p)
|
|||
|
|
8. POST `https://montana.quest/vpn/node/register` с admin TOKEN → CF добавляет IP в multi-A
|
|||
|
|
|
|||
|
|
### 4. Проверка
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ssh new-node 'systemctl is-active xray fail2ban montana-node'
|
|||
|
|
curl https://montana.quest/vpn/node/pool | jq '.records[].ip' # должен видеть наш IP
|
|||
|
|
openssl s_client -connect <new-node-ip>:443 -servername www.googletagmanager.com -tls1_3 </dev/null 2>&1 | grep Verification
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Удаление узла
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 1. На самом узле:
|
|||
|
|
systemctl stop xray # триггерит ExecStopPost → /deregister
|
|||
|
|
|
|||
|
|
# 2. Если узел уже мёртв — admin вручную:
|
|||
|
|
TOKEN=$(security find-generic-password -s montana-orchestrator-admin -a token -w)
|
|||
|
|
curl -X POST -H 'Content-Type: application/json' \
|
|||
|
|
https://montana.quest/vpn/node/deregister \
|
|||
|
|
--data "{\"ip\":\"<dead-ip>\",\"secret\":\"$TOKEN\"}"
|
|||
|
|
|
|||
|
|
# 3. Если orchestrator недоступен — напрямую через CF API:
|
|||
|
|
CF=$(security find-generic-password -s cloudflare-api-token -a montana-quest -w)
|
|||
|
|
ZONE=2bc47161267258960d48bedfdf476f1a
|
|||
|
|
REC=$(curl -sH "Authorization: Bearer $CF" \
|
|||
|
|
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records?type=A&name=cdn.montana.quest" \
|
|||
|
|
| jq -r '.result[] | select(.content=="<dead-ip>") | .id')
|
|||
|
|
curl -X DELETE -H "Authorization: Bearer $CF" \
|
|||
|
|
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records/$REC"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Ротация ключей Reality
|
|||
|
|
|
|||
|
|
Когда нужно: подозрение на утечку privateKey, plan-rotation каждые 6 месяцев.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 1. Сгенерировать новый keypair (на любом узле):
|
|||
|
|
ssh montana-finland 'xray x25519'
|
|||
|
|
# → PrivateKey: <NEW_PRIV>
|
|||
|
|
# Password: <NEW_PBK> (этот идёт в URL как pbk=)
|
|||
|
|
# Hash32: ...
|
|||
|
|
|
|||
|
|
# 2. Сгенерировать новый shortId:
|
|||
|
|
openssl rand -hex 8 # → <NEW_SID>
|
|||
|
|
|
|||
|
|
# 3. Раскатать NEW_PRIV/SID на каждый узел одной командой:
|
|||
|
|
for n in montana-finland montana-frankfurt montana-us; do
|
|||
|
|
ssh $n "python3 -c \"
|
|||
|
|
import json
|
|||
|
|
p='/usr/local/etc/xray/config.json'
|
|||
|
|
c=json.load(open(p))
|
|||
|
|
ib=c['inbounds'][0]
|
|||
|
|
ib['streamSettings']['realitySettings']['privateKey']='<NEW_PRIV>'
|
|||
|
|
ib['streamSettings']['realitySettings']['shortIds']=['<NEW_SID>']
|
|||
|
|
json.dump(c,open(p,'w'),indent=2)\" && systemctl restart xray"
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# 4. Обновить /vpn/sub (отдаётся с Moscow :5008) — поменять pbk= и sid= в источнике подписки.
|
|||
|
|
|
|||
|
|
# 5. Уведомить пользователей — у них в Happ профиль больше не работает (другой pbk).
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Стоимость ротации: все клиенты должны переустановить профиль. Делать только при необходимости.
|
|||
|
|
|
|||
|
|
## Падение orchestrator
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ssh montana-moscow 'systemctl status montana-orchestrator'
|
|||
|
|
# если crashed:
|
|||
|
|
ssh montana-moscow 'journalctl -u montana-orchestrator -n 50 --no-pager'
|
|||
|
|
ssh montana-moscow 'systemctl restart montana-orchestrator'
|
|||
|
|
|
|||
|
|
# multi-A остаётся как есть → существующие подключения продолжают работать
|
|||
|
|
# (deg-graceful: только новых узлов нельзя зарегистрировать пока orch down)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Падение CF API
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# DNS остаётся как есть (CF resolvers независимы от API).
|
|||
|
|
# Невозможно add/remove IP. Существующий pool работает.
|
|||
|
|
# При длительном outage — ждать восстановления CF.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Доступ к секретам
|
|||
|
|
|
|||
|
|
| Секрет | Где |
|
|||
|
|
|---|---|
|
|||
|
|
| Reality privateKey | macOS Keychain `montana-vpn-privkey` (служба), и `/etc/montana/vpn-privkey` (0600) на узле |
|
|||
|
|
| Admin token | macOS Keychain `montana-orchestrator-admin` / `token`; `/etc/montana/orchestrator-admin-token` (0600) на Moscow |
|
|||
|
|
| CF API token | macOS Keychain `cloudflare-api-token` / `montana-quest`; `/etc/montana/cf-api-token` (0600) на Moscow |
|
|||
|
|
| SSH ключи к узлам | macOS Keychain (через ssh-add) + `~/.ssh/montana-*` (0600) |
|
|||
|
|
|
|||
|
|
## Аптайм-критерии
|
|||
|
|
|
|||
|
|
- xray на узле: `Restart=always`, StartLimitBurst=10/5min — выживает все аварии кроме битого конфига
|
|||
|
|
- orchestrator: `Restart=on-failure`, RestartSec=10
|
|||
|
|
- fail2ban: `Restart=on-failure`
|
|||
|
|
- nginx (Moscow): `Restart=on-failure`
|
|||
|
|
- Все critical units `enabled` — переживают reboot
|