# nebula-mesh: 23 уязвимости за четыре с половиной месяца


В мае писал про nebula-mesh[^1] — панель управления для Nebula, которую собрал сам. Всего у проекта уже 41 релиз, а в разделе Security Advisories репозитория[^2] появилось 23 опубликованные записи, 16 из них получили номера CVE. Я собираюсь рассказывать о проекте шире, и первым вопросом к панели, которая хранит ключи центра сертификации, будет вопрос о безопасности. Поэтому начну с него сам.

## Цифры

| Критичность | Количество |
| --- | --- |
| Critical | 1 |
| High | 10 |
| Medium | 10 |
| Low | 2 |

По времени записи распределились неравномерно: 12 в мае, 6 в июне, одна в июле, ни одной в августе и четыре в конце сентября. Все исправлены, последние четыре — в версиях 0.17.1 и 0.18.0 от 29 сентября.

Нашли их разные люди. Двенадцать майских прислал один человек, Адам ([ak2k](https://github.com/ak2k)). Три июньские найдены при собственных проверках кода. Остальные восемь сообщили ещё пятеро. Две записи вышли по отчёту Нгуена Хюи Ву Зунга ([dungNHVhust](https://github.com/dungNHVhust)) и Тхай Шон Диня ([sondt99](https://github.com/sondt99)), по одной прислали Адам Джордан ([adamyordan](https://github.com/adamyordan)) и [Pig-Tail](https://github.com/Pig-Tail), четыре сентябрьские нашёл Дэниел Коулз ([manus-pi](https://github.com/manus-pi)).

## Первая волна

Первый релиз вышел 11 мая. Через восемь дней Адам попросил включить закрытый приём сообщений об уязвимостях, и за следующие шесть дней по его отчётам вышло двенадцать записей.

Самая тяжёлая из них получила оценку critical[^3]. API проверял только токен и не проверял, кому принадлежит объект запроса. Оператор с обычным ключом мог выпустить API-ключ для любого другого оператора, в том числе для администратора, и получить полные права. Для центров сертификации проверка была, для хостов, сетей и правил firewall — нет, и в коде об этом прямо говорил комментарий.

Остальное из той же волны — привычный набор для молодого веб-приложения: не было CSRF-токенов, заголовков безопасности и флага `Secure` у cookie, токены регистрации лежали в базе открытым текстом, а через дополнительные параметры хоста можно было подмешать свой YAML в конфиг агента.

История закончилась хорошо: Адам не только сообщал, но и чинил. Сейчас за ним 48 pull request'ов, и он второй по вкладу человек в проекте.

## Отзыв, который не отзывал

Эту запись я считаю самой неприятной, хотя оценка у неё high, а не critical[^4].

В Nebula нет ни CRL, ни OCSP. Отозвать сертификат можно одним способом: внести его отпечаток в список `pki.blocklist` в конфиге каждого узла. В nebula-mesh была кнопка «Заблокировать». Сервер добавлял отпечаток в список и отдавал его агентам при каждом опросе. Агент список получал, разбирал и выбрасывал. Генератор конфига поля `blocklist` вообще не знал.

В панели хост значился заблокированным, в журнале была запись. А в самой сети его сертификат продолжал работать до конца срока: до 30 дней у хоста с агентом и до 365 дней у телефона.

Тесты проекта не поднимали настоящую Nebula и не проверяли, что заблокированный узел действительно теряет связь. Такой тест описан в плане проекта и до сих пор не написан; это один из открытых пунктов в списке готовности к промышленной эксплуатации[^5].

## Одна ошибка, четыре раза

Если разложить записи по типам, видно, что большинство — повторы.

| Класс ошибки | Записей |
| --- | --- |
| Нет проверки, чей это объект | 4 |
| Ключ CA остаётся в памяти после использования | 4 |
| Секрет лежит в базе открытым текстом | 3 |
| Отзыв сертификата не доходит до сети | 3 |

Про ключ CA в памяти сообщали в мае, в июне и дважды в сентябре — каждый раз в новом месте кода. С открытым текстом в базе то же самое: сначала токены регистрации, потом сессии, потом секреты TOTP.

Каждое исправление закрывало найденное место, а не класс ошибок. Следующий отчёт показывал ту же ошибку в соседней функции.

## Что изменилось в проекте

В июле подход поменялся. В репозитории появился документ с инвариантами безопасности[^6]: семь правил, у каждого постоянный идентификатор. Например, `SEC-TENANT-001` требует, чтобы область видимости любого запроса выводилась из того, кто его делает, а не из параметров. Правило проекта такое: изменение, которое затрагивает инвариант, не принимается без теста на отказ со ссылкой на его идентификатор.

Часть правил проверяется автоматически. Тест обходит все маршруты работающего роутера, и новый GET-обработчик без явного решения о том, кому он доступен, роняет сборку.

Что ещё добавилось за это время:

- **Модель угроз**[^7] — активы, точки входа, разбор по STRIDE и риски, которые приняты осознанно.
- **Хранение учётных данных.** API-ключи, сессии, токены и резервные коды лежат в базе в виде HMAC-SHA-256 с ключом, производным от мастер-ключа. Утечка одной только базы не даёт значения, которое примет сервер.
- **Шифрование секретов TOTP** в базе тем же мастер-ключом.
- **Проверки при каждом pull request:** детектор гонок, gosec, govulncheck, CodeQL. По ночам — фаззинг.
- **Симуляция сети** из многих узлов, которая проверяет изоляцию операторов, одноразовость токенов и распространение списка отзыва.

Тестового кода в репозитории теперь почти вдвое больше, чем рабочего: 57 тысяч строк против 31 тысячи.

Отдельно скажу про то, как пишется код. Я разрабатываю nebula-mesh с помощью AI-агентов, и в репозитории лежит файл `AGENTS.md` с правилами для них. Инварианты записаны именно там: агент обязан прочитать их перед тем, как трогать аутентификацию, криптографию или хранилище.

## Что появилось кроме исправлений

За то же время в проекте появились:

- команды резервного копирования и восстановления;
- вебхуки на события жизненного цикла хостов;
- подключение уже работающей сети Nebula без перевыпуска ключей хостов;
- агент как служба Windows и графический установщик;
- правила firewall для отдельного хоста и шлюзы в сети за пределами Nebula;
- ротация CA с периодом, когда узлы доверяют обоим сертификатам.

Правила firewall для отдельного хоста, шлюзы и графический установщик написал Франсиско Хавьер ([inode64](https://github.com/inode64)).

## Где проект сейчас

Статус по-прежнему beta. В репозитории есть документ с условиями, при которых проект можно будет назвать готовым к промышленной эксплуатации[^5]. Из 34 пунктов закрыт один.

Среди открытых — независимый аудит, подпись релизов, тест с настоящей Nebula и то, чего я не могу сделать в одиночку: хотя бы один оператор, кроме меня, который держит на nebula-mesh настоящую сеть и рассказывает, что ломается.

Если вы ведёте Nebula на скриптах и готовы попробовать панель на некритичной сети — напишите в [Discussions](https://github.com/forgekeep/nebula-mesh/discussions) или заведите issue. Если найдёте уязвимость, для этого есть закрытая форма[^8]; судя по истории выше, отчёты там не теряются.

[^1]: [nebula-mesh: панель управления для Nebula, которую я собрал сам](/2026/05/25/nebula-mesh/) — первая статья о проекте.
[^2]: [Security Advisories](https://github.com/forgekeep/nebula-mesh/security/advisories) репозитория forgekeep/nebula-mesh.
[^3]: [GHSA-598g-h2vc-h5vg](https://github.com/forgekeep/nebula-mesh/security/advisories/GHSA-598g-h2vc-h5vg), CVE-2026-47724. Исправлено в 0.3.2.
[^4]: [GHSA-cm26-5974-52h8](https://github.com/forgekeep/nebula-mesh/security/advisories/GHSA-cm26-5974-52h8), CVE-2026-61699. Исправлено в 0.7.1.
[^5]: [Production readiness](https://github.com/forgekeep/nebula-mesh/blob/main/docs/production-readiness.md) — условия и текущее состояние.
[^6]: [Security invariants](https://github.com/forgekeep/nebula-mesh/blob/main/docs/security/invariants.md).
[^7]: [Threat model](https://github.com/forgekeep/nebula-mesh/blob/main/docs/security/threat-model.md).
[^8]: [Сообщить об уязвимости](https://github.com/forgekeep/nebula-mesh/security/advisories/new).

