# Спецификация готовности к внешнему аудиту Версия: **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 :443 -servername ` → `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) |