81 lines
7.1 KiB
Markdown
81 lines
7.1 KiB
Markdown
# Спецификация готовности к внешнему аудиту
|
||
|
||
Версия: **2026-05-18**
|
||
|
||
## Что мы гарантируем (must hold)
|
||
|
||
### A. Архитектурные инварианты
|
||
|
||
| # | Инвариант | Как проверить |
|
||
|---|---|---|
|
||
| A1 | На каждом VPN-узле один inbound `xray vless+reality :443`, без посредников (нет haproxy, нет xray-pinned cascade) | `ss -tlnp \| grep :443` → один процесс xray; `systemctl is-active haproxy` → inactive |
|
||
| A2 | Все VPN-узлы используют один и тот же Reality keypair (privateKey, publicKey, shortId) и один UUID | TLS-probe с тем же `pbk` к каждому IP проходит handshake → `Verification: OK` |
|
||
| A3 | `cdn.montana.quest` resolves в multi-A pool из живых VPN-узлов, TTL ≤ 120s, не proxied через CF | `dig @1.1.1.1 cdn.montana.quest A` показывает несколько IP; TTL ≤ 120 |
|
||
| A4 | `/vpn/sub` отдаёт ровно одну vless-ссылку с `cdn.montana.quest:443` | `curl /vpn/sub \| base64 -d` — одна строка `vless://…` |
|
||
| A5 | Клиент с одним профилем подключается к любому из IP pool без переустановки профиля | iPhone Happ → connect; toggle airplane → reconnect; в xray access.log видно соединение |
|
||
|
||
### B. Самовосстановление
|
||
|
||
| # | Инвариант | Как проверить |
|
||
|---|---|---|
|
||
| B1 | При отказе узла его IP выпадает из multi-A не позже чем за **2 минуты** | E2E test: `systemctl stop xray` на узле → `/vpn/node/pool` через 60-90s не содержит IP |
|
||
| B2 | При восстановлении узла IP возвращается в multi-A не позже чем за **2 минуты** | Обратный E2E |
|
||
| B3 | Watchdog запускается автоматически при старте orchestrator; падение Flask → systemd restart | `systemctl is-enabled montana-orchestrator` = enabled; `Restart=on-failure` в unit |
|
||
| B4 | xray на узле имеет `Restart=always` с `StartLimitBurst=10/300s` (защита от крэш-цикла) | `cat /etc/systemd/system/xray.service.d/30-autorestart.conf` |
|
||
|
||
### C. Секреты
|
||
|
||
| # | Инвариант | Как проверить |
|
||
|---|---|---|
|
||
| C1 | Reality `privateKey` отсутствует в **публичных** артефактах: git, hub.montana.quest, sub-endpoint, `/vpn/`, `/net/` карта | `grep -r 'cL7D6FCqH5' Montana/ \| grep -v External-Audit \| grep -v memory` ⇒ 0 совпадений (allowed только в Memory/internal) |
|
||
| C2 | На узле `privateKey` лежит в `/usr/local/etc/xray/config.json` с правами `600 root:root` | `ls -la /usr/local/etc/xray/config.json` |
|
||
| C3 | Cloudflare API token хранится только в macOS Keychain (`cloudflare-api-token`) и в `/etc/montana/cf-api-token` (0600) на orchestrator-узле | `ls -la /etc/montana/cf-api-token` |
|
||
| C4 | Admin secret для `/register` `/deregister` — random 32-byte hex, в Keychain `montana-orchestrator-admin` и `/etc/montana/orchestrator-admin-token` (0600) | то же |
|
||
| C5 | `join.sh` не содержит секретов; `VPN_PRIVKEY` передаётся через env или защищённый файл | `grep -E 'privateKey|PRIV=' Montana/Node/join.sh` ⇒ только переменная |
|
||
|
||
### D. Минимальные привилегии и изоляция
|
||
|
||
| # | Инвариант | Как проверить |
|
||
|---|---|---|
|
||
| D1 | xray работает под dedicated user `xray:xray`, не `nobody`/`root` | `ps -ef \| grep [x]ray run` — owner = xray |
|
||
| D2 | На узле ufw default deny incoming; открыты только 22, 80, 443, 8444 (p2p) | `ufw status verbose` |
|
||
| D3 | fail2ban активен с jail для sshd (maxretry=3, bantime≥1h) | `systemctl is-active fail2ban && fail2ban-client status sshd` |
|
||
| D4 | orchestrator слушает только 127.0.0.1:5020, наружу — через nginx | `ss -tlnp \| grep :5020` — bound to 127.0.0.1 |
|
||
|
||
### E. Производственное качество кода
|
||
|
||
| # | Инвариант | Как проверить |
|
||
|---|---|---|
|
||
| E1 | `xray` версия pinned (XRAY_PIN) в join.sh | `grep XRAY_PIN Montana/Node/join.sh` |
|
||
| E2 | Все systemd-units включены (`enabled`) | `systemctl is-enabled xray montana-orchestrator fail2ban` |
|
||
| E3 | Reality `serverNames` указывает на реальный публичный сайт с валидной CA-цепочкой | `openssl s_client -connect <IP>:443 -servername <SNI>` → `Verification: OK` |
|
||
|
||
## Что мы НЕ гарантируем (out of scope)
|
||
|
||
| # | Не гарантия | Причина |
|
||
|---|---|---|
|
||
| N1 | Защита от компрометации единого privateKey | Reality по дизайну требует shared keypair для нескольких backends. Mitigation в [OPERATIONS.md#rotation](OPERATIONS.md#ротация-ключей-reality) |
|
||
| N2 | Защита от блокировки SNI `www.googletagmanager.com` | Один SNI = single point. В roadmap — SNI pool (см. OPEN-RISKS.md R-1) |
|
||
| N3 | Защита от DDoS на узлы | Опирается на анти-DDoS хостинга. Mitigation: BGP-уровень провайдера |
|
||
| N4 | Защита от компрометации Cloudflare API token | Token — единственная точка управления DNS. Mitigation: scoped permissions, rotation |
|
||
| N5 | Анонимность пользователя относительно exit-IP | Узлы видят destination, как любой VPN |
|
||
| N6 | Защита от атак на montana-node p2p :8444 (Montana TimeChain) | Отдельный аудит TimeChain protocol (`Montana-Protocol/Code/AUDIT.md`) |
|
||
| N7 | Защита от компрометации хостинга (image dump) | Дано на уровне договора с провайдером, не на уровне кода |
|
||
|
||
## Критерии «mainnet-ready» (must pass)
|
||
|
||
1. **A1–A5, B1–B4, C1–C5, D1–D4, E1–E3** — все проверки зелёные (см. `scripts/audit-probe.sh`).
|
||
2. **E2E auto-prune** при отказе узла занимает ≤ 120 секунд (B1).
|
||
3. **E2E auto-restore** при возврате узла занимает ≤ 120 секунд (B2).
|
||
4. **0 секретов** утекло в публичные артефакты (C1).
|
||
5. **Все P0/P1 риски** из [OPEN-RISKS.md](OPEN-RISKS.md) закрыты или явно приняты с обоснованием.
|
||
|
||
## История проверки
|
||
|
||
| Дата | Что проверено | Результат |
|
||
|---|---|---|
|
||
| 2026-05-18 | E2E auto-prune US (down) | ✅ 2 секунды |
|
||
| 2026-05-18 | E2E auto-restore US (up) | ✅ 29 секунд |
|
||
| 2026-05-18 | Reality probe на всех 3 IP | ✅ Verification: OK |
|
||
| 2026-05-18 | C1 grep на privateKey в публичных артефактах | ✅ 0 (см. scripts/check-leaked-secrets.sh) |
|