145 lines
6.5 KiB
Markdown
145 lines
6.5 KiB
Markdown
|
|
# Incident response — VPN-сеть Montana
|
|||
|
|
|
|||
|
|
Версия: **2026-05-18**
|
|||
|
|
|
|||
|
|
## Severity матрица
|
|||
|
|
|
|||
|
|
| Sev | Симптом | Reaction time |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **S1** | ВСЕ узлы в pool недоступны (нет VPN ни у кого) | < 15 минут |
|
|||
|
|
| **S2** | Один из узлов недоступен > 5 минут после автоматического prune не отрабатывает | < 30 минут |
|
|||
|
|
| **S3** | Утечка любого секрета подтверждена | < 1 час, по ротации |
|
|||
|
|
| **S4** | DPI начал блокировать наш Reality в одной стране | < 4 часа, переключение SNI |
|
|||
|
|
| **S5** | Падение orchestrator | < 1 час (multi-A продолжает работать) |
|
|||
|
|
|
|||
|
|
## Playbook
|
|||
|
|
|
|||
|
|
### S1: вся сеть лежит
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 1. Проверить multi-A:
|
|||
|
|
dig @1.1.1.1 cdn.montana.quest A
|
|||
|
|
# если пусто или мало → CF API упал, либо watchdog ошибся и удалил всё
|
|||
|
|
|
|||
|
|
# 2. Прямой probe каждого узла:
|
|||
|
|
for n in 91.132.142.42 89.19.208.158 86.104.72.12; do
|
|||
|
|
echo "$n: $(timeout 4 openssl s_client -connect $n:443 -servername www.googletagmanager.com -tls1_3 </dev/null 2>&1 | grep -c 'Verification: OK')"
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# 3. Если узлы живы, а pool пуст — force re-register:
|
|||
|
|
TOKEN=$(security find-generic-password -s montana-orchestrator-admin -a token -w)
|
|||
|
|
for spec in "91.132.142.42|helsinki|FI" "89.19.208.158|frankfurt|DE" "86.104.72.12|newyork|US"; do
|
|||
|
|
ip=$(echo $spec|cut -d'|' -f1); alias=$(echo $spec|cut -d'|' -f2); c=$(echo $spec|cut -d'|' -f3)
|
|||
|
|
curl -sX POST -H 'Content-Type: application/json' \
|
|||
|
|
https://montana.quest/vpn/node/register \
|
|||
|
|
--data "{\"ip\":\"$ip\",\"alias\":\"$alias\",\"country\":\"$c\",\"hosting\":\"x\",\"label\":\"$alias\",\"coords\":[0,0],\"secret\":\"$TOKEN\"}"
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# 4. Если узлы тоже мертвы — каскад инфраструктурного фейла:
|
|||
|
|
# a) Проверить статус хостинга (Timeweb dashboard / THE.Hosting)
|
|||
|
|
# b) Если хостинг ОК — SSH к узлу, проверить xray
|
|||
|
|
ssh montana-finland 'systemctl status xray && tail -50 /var/log/xray/error.log'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### S2: один узел мёртв > 5 мин, не выпал
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 1. Проверить watchdog работает:
|
|||
|
|
ssh montana-moscow 'cat /var/lib/montana-orchestrator/pool-health.json | jq'
|
|||
|
|
|
|||
|
|
# 2. Проверить probe от Moscow к узлу:
|
|||
|
|
ssh montana-moscow 'python3 -c "import ssl,socket;
|
|||
|
|
ctx=ssl.create_default_context()
|
|||
|
|
s=socket.create_connection((\"<dead-ip>\",443),timeout=5)
|
|||
|
|
ws=ctx.wrap_socket(s,server_hostname=\"www.googletagmanager.com\")
|
|||
|
|
print(ws.getpeercert())"'
|
|||
|
|
|
|||
|
|
# Если timeout — но узел живой → проверить ufw/CrowdSec на узле
|
|||
|
|
ssh <dead-node> 'cscli decisions list 2>/dev/null | grep 176.124.208.93'
|
|||
|
|
|
|||
|
|
# 3. Если watchdog не работает — restart orchestrator:
|
|||
|
|
ssh montana-moscow 'systemctl restart montana-orchestrator'
|
|||
|
|
|
|||
|
|
# 4. Ручное удаление IP:
|
|||
|
|
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\"}"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### S3: утечка секрета
|
|||
|
|
|
|||
|
|
#### S3a: утечка Reality privateKey
|
|||
|
|
|
|||
|
|
1. Ввести **ускоренную ротацию** (см. OPERATIONS.md → "Ротация ключей Reality").
|
|||
|
|
2. Уведомить пользователей через канал коммуникации (Telegram, sitemontana.quest баннер).
|
|||
|
|
3. Заблокировать старый pbk (xray на узле не примет старый клиент после смены privateKey).
|
|||
|
|
|
|||
|
|
#### S3b: утечка admin TOKEN
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# 1. Сгенерировать новый:
|
|||
|
|
NEW=$(openssl rand -hex 32)
|
|||
|
|
# 2. Положить на Moscow:
|
|||
|
|
ssh montana-moscow "install -m 600 /dev/stdin /etc/montana/orchestrator-admin-token <<<'$NEW'"
|
|||
|
|
ssh montana-moscow 'systemctl restart montana-orchestrator'
|
|||
|
|
# 3. Обновить Keychain:
|
|||
|
|
security add-generic-password -s montana-orchestrator-admin -a token -w "$NEW" -U
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### S3c: утечка CF API token
|
|||
|
|
|
|||
|
|
1. Сразу revoke в Cloudflare dashboard.
|
|||
|
|
2. Создать новый scoped token (Zone montana.quest, Permissions: Zone.DNS.Edit).
|
|||
|
|
3. Обновить в Keychain `cloudflare-api-token` и в `/etc/montana/cf-api-token` на Moscow.
|
|||
|
|
4. Restart orchestrator.
|
|||
|
|
|
|||
|
|
### S4: DPI блокировка в стране
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# Временное решение: сменить SNI в realitySettings на другой популярный домен.
|
|||
|
|
# Все узлы должны иметь тот же SNI (привязан к pbk).
|
|||
|
|
|
|||
|
|
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']['serverNames']=['www.gstatic.com']
|
|||
|
|
ib['streamSettings']['realitySettings']['dest']='www.gstatic.com:443'
|
|||
|
|
json.dump(c,open(p,'w'),indent=2)\" && systemctl restart xray"
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Обновить /vpn/sub: в URL sni=www.gstatic.com
|
|||
|
|
ssh montana-moscow 'sed -i "s/www.googletagmanager.com/www.gstatic.com/g" <path-to-vpn-sub-source>'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### S5: orchestrator упал, не поднимается
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ssh montana-moscow 'journalctl -u montana-orchestrator -n 200 --no-pager'
|
|||
|
|
# Типичные причины:
|
|||
|
|
# - синтакс ошибка в server.py → откатить из *.bak
|
|||
|
|
ssh montana-moscow 'ls /opt/montana-orchestrator/server.py.bak-*'
|
|||
|
|
# - /etc/montana/cf-api-token нечитаем → chmod 600 root:root
|
|||
|
|
# - Flask недоступен → apt install python3-flask
|
|||
|
|
|
|||
|
|
# Существующие подключения клиентов НЕ затронуты до DNS TTL expire (120s).
|
|||
|
|
# Если orch будет down > 30 минут — manual prune через CF API напрямую.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Эскалация
|
|||
|
|
|
|||
|
|
| Уровень | Кто | Канал |
|
|||
|
|
|---|---|---|
|
|||
|
|
| L1 | Архитектор сети | efir369999@gmail.com |
|
|||
|
|
| L2 | Cloudflare support (DNS issues) | https://dash.cloudflare.com → support |
|
|||
|
|
| L3 | Хостинг провайдер | Timeweb / THE.Hosting dashboard |
|
|||
|
|
|
|||
|
|
## После инцидента
|
|||
|
|
|
|||
|
|
1. Записать в `OPEN-RISKS.md` если выявлен новый риск.
|
|||
|
|
2. Если фикс уровня кода — соответствующее изменение в `Montana/Node/join.sh` или orchestrator.
|
|||
|
|
3. Записать дату и причину в `AUDIT-READINESS.md → История проверки`.
|