montana/Node/External-Audit/AUDIT-READINESS.md
2026-05-21 03:44:38 +03:00

81 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Спецификация готовности к внешнему аудиту
Версия: **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. **A1A5, B1B4, C1C5, D1D4, E1E3** — все проверки зелёные (см. `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) |