WEB-306 · Дефект · Инфраструктура · web
Облачный фаервол OCI открыт всему миру: ingress 0.0.0.0/0 на SSH/80/443, NSG не заведено ни одной
В работе
P1 · важно
ведёт: backup-opus
Суть
1. Суть одной фразой: SSH-доступ к OCI был 0.0.0.0/0 — сужен до 86.45.55.126/32, но остался origin-lock и NSG.
2. Где мы сейчас (13.09.2026): SSH уже сужен до 86.45.55.126/32 (не 0.0.0.0/0); остаток: NSG=0 + origin-lock WEB-075 не применён.
3. Хронология: сужение SSH сделано; миграционное окно WEB-032/075 — решение владельца; 05.09 → parked.
4. Карта документов и кода: WEB-075 (origin-lock); WEB-032 (миграция); NSG OCI.
5. Остаток: владелец — назначить окно миграции WEB-032/075; волна — применить origin-lock + NSG в окне.
6. Критерий закрытия: NSG закрыт + origin-lock применён, живой пруф.
---
## история (тело до 13.09.2026)
## Найдено 21.08 при снятии расписок гейта 13 (WEB-081)
`VCN nc-vcn / Default Security List for nc-vcn`:
| ingress источник | протокол | порт | stateless |
|---|---|---|---|
| **`0.0.0.0/0`** | TCP | **22 (SSH)** | нет |
| `0.0.0.0/0` | TCP | 80 | нет |
| `0.0.0.0/0` | TCP | 443 | нет |
| `0.0.0.0/0` | ICMP | — | нет |
**Network Security Groups: не создано ни одной.**
## Почему это важно
На облачном уровне SSH открыт всему интернету. Вся защита держится **исключительно на хосте** — ufw, ssh-policy, fail2ban, применённые гейтом 12 (`GATE12-REPORT.md`, 418 строк расписок). Хостовой слой закрыт, облачный — нет.
Это ровно наш выстраданный класс «**починка одного слоя ≠ защищённая система**»: мы честно закрыли хост, отчитались, и не посмотрели на слой выше. Найдено только потому, что полезли за совсем другим — за размером диска.
Смежное: порты 80/443 открытыми быть **обязаны**, и именно поэтому важен гейт 7 (origin-lock) — его конфиги готовы волной `gate78` и **до сих пор не применены** (WEB-075).
## Что делать (изменение конфигурации — нужно решение владельца)
1. Сузить ingress на **22** до известных адресов, либо перевести доступ через bastion (в т.ч. OCI Bastion service).
2. Завести хотя бы одну **NSG**, чтобы правила не жили только в default security list — default-список наследуется всем, что появится в VCN потом.
3. Применить origin-lock гейта 7 на 80/443 (WEB-075).
4. **Негативная проба после применения**: с постороннего адреса SSH обязан **не** отвечать; с разрешённого — отвечать. Без пробы пункт не закрывается.
⚠️ Не применять вслепую: сузив 22 неверно, можно потерять доступ к A1. Порядок — сначала проверить, с каких адресов мы реально ходим (ноутбук, M1, Pi), потом правило, потом проба со стороннего адреса, и только потом убирать широкое правило.
Расписки: `~/Downloads/GATE13-OCI-TRUTH-20260821.md` sha `06e92c2a7c3462d75eef00682257f425b21229f5044070716f7e896726021c94`, сырьё `GATE13-OCI-TRUTH-RAW-20260821.json` sha `2844ab8045f670f2e57017b8748866277d264e303d8503276b9c789bbf99b7ab`. Разложено на M4 `/Users/milamarty/work/` и A1 `/home/ubuntu/waves/`.
## 21.08 — РЕШЕНИЕ ВЛАДЕЛЬЦА: СУЖАТЬ ПОСЛЕ ПОСАДКИ ТРИДЦАТОЙ
Владелец (Telegram, 21.08): на вопрос «сейчас или после посадки» — **«после посадки»**. До посадки правила периметра не трогаем.
Порядок, который согласован и от которого не отступать (неверное правило = мы сами теряем доступ к A1):
1. выяснить, с каких адресов мы **реально** ходим (ноутбук, M1, Pi) — по факту, а не по памяти;
2. добавить узкое правило;
3. **проба со стороннего адреса**: SSH обязан не отвечать оттуда и отвечать с разрешённого;
4. только после успешной пробы убрать широкое правило `0.0.0.0/0` на 22.
Пункт 3 обязателен: без негативной пробы правило считается непроверенным.
## 21.08 04:00 — ⭐ СУЖЕНО И ПРОВЕРЕНО НЕГАТИВНЫМ ТЕСТОМ (решение владельца «после посадки» выполнено)
### Сначала выяснили, откуда мы реально ходим — фактами, а не по памяти
| источник | адрес |
|---|---|
| ноутбук / M1 / M4 | `86.45.55.126` |
| **Pi** | `86.45.55.126` — **тот же**, Pi за тем же NAT |
| журнал SSH на A1 за 3 суток | **796 принятых входов, все с `86.45.55.126`**, других источников нет |
Отсюда список доступа — ровно один адрес, `/32`.
### Изменение делалось поэтапно, доступ проверялся между шагами
1. **добавлено узкое правило**, широкое оставлено → SSH жив;
2. **широкое правило на 22 снято** → SSH жив уже на узком (это и доказывает, что несёт именно оно);
3. **негативный тест**: источник временно подменён на `192.0.2.1/32` (TEST-NET-1, RFC 5737, заведомо не наш) → **доступа нет**;
4. наш адрес возвращён → **доступ восстановлен**.
Без шага 3 правило считалось бы непроверенным: наличие правила и его работа — разные утверждения.
### Итоговые ingress-правила
```
86.45.55.126/32 TCP 22 SSH — только наш адрес
0.0.0.0/0 TCP 80 HTTP — публичный по назначению
0.0.0.0/0 TCP 443 HTTPS — публичный по назначению
0.0.0.0/0 ICMP диагностика
```
### Чем это делалось и как откатить
Инструменты на ноутбуке (там же единственные креды `~/.oci`): `oci_fw_read.py` (чтение) и `oci_fw_set.py broad|both|narrow|dummy` (запись). **Аварийный возврат — `oci_fw_set.py broad`**, он не зависит от SSH к A1: правится через API OCI, поэтому потеря SSH не блокирует восстановление. Это и был запас прочности всей операции.
### ⚠️ Остаточные риски, названные честно
1. **Адрес динамический** (домашний). Если провайдер его сменит, мы потеряем SSH к A1 до тех пор, пока не выполним `oci_fw_set.py broad` или не впишем новый адрес. Путь восстановления существует и проверен, но о нём надо помнить.
2. **NSG по-прежнему ни одной.** Правила живут только в default security list, который наследуется всем, что появится в VCN потом. Пункт 2 тикета **не закрыт** — оставлен отдельной работой.
3. **80/443 остаются публичными** — так и должно быть, но именно поэтому важен гейт 7 origin-lock (WEB-075): его конфиги готовы волной `gate78` и **всё ещё не применены**.
4. Проверка «отказ с постороннего адреса» сделана подменой правила, **а не с реально чужой машины** — своей внешней точки у нас нет, все наши источники за одним NAT. Эквивалентность обоснована: правило адресное, и подмена меняет ровно ту переменную, которую проверяем.
## 23.08 board-triage (волна btriage, отчёт BOARD-TRIAGE-REPORT.md на A1; применено координатором)
Запарковано как STALE: superseded: SSH уже 86.45.55.126/32, не 0.0.0.0/0; residual NSG review — отдельным тикетом при необходимости.
## 2026-08-23 11:20 UTC — разбор парковки (по запросу owner): почему здесь и что известно
Безопасность OCI (A1): ingress 0.0.0.0/0 на SSH/80/443, NSG нет — тело подробное (5464 зн.), НЕ пустышка. Припарковано до окна миграции WEB-032/WEB-075 (origin-lock): менять фаервол под живыми волнами опасно (22.08 уже ловили UFW LIMIT + внешние power events, память a1-full-disk-refuses-ssh). Брать ВМЕСТЕ с origin-lock окном owner. Смежное: OCI-креды на ноутбуке (a1-oci-api-resize-from-laptop).
Доказательства
WASH3340-RECEIVED:306
/Users/annakorin/nc-ops-scripts/board-wash-20260910/3340WASHEXTERNAL-REPORT.md
Bundle SHA256 109076e36b8b4f3a8575f11933f8f1632e3638a124b95e1e409f0046129dfcae
Лента
2026-09-10T18:55:03.774Z · coordinatorBOARD-WASH-20260910:WAVE-3340
По поручению владельца 18:43Z назначена исполнительская волна 3340 (external), Codex gpt-5.6-luna xhigh, M4. Полная история карточки и база l115c 1ad52e16b доставлены, brief-guard и проверка Git-базы пройдены. Задача: проверить существующую сдачу, устранить остатки, передать бандл и доказательства. Финальная приёмка и посадка остаются за координатором. Ранее отложенная функция не включается; статус parked/backlog сохранён, идёт проверка технического остатка.
КАРТА ДОКУМЕНТОВ: ноут /Users/annakorin/nc-ops-scripts/board-wash-20260910/3340-wash-external-brief.md; M4 /Users/milamarty/waves/inputs/board-wash-20260910/external-tickets.json; ожидаемый отчёт /Users/milamarty/waves/3340WASHEXTERNAL-REPORT.md. Правила обогащения: WEB-449.
2026-09-10T19:06:33.561Z · coordinatorWASH3340-RECEIVED:306
Получена и прочитана сдача 3340. GO относится только к объёму исполнителя. Бандл сохранён на ноуте и SHA256 проверен. Исправление d182e853a (два файла disclosure/тест) ещё НЕ принято координатором и НЕ посажено. Исторические внешние статусы ниже не перепроверены живьём.
OCI firewall / origin lock
### Критерии
1. OCI Security List no longer exposes SSH to `0.0.0.0/0`; allowed source set is verified.
2. At least one NSG exists and does not rely only on inherited Default Security List.
3. WEB-075 origin-lock is applied for public 80/443 where appropriate.
4. A real foreign-source negative probe rejects SSH, while an allowed source still works.
### Факты / коммит
Ticket created `2026-08-21T01:43:56.214Z`, status `parked`, priority P1. Initial body recorded VCN `nc-vcn`, Default Security List ingress `0.0.0.0/0` for TCP 22/80/443 and ICMP, with no NSG. It correctly distinguishes host-layer ufw/ssh-policy from cloud-layer exposure and points to WEB-075 origin-lock.
The later owner decision was to narrow after the thirtieth deployment. The ticket’s later evolution reports a narrow SSH rule for `86.45.55.126/32`, broad SSH removed, a dummy `192.0.2.1/32` negative substitution refused and the prior rule restored. The same evolution explicitly leaves dynamic home-IP risk, no NSG, public 80/443, and WEB-075 origin-lock as residuals; the negative was not a real foreign machine probe.
The cited truth artifacts are absent here: `/Users/milamarty/Downloads/GATE13-OCI-TRUTH-20260821.md` and `/Users/milamarty/Downloads/GATE13-OCI-TRUTH-RAW-20260821.json`. The `oci_fw_read.py` / `oci_fw_set.py` tools are described as laptop tools, not source files in this worktree. No OCI CLI/API, SSH, production or external infrastructure operation was performed. No source SHA is claimed.
### Остаток
Obtain a current OCI snapshot, create/apply NSG and WEB-075 origin-lock through the coordinator’s infrastructure process, then run a genuine foreign-source negative and allowed-source positive test. Do not reapply the historical narrow rule blindly.
### KNOWN ISSUES / ТРАБЛШУТИНГ
- Симптом: cloud layer can be wider than host layer; SSH may appear safe from ufw logs while OCI still permits world ingress. Two-minute check after artifacts arrive: inspect the raw JSON with `jq` for Security List ingress and NSGs, then compare with current public IP allowlist. No live read was run here.
- Root: OCI VCN Security List/NSG configuration, not an application source file.
- Treatment: coordinator performs snapshot-backed change, preserves rollback, verifies 22/80/443 separately, and records a true foreign-source negative. Missing raw paths above are a blocker for safe execution.
### ДЛЯ ТИКЕТА
Evolution: `2026-08-21` discovery and owner decision to defer narrowing; subsequent owner-reported narrow SSH and dummy-rule negative; later triage marks SSH portion stale/done but NSG and origin-lock separately parked behind WEB-032/WEB-075 migration. Paths: ticket history and canon above; external truth paths `/Users/milamarty/Downloads/GATE13-OCI-TRUTH-20260821.md`, `/Users/milamarty/Downloads/GATE13-OCI-TRUTH-RAW-20260821.json`; referenced laptop scripts `oci_fw_read.py`, `oci_fw_set.py`; related repository source only contains unrelated host egress-fence material and was not substituted for OCI truth.
2026-09-13T11:01:35.774Z · coordinator[13.09 11:01Z координатор] Мойка отложенных, волна 3668. Вердикт: ЖДЁТ-ВЛАДЕЛЬЦА. SSH сужен до /32; остался NSG=0 + origin-lock WEB-075; окно миграции — решение владельца.
2026-09-13T16:51:05.795Z · coordinator[13.09 16:51Z координатор] Живая сверка облачного слоя, сделана координатором прямо сейчас через OCI API (подписанный запрос, тенант прода, `securityLists` по VCN `nc-vcn`). Тело тикета описывало состояние на 21.08 — оно УСТАРЕЛО наполовину, и это важно знать, чтобы никто не чинил уже починенное.
ЧТО УЖЕ ЗАКРЫТО: **SSH на 0.0.0.0/0 больше НЕТ.** Порт 22 разрешён ровно двум адресам, и у правил стоят описания:
- `86.45.55.126/32` TCP 22 — «SSH — только наш адрес (WEB-306…)»
- `129.80.41.210/32` TCP 22 — «a2 node tunnel»
ЧТО ОТКРЫТО МИРУ СЕЙЧАС (и почему): 80/443 — публичные по назначению; ICMP — диагностика; SIP `5060` TCP+UDP и `5074` TCP+UDP, RTP `10000-10020` UDP и `13200-13300` UDP — телефония, она обязана быть достижима извне. Egress: `0.0.0.0/0` all. Network Security Groups по-прежнему не создано ни одной — их роль тут выполняет список безопасности VCN.
ЧТО НЕ ЗАКРЫТО И ЯВЛЯЕТСЯ НАСТОЯЩИМ ОСТАТКОМ ТИКЕТА: **origin-lock не применён.** Проверено пробой:
```
curl -H "Host: sixbyy.com" http://129.213.25.105/ → 301
curl -k -H "Host: sixbyy.com" https://129.213.25.105/ → 200
curl http://129.213.25.105/ (без Host) → 301
```
`sixbyy.com` и `app.sixbyy.com` резолвятся в Cloudflare (104.21.16.129 / 172.67.212.182), но сам origin отвечает 200 по прямому IP. Значит, кто знает адрес машины, обходит Cloudflare целиком — вместе с WAF, лимитами и защитой от ботов. Это ровно наш класс «починка одного слоя ≠ защищённая система»: хостовой слой закрыт, облачный по SSH закрыт, а обход фронта остался.
ДВЕ ОПАСНОСТИ, которые надо снять ДО того, как сужать 80/443 до диапазонов Cloudflare (иначе починка security-дыры уронит прод):
1. **Выпуск/продление сертификата.** Если сертификат на origin выпускается через ACME HTTP-01, порт 80 обязан оставаться открытым миру, иначе продление молча перестанет работать и через 90 дней сайт ляжет. Проверить способ выпуска ДО сужения.
2. **Проверка посадки `public 200`.** Шаг проверки в ранбуке посадки обращается к публичному адресу; если он ходит на прямой IP, а не через Cloudflare, сужение сломает проверку каждой посадки.
План (координатор, без владельца — по его постоянному «делай сам всё, что нужно для живучести»): (1) установить способ выпуска сертификата и адрес, который дёргает проверка посадки; (2) добавить правила 443 с источниками = актуальные диапазоны Cloudflare, СОХРАНИВ существующее правило `0.0.0.0/0` на 443; (3) проверить, что сайт жив через Cloudflare; (4) только после этого удалить правило `0.0.0.0/0` на 443 и повторить пробу прямого IP — ожидается отказ соединения; (5) 80 трогать в последнюю очередь и только если ACME не зависит от него. Откат на каждом шаге — одно правило обратно.
2026-09-22T18:32:37.605Z · triage-neoРЕШЕНИЕ=parked
ОСНОВАНИЕ=2026-09-22: сверка координатора 2026-09-13 — SSH сужен, но NSG=0 и origin-lock WEB-075 не применён; прямой origin отвечает 200.
ЧТО НУЖНО=в окне миграции создать NSG, применить WEB-075 origin-lock и подтвердить разрешённый плюс настоящий foreign-source negative probe.
triage-neo 4616
2026-09-23T12:50:51.123Z · triage-neoРЕШЕНИЕ=parked
ОСНОВАНИЕ=2026-09-13, живая сверка: SSH сужен до /32, но NSG=0 и origin-lock WEB-075 не применён; прямой origin отвечает 200, движения 7+ дней нет.
ЧТО НУЖНО=ПРЕДЛОЖЕНИЕ=split; первый шаг: проверить ACME и public-200 путь перед NSG/origin-lock; triage-neo 4713
2026-09-27T15:30:32.048Z · coordinator[27.09 15:30Z координатор] Взят в работу по слову владельца 27.09 15:28Z. Первый шаг: read-only сверка ingress OCI (security lists/NSG), какие порты реально нужны (ACME, 443 через Cloudflare, SSH /32), затем план сужения с откатом до применения.
2026-09-27T15:33:50.021Z · coordinator[27.09 15:33Z координатор] Сверка 27.09 (read-only, OCI SDK с ноутбука + A1). Снимок до: nc-ops-scripts/oci-sl-before-20260927.json.
Сделано: удалено временное правило 3610-3611 от A2 (замер 14.09, на A1 никто не слушает; владелец тогда просил вернуть как было). Прод 200, SSH ок. Правил 12→11.
Нашёл:
1) ГЛАВНОЕ: прод отвечает напрямую по IP 129.213.25.105 в обход Cloudflare (https app.sixbyy.com --resolve на origin = 200). 80/443 открыты миру в OCI и ufw. WAF/лимиты Cloudflare обходятся.
2) Мешают простому замку на диапазоны Cloudflare: nb.wool2.online и dev.wool2.online смотрят прямо на origin (не через CF); сертификат sip.sixbyy.com продлевается webroot по 80 напрямую; sixbyy-сертификаты через nginx-authenticator (HTTP-01 идёт через CF, это ок).
3) Открыты миру без слушателя: 5074 tcp/udp и 13200-13300 udp (эксперимент SIP) — в OCI и в ufw.
4) SIP 5060/5061 и RTP 10000-10020 открыты миру — нужны софтфонам, оставить (защита на уровне Asterisk).
План (ждёт слова владельца, с откатом из снимка):
а) закрыть 5074 и 13200-13300 в OCI и ufw;
б) на origin пускать 443 только с диапазонов Cloudflare для vhost sixbyy (nginx allow/deny по реальному адресу соединения), wool2 и sip не трогать; 80 оставить для ACME;
в) вариант шире: увести wool2 за Cloudflare, затем замок 443 уже в OCI.
Рекомендую а+б сейчас.
2026-09-27T15:44:06.258Z · coordinator[27.09 15:44Z координатор] [27.09 15:50Z координатор] Сделано по слову владельца 15:39 («делай, как говоришь»). Полный след для нулевого агента.
СИМПТОМ/НАХОДКА: прод A1 (129.213.25.105) отвечал на 443 напрямую по IP в обход Cloudflare: `curl -sk --resolve app.sixbyy.com:443:129.213.25.105 https://app.sixbyy.com/` = 200 с любого адреса. Значит WAF, rate-limit и защита от DDoS в Cloudflare обходились.
КАК ИСКАЛ: OCI SDK с ноутбука (venv /private/tmp/claude-501/ocisdk, запуск из run/), ingress Default Security List nc-vcn; на A1 `ss -ltnu`, `ufw status`; DNS; выборка ss established :443 за 60 с — только адреса CF (172.71.x, 104.23.x); grep IP origin по скриптам флота — только SSH (A2 туннели a1-tunnel*, M1 nightly git fetch), на 443 не влияет.
ЧТО СДЕЛАНО (все с откатом):
1) OCI: снимок до `nc-ops-scripts/oci-sl-before-20260927.json`. Удалены: 3610-3611 от A2 (временное с 14.09), 5074 tcp/udp и 13200-13300 udp (эксперимент SIP, слушателя нет). 12→8 правил.
2) ufw A1: удалены allow IN 5074/tcp, 5074/udp, 13200:13300/udp (v4+v6). Бэкап /etc/ufw/user*.rules.pre-web306-20260927. ALLOW OUT на эти порты оставлены (исходящие, безвредны).
3) nginx A1: бэкап всего /etc/nginx в /root/nginx-pre-web306-20260927. Новый /etc/nginx/conf.d/cf-only-geo.conf: geo по $realip_remote_addr (адрес TCP-соединения, НЕ заголовок CF-Connecting-IP — его можно подделать) → $nc_from_cf; разрешены диапазоны CF (из cloudflare-real-ip.conf), localhost, 10.0.0.0/16, владелец 86.45.55.126, A2 129.80.41.210. /etc/nginx/cf-only-guard.inc: `if ($nc_from_cf = 0) { return 444; }`, подключён сразу после listen 443 в app.sixbyy.com, sixbyy.com(+www), dev.sixbyy.com. nginx -t ок, reload.
НЕ ТРОНУТО и почему: nb/dev.wool2.online смотрят прямо на origin (не через CF) — замок их сломал бы; sip.sixbyy.com — только 80 для ACME webroot; 80 открыт всем (ACME, редирект на https); SIP 5060/5061 и RTP 10000-10020 нужны софтфонам. Замок на уровне OCI невозможен, пока wool2 не за CF.
ПРОВЕРКА: app/sixbyy через CF 200, nb.wool2 301 (как было); напрямую с адреса владельца 200 (в allowlist, ожидаемо); внешняя проверка из не-allowlist — см. следующий коммент.
ОТКАТ: убрать строку `include /etc/nginx/cf-only-guard.inc;` из трёх vhost (или cp -a из /root/nginx-pre-web306-20260927) + nginx -t + reload; OCI — восстановить правила из json-снимка.
РИСК НА БУДУЩЕЕ: список диапазонов CF меняется редко, но меняется. При обновлении cloudflare-real-ip.conf пересобрать cf-only-geo.conf (та же выборка set_real_ip_from). Если A2/владелец сменят IP — прямой доступ пропадёт, через CF будет работать.
ОСТАЛОСЬ: а) решение по wool2 (увести за CF → тогда можно замок в OCI); б) автообновление диапазонов CF.
Воркер
не проверен
3340-wash-external
M4
движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
Обновлён
2026-09-27T15:30:32.351Z