Любой кластерный сканер рано или поздно обрастает скрытыми уязвимостями (suppressions). Что-то помечается как проблема, команда решает, что пока это приемлемо, и алерт замолкает. Интересный вопрос не в том, можно ли заглушить проверку. Вопрос в том, есть ли у этой тишины срок годности.
Я провел вечер за плагином KubeBuddy для Headlamp, и именно эта фича оказалась достойной отдельного поста.
Что такое KubeBuddy
Это плагин для Headlamp, который прогоняет каталог из 137 проверок (RBAC, безопасность, сеть, рабочие нагрузки, хранилища) по активному кластеру и выдает взвешенную оценку безопасности.
Архитектурное решение, которое имеет значение: в сам кластер ничего не разворачивается. Никаких операторов, CRD или агентов. Плагин оценивает данные ресурсов, которые Headlamp и так считывает с использованием ваших собственных учетных данных. Trivy Operator и Kubescape требуют установки внутрикластерных компонентов, что означает деплой, запрос на расширенные права и, в клиентской среде, долгие согласования.
Для консалтинга это главное преимущество. Я могу провести аудит клиентского кластера, не прося ни у кого разрешения на деплой.
Механизм подавления (suppression)
Три аннотации на ресурсе:
kubebuddy.io/ignore-checks
kubebuddy.io/ignore-reason
kubebuddy.io/ignore-until
В документации утверждается, что ignore-until истекает. Это утверждение стоит проверить, потому что слово “истекает” может означать многое: аннотация игнорируется, ошибка возвращается в список, или же где-то появляется предупреждение, которое никто никогда не читает.
Вот фактический код из бандла плагина, с восстановленными минифицированными именами переменных (ja = ignore-checks, za = ignore-reason, Ga = ignore-until):
// истек ли срок?
function vl(s){
const e = s.trim();
if (!e) return false;
const t = Date.parse(e);
return Number.isFinite(t) && Date.now() > t;
}
// следует ли скрыть эту ошибку?
function bl(s, e){
const t = e.sourceAnnotations || {},
n = String(t[ja] || "").trim();
if (!n || !yl(n, s)) return null; // проверки нет в списке игнорируемых
const i = String(t[Ga] || "").trim();
return i && vl(i)
? null // срок истек, не скрывать
: { reason: String(t[za] || "").trim(), until: i };
}
Из этих пятнадцати строк следуют три вывода:
- Дедлайн сравнивается с реальным временем (wall clock time). Если дата в прошлом, возвращается
null, и ошибка снова считается активной. - Скрытые ошибки не удаляются. Они перемещаются в отдельный массив
suppressedFindings, помеченные какsuppressionReasonиsuppressionUntil. ignore-checksпринимает список, разделенный запятыми, пробелами или точкой с запятой, и*для всех проверок. Регистр не имеет значения.
Проверка на реальном кластере
Чтение кода не доказывает механику. Десять минут на kind показывают реальность.
kind create cluster --name kb
npx @kinvolk/headlamp-plugin install \
https://artifacthub.io/packages/headlamp/kubebuddy/kubebuddy-headlamp-plugin \
--folderName ./plugins/kubebuddy
Один нюанс, которого нет ни в одной документации: Headlamp ожидает структуру plugins/<name>/main.js, а CLI создает лишний уровень вложенности директорий. Пока вы его не уберете, /plugins будет возвращать пустой список, и плагин никогда не появится в боковом меню.
Далее три деплоймента. Один без аннотаций в качестве контрольного, два других с исключениями по разные стороны от сегодняшней даты:
kubectl create deploy nginx-bad --image=nginx:latest
kubectl create deploy sup-active --image=nginx:latest
kubectl create deploy sup-expired --image=nginx:latest
# исключение действительно до 2030 года
kubectl annotate deploy sup-active \
kubebuddy.io/ignore-checks='*' \
kubebuddy.io/ignore-reason='approved by security, ticket SEC-42' \
kubebuddy.io/ignore-until='2030-01-01T00:00:00Z'
# срок исключения истек в 2020 году
kubectl annotate deploy sup-expired \
kubebuddy.io/ignore-checks='*' \
kubebuddy.io/ignore-reason='temporary waiver' \
kubebuddy.io/ignore-until='2020-01-01T00:00:00Z'
deployment.apps/nginx-bad created
deployment.apps/sup-active created
deployment.apps/sup-expired created
deployment.apps/sup-active annotated
deployment.apps/sup-expired annotated
Сканирование показало оценку 87 из 100 по 146 проверкам. 31 тест провален, причем RBAC002 RBAC Overexposure оказался на самом верху, потому что я также создал привязку cluster-admin, чтобы сканеру было что находить.
На карточке WRK005 Missing Resource Requests написано 2 findings и 1 suppressed. Список ниже выглядит так:
| Deployment | В списке ошибок | Почему |
|---|---|---|
nginx-bad | да | нет аннотаций |
sup-expired | да | исключение истекло в 2020 |
sup-active | нет, скрыта | исключение до 2030 |
Итак, ошибка с истекшим сроком действительно возвращается. Не как предупреждение где-то в логах, а прямо в списке проблем, куда вы обычно смотрите.
А скрытая ошибка не выбрасывается:
{
"resource": "sup-active",
"kind": "Deployment",
"details": "Container nginx missing CPU request, MEMORY request",
"suppressionReason": "approved by security, ticket SEC-42",
"suppressionUntil": "2030-01-01T00:00:00Z",
"link": "/c/kind-kb/deployments/default/sup-active"
}
То же самое происходит с восемью другими проверками одновременно, потому что в аннотации использовалась *.
Почему именно эта часть имеет значение
Подавление уязвимости без даты окончания становится решением, которое переживает свою причину. Рабочая нагрузка в конце концов получает свои лимиты, устаревший сервис выводится из эксплуатации, человек, одобривший исключение, увольняется, а аннотация остается. Сканер продолжает молчать о проблеме, которую уже никто не считает допустимой, и оценка продолжает показывать здоровье, которого не существует.
Отсутствие возможности подавления вообще выглядит гораздо честнее. Это раздражает, но не врет.
Исключение с обязательным обоснованием и дедлайном остается единственной версией, которая остается правдивой: она превращает “мы приняли этот риск” в “мы приняли этот риск до марта”, а после марта система сама спросит снова.
На что обратить внимание
Оценка меняется по неверным причинам. Любой инструмент с большой цифрой на главной странице провоцирует людей “подкручивать” её самым дешевым способом. ignore-checks: '*' на одном деплойменте убрал восемь ошибок одной аннотацией. Если такие аннотации не проверяются на код-ревью (pull requests), метрика деградирует в самообман за пару месяцев.
Сканирование умирает при уходе со страницы. Плагин регистрирует обработчик beforeunload с сообщением KubeBuddy is still scanning. Leave this page and the scan will stop. Это осознанно сделано для ревью по запросу, а не для постоянного мониторинга. Непрерывный контроль требует использования CLI в CI/CD пайплайне.
Покрытие плагина не равно покрытию CLI. Здесь нет проверок AKS или GKE, требующих облачных API, нет проверок Prometheus, нет нативного Go-движка. Сравнение отчета плагина с отчетом CLI покажет ложные расхождения по проверкам, которых в плагине просто нет. Сравнивайте только пересекающиеся ID проверок.
История оценок живет в вашем браузере. Ключ kubebuddy:score-history:<cluster> лежит в localStorage, помеченный как Local cache only в UI. Другой браузер, другой инженер, очистка данных сайта, и тренд исчезает.
Скрытые ошибки работают как счетчик, а не список. Раскрытая карточка показывает 1 suppressed без подробностей. Причина и дедлайн находятся в полном отчете и экспорте. Если вы выстраиваете процесс ревью вокруг этого, проверяйте экспорт, а не экран.
Безопасно ли запускать сам инструмент
Полезно уметь отвечать на этот вопрос с доказательствами, а не ссылкой на маркетинговую страницу, особенно перед тем как натравливать инструмент на чужой кластер.
# мутирующие запросы
grep -oE '"(POST|PUT|PATCH|DELETE)"' main.js | sort | uniq -c
# любые собственные сетевые вызовы
grep -oE 'fetch\(|XMLHttpRequest|axios' main.js | sort | uniq -c
# куда он вообще обращается
grep -o 'https\?://[a-zA-Z0-9./-]*' main.js | sort -u
Первые две команды ничего не возвращают. Чтение идет через клиент API Headlamp, и бандл не имеет собственного сетевого кода. Третья возвращает только ссылки на документацию. Хостинг сервиса вендора упоминается ровно один раз, как строка внутри сгенерированного kubebuddy-config.yaml, в секции, которая отключена по умолчанию.
Итог: работает с вашими правами, только на чтение, ничего не деплоит и не имеет путей в коде для отправки результатов куда-либо. Три команды, и вы можете сказать это клиенту с невозмутимым лицом.
Очистка
kind delete cluster --name kb
Вывод
Если вы выбираете сканер кластера, смотрите не только на количество проверок. Спросите, что происходит с исключением через шесть месяцев после того, как кто-то его добавил. Этот ответ скажет вам, будет ли оценка на дашборде вообще что-то значить к тому времени.
Проверено на плагине версии 0.5.1, включающем проверки KubeBuddy из v0.0.36, на kind кластере, сентябрь 2026 года.