Безопасность IT-инфраструктуры: CVE-сканирование Docker-образов и аудит открытых портов
CVE-сканирование Docker и аудит открытых портов.
Несколько месяцев назад я чуть не проспал критическую уязвимость в production-окружении. Не потому что не следил — просто образ supabase/postgres:15.8 обновился в upstream, в старой версии обнаружили три CVE с рейтингом High, и я узнал об этом случайно, когда полез смотреть что-то другое. Мы обновили образ вовремя, обошлось без последствий, но осадок остался. Вопрос "а что ещё я не замечаю прямо сейчас?" с тех пор не даёт покоя. Именно это подтолкнуло меня к тому, чтобы выстроить нормальную систему безопасности в собственной инфраструктуре — не энтерпрайзную, без раздутых бюджетов и отдела ИБ, а практичную и реально работающую.
Я управляю инфраструктурой из примерно десятка серверов: Timeweb Cloud плюс несколько выделенных. На них крутятся Supabase, несколько production-сайтов, Bitrix24 с телефонией, 1C ERP, VPN, почтовый сервер. Всё это в Docker, всё взаимосвязано. И у каждого сервиса — свои образы, свои порты, свои потенциальные дыры. Держать это в голове невозможно. А нанимать отдельного специалиста по безопасности ради небольшой инфраструктуры — избыточно и дорого. Поэтому я сделал инструмент для себя: встроил CVE-сканирование и аудит портов прямо в свой дашборд управления инфраструктурой cos-it.ru.
Это не теоретическая история про то, как надо делать. Это описание системы, которая работает у меня прямо сейчас — со всеми её ограничениями, компромиссами и реальными находками. Я специально избегаю слова "кейс" — потому что оно создаёт иллюзию завершённости, когда на самом деле это живой, развивающийся процесс. Каждый месяц я что-то дорабатываю, что-то нахожу неожиданное, что-то пересматриваю. Именно в таком виде это и представляю.
Расскажу, как это устроено, с какими граблями я столкнулся и почему выбрал именно такой подход, а не готовые платные решения. И заодно поговорим о том, что вообще стоит за словами "безопасность инфраструктуры" для компаний среднего уровня — не про теорию, а про то, что реально происходит, когда начинаешь копать.
Сразу скажу: я не специалист по информационной безопасности в классическом понимании этого слова. Я директор по развитию, который занимается маркетингом и IT-инфраструктурой одновременно — и именно поэтому мне нужны практические инструменты, которые работают без постоянного вмешательства. Статья написана с позиции практика, который решал реальные задачи, а не с позиции теоретика который читал все правильные книги. Именно это, на мой взгляд, делает её полезной для тех, кто находится в похожей ситуации.
Когда говоришь коллегам "нужно сканировать Docker-образы на уязвимости", часто слышишь в ответ примерно следующее: "Мы используем официальные образы, там всё должно быть нормально." Это заблуждение, и довольно опасное. Официальные образы — это не означает безопасные. Это означает, что их кто-то поддерживает и они соответствуют определённым техническим стандартам Docker. Но внутри официального образа nginx или postgres могут быть библиотеки с известными уязвимостями — просто потому что maintainer ещё не собрал новую версию, или потому что уязвимость обнаружили вчера, а образ релизили три недели назад.
CVE — это Common Vulnerabilities and Exposures, публичный реестр известных уязвимостей. Каждой уязвимости присваивается номер, описание и оценка по шкале CVSS: от Low (низкий риск) до Critical (можно выполнить произвольный код без аутентификации). Именно поэтому CVE-сканирование образов — это не разовая акция "проверили и забыли", а постоянный процесс. Новые уязвимости появляются каждый день. Образ, который был чист месяц назад, сегодня может содержать три Critical.
Конкретный пример из нашей практики: supabase/postgres:15.8 в какой-то момент получил три High CVE. Все три касались библиотеки OpenSSL внутри базового Debian-образа. Напрямую они не позволяли взломать Supabase снаружи — слой за слоем там есть ограничения сетевого доступа — но если бы атакующий каким-то образом попал внутрь контейнера, у него были бы инструменты для эскалации привилегий. Обновление образа до 15.9 закрыло эти CVE. Цена решения — 20 минут работы. Цена игнорирования — потенциально компрометация всей базы данных.
Но вот в чём штука: чтобы знать, что нужно обновиться, надо сначала узнать о проблеме. Без автоматического сканирования вы узнаёте об этом либо случайно, либо когда уже поздно. Именно поэтому я добавил в свой дашборд раздел CVE-сканирования — чтобы видеть состояние всех образов на всех серверах в одном месте, без необходимости логиниться на каждый сервер и запускать команды вручную.
Когда я начинал выбирать инструмент для CVE-сканирования, на столе было несколько вариантов. Snyk — хорошее решение, интегрируется с CI/CD, красивый интерфейс, но цена для команды больше одного человека начинается от нескольких сотен долларов в месяц. За что? За тот же NVD (National Vulnerability Database), который бесплатен, плюс дополнительные базы данных и удобный UI. Nessus — это уже Enterprise история, которую используют компании с выделенными командами безопасности. Docker Scout — встроенная штука в Docker Desktop, работает, но требует Docker Hub и имеет ограничения по количеству сканирований в бесплатном плане. Qualys, Prisma Cloud — ещё дороже и сложнее.
Trivy от Aqua Security — это open source инструмент, который я выбрал для интеграции в свой дашборд. Он умеет сканировать Docker-образы, файловые системы, репозитории, IaC-конфиги. Поддерживает множество баз уязвимостей: NVD, GitHub Security Advisory, Red Hat, Debian, Ubuntu и другие. Бесплатен. Активно поддерживается — релизы выходят почти каждую неделю. И, что важно для моего сценария, его легко запустить через SSH на удалённом сервере и получить результат в формате JSON для дальнейшей обработки.
В моей реализации сканирование работает так. На каждом сервере установлен Trivy (или в крайнем случае использую `docker scout` если Trivy по какой-то причине недоступен). Через SSH я запускаю сканирование нужного образа с флагом вывода в JSON, получаю результат, парсю его на бэкенде и сохраняю отчёт в базу данных — таблица `cve_reports`. В дашборде показываю сводку: сколько Critical, High, Medium, Low уязвимостей найдено в каждом образе на каждом сервере. Можно провалиться в детали — увидеть конкретный CVE-идентификатор, описание, CVSS-score, пакет с уязвимостью и версию, в которой проблема устранена.
Одна из главных задач при проектировании была сделать это удобным, а не превратить в источник постоянного шума. Проблема многих систем безопасности в том, что они генерируют такое количество алертов, что люди начинают их игнорировать. Если у тебя приходит 200 уведомлений о Low CVE, которые нельзя исправить без полного пересборки базового образа — ты перестаёшь обращать внимание. Поэтому я настроил приоритизацию: инциденты автоматически создаются только для Critical и High, остальное видно в дашборде, но не шумит.
И тут есть важный нюанс, который я понял не сразу. Не все CVE одинаково опасны именно в вашем контексте. CVSS-score 9.8 (Critical) — это оценка уязвимости в вакууме. Но если уязвимость требует локального доступа к файловой системе контейнера, а ваш контейнер вообще не имеет внешнего сетевого интерфейса и крутится за двумя слоями файрволов — реальный риск значительно ниже. Это называется contextual risk — и именно поэтому хороший специалист по безопасности незаменим, но это не значит, что без него нельзя выстроить разумный базовый уровень защиты.
Ещё одна вещь, которую стоит понимать про CVE-базы данных: они не мгновенно отражают реальность. Уязвимость может быть известна в узком кругу специалистов несколько недель до того, как попадёт в NVD. Это называется zero-day — уязвимость без публичного патча. Trivy и другие сканеры работают с публичными базами данных и против zero-day бессильны. Это ограничение нужно понимать: CVE-сканирование защищает от известных угроз, не от неизвестных. Но именно известные угрозы составляют подавляющее большинство реальных атак — атакующие используют то, что уже задокументировано и для чего есть готовые эксплойты, а не тратят месяцы на разработку нового.
Ещё один момент, связанный со сканированием образов, который я поначалу упускал: важно сканировать не только образы запущенных контейнеров, но и образы, которые есть на сервере в docker image ls, даже если контейнер на их основе не запущен. Незапущенный контейнер с уязвимым образом — не угроза сам по себе, но он может быть запущен в любой момент. И если процесс деплоя автоматизирован, новая версия с уязвимым базовым образом может оказаться в production без вашего участия. Поэтому в свою систему я добавил возможность сканировать все локально хранящиеся образы, а не только активные.
Если CVE-сканирование — это проверка того, что внутри ваших контейнеров, то аудит портов — это проверка того, что ваша инфраструктура показывает наружу. И вот здесь, честно говоря, я нашёл больше неожиданного.
Расскажу конкретный случай. Мы деплоили новый сервис — небольшой Node.js микросервис для обработки вебхуков. В конфиге разработчика был прописан порт 8080 для debug endpoint с подробной телеметрией: состояние очередей, входящие запросы, внутренние метрики. Очень удобная штука для отладки. Конфиг попал в production без изменений. Сервис запустился, порт 8080 открылся. Nginx не проксировал этот порт — он не был нужен снаружи — но файрвол на сервере его не закрывал. И вот несколько недель порт был доступен из интернета.
Что там было? Ничего катастрофического — внутренние метрики без аутентификации. Но любой желающий мог видеть версии зависимостей, внутренние URL-адреса, структуру очередей. Это называется information disclosure — информация, которая помогает атакующему спланировать более точечную атаку. Мы закрыли порт, добавили аутентификацию на debug endpoint. Но главное — я понял, что без систематического мониторинга открытых портов такие вещи будут повторяться.
Теперь у меня в дашборде работает регулярное сканирование портов на каждом сервере через `ss -tlnp` (или `netstat -tlnp` как fallback). Команда запускается через SSH, результат парсится, сравнивается с предыдущим сканом. Если появился новый открытый порт — создаётся инцидент: "На сервере X обнаружен новый открытый порт Y, процесс Z." Если порт, который был открыт, закрылся — тоже фиксируется, просто как информация, без алерта. Логика простая: открытие нового порта — это потенциальная угроза, закрытие — это хорошая новость.
На практике за несколько месяцев работы этой системы я несколько раз получал такие алерты. Большинство оказывались ожидаемыми — что-то деплоили и забыли обновить baseline. Но один раз был действительно неожиданный: на одном из серверов открылся порт, связанный с процессом, которого там не должно было быть. Оказалось, разработчик логинился через SSH, запустил тестовый сервер "на минуту посмотреть" и забыл его остановить. Ничего страшного, но без мониторинга это так бы и висело неопределённое время.
Важный момент в настройке такого сканирования — правильно определить baseline. У каждого сервера есть "нормальный" набор открытых портов: 22 (SSH), 80/443 (HTTP/HTTPS через Nginx), специфичные порты конкретных сервисов. Всё что сверх этого — аномалия, требующая объяснения. Хранить baseline в базе данных и автоматически сравнивать с текущим состоянием — это и есть суть аудита портов в его практическом исполнении.
Сравнивал этот подход с альтернативами. Nmap — классический инструмент сканирования портов, но он работает снаружи сервера и не видит процессы, которые слушают только на localhost. `ss -tlnp` запускается внутри сервера и видит всё, включая сервисы, доступные только локально. Это важно: много потенциально опасного вообще не торчит наружу через файрвол, но доступно внутри сервера — и это тоже риск, особенно если у атакующего есть точка входа внутрь.
Параллельно с `ss -tlnp` я сделал ещё одну полезную вещь: мониторинг правил файрвола. На каждом сервере у меня настроен UFW или iptables, и периодически я снимаю актуальный список правил. Это позволяет замечать изменения в конфигурации файрвола — что бывает редко, но когда бывает, обычно имеет значение. Пример из практики: на одном сервере после очередного обновления системных пакетов какой-то скрипт постустановки добавил правило, разрешающее входящий трафик на порт 8888. Без мониторинга правил файрвола я бы это не заметил, потому что `ss -tlnp` показывает только то, что реально слушает — а на этом порту никто ещё не слушал.
Ещё один паттерн, который я обнаружил при анализе открытых портов: Docker сам по себе может обходить правила UFW. Это известная проблема: Docker напрямую модифицирует iptables и при публикации порта через `-p 0.0.0.0:8080:8080` открывает его для всего интернета, игнорируя настроенные правила UFW. Многие разработчики об этом не знают и думают, что раз в UFW порт закрыт — значит снаружи он недоступен. Неверно. Решение: публиковать порты только на localhost (`127.0.0.1:8080:8080`) и проксировать через Nginx с нужным контролем доступа. Именно мониторинг реально открытых портов через `ss -tlnp` помог мне однажды обнаружить контейнер, порт которого был виден снаружи несмотря на правило UFW — именно из-за этой особенности Docker.
Пока я строил систему безопасности, обнаружил кое-что неудобное. Большинство статей про безопасность Docker-инфраструктуры уделяют основное внимание CVE и файрволам — это понятно и измеримо. Но есть тема, которую часто замалчивают: как хранятся секреты в реальных проектах. Токены, пароли, API-ключи.
Я долго думал, почему так происходит. Думаю, потому что честный ответ на этот вопрос у большинства компаний звучит примерно так: "В .env файлах на серверах, иногда прямо в docker-compose.yml, иногда в коде, иногда в истории git." Это не злой умысел — просто так складывается исторически. Разработчик добавил токен напрямую "на время", потом забыл. В docker-compose.yml прописали пароль для удобства. В git push попал .env файл, который не был добавлен в .gitignore.
В своей системе я реализовал secret manager с AES-256-GCM шифрованием. Идея простая: секреты хранятся в базе данных в зашифрованном виде, ключ шифрования — в переменной окружения, которая нигде не логируется и не отображается в дашборде. Для работы с секретами через интерфейс нужна авторизация, все операции логируются в аудит-лог. Ротация ключей — отдельная процедура с перешифрованием всех записей.
Но вот честный вопрос: насколько это надёжно на практике? Если у атакующего есть доступ к серверу где стоит приложение — у него есть доступ и к переменным окружения, и к базе данных. Секьюрити специалисты это называют "trusted boundary" — граница доверия. Если граница скомпрометирована, шифрование внутри этой границы уже не помогает. Тогда зачем оно нужно?
Ответ в том, что большинство утечек происходят не из-за взлома сервера, а из-за утечки кода или конфигурации. Разработчик случайно запушил в публичный репозиторий. Злоумышленник получил доступ к бэкапу. В логах оказался вывод с переменными окружения. Вот здесь шифрование и разделение доступов реально работает: токен в зашифрованном виде бесполезен без ключа, который хранится отдельно. Это не панацея, но это отдельный слой защиты в нормальной defense in depth стратегии.
Отдельная история — ротация секретов. В теории её нужно делать регулярно: раз в месяц менять все API-ключи, токены, пароли. На практике это мучительно, потому что надо обновить ключ во всех местах где он используется, ничего не сломав. Поэтому большинство компаний не ротируют секреты вообще, пока что-нибудь не утечёт. Я в своей системе сделал минимум: отображаю в дашборде когда секрет последний раз обновлялся, и если больше 90 дней — ставлю предупреждение. Это не автоматическая ротация, но хотя бы не дает забыть совсем.
Ещё один компонент безопасности, который у многих вызывает вопросы: зачем аудит действий в single-user системе? Кто кроме меня туда заходит — я же один. Логировать самого себя?
Да, и вот почему. Первое — это не только про безопасность от внешних угроз, но и про восстановление после ошибок. Когда что-то сломалось, нужно понять что именно произошло. Audit log говорит: в 14:23 был сделан такой-то запрос к такому-то endpoint, с такими-то параметрами. Это гораздо быстрее, чем копаться в памяти или поднимать логи Nginx.
Второе — SSH-сессии. Я записываю все SSH-сессии через свой дашборд: каждую команду с временной меткой, вывод терминала. Если через три недели обнаружится, что на сервере появилось что-то странное — я могу воспроизвести всё что делал в этом терминале и найти момент где пошло не так. Это инструмент расследования, не слежки. И это работает даже когда ты работаешь один.
Третье — это привычка. Если завтра у тебя появится второй человек с доступом к инфраструктуре — аудит уже есть. Не нужно внедрять его в пожарном режиме когда что-то пошло не так.
Технически аудит реализован как middleware в Express: каждый API-запрос логируется в таблицу `audit_logs` с userId, методом, URL, IP-адресом, временной меткой и кратким описанием действия. SSH-записи хранятся как timeline объектов с типом "input" или "output" — это позволяет воспроизвести сессию в интерфейсе с реальными паузами между командами.
Отдельный вопрос — хранение и очистка этих логов. Аудит-логи имеют тенденцию расти быстро, особенно если у тебя активный дашборд с частыми автоматическими запросами. Я сделал ротацию: логи старше 90 дней удаляются автоматически. Для расследования большинства инцидентов этого достаточно, а хранить всю историю вечно — дорого и бесполезно.
Есть ещё один аспект аудита, который я ценю непропорционально его сложности реализации: логирование неудачных попыток входа. Каждый неуспешный логин в дашборд фиксируется с IP-адресом, временной меткой и браузером. Это позволяет видеть картину: если с какого-то IP идут сотни попыток подбора пароля — это видно в статистике. Для реакции на подобное у меня настроен rate limiting на уровне API, но без логирования попыток я бы просто не знал, что это происходит. Несколько раз в дашборде я видел всплески неудачных попыток входа — ботнеты регулярно сканируют всё подряд. Ничего страшного, но неприятно думать, что это происходит в фоне незамеченно.
Этот вопрос мне задают чаще всего. Зачем городить своё, когда есть готовые решения? Давай разберу честно.
Snyk — хороший продукт для команд разработчиков. Его сила в интеграции с CI/CD: он сканирует зависимости прямо в pull request, не даёт смержить код с критическими уязвимостями. Для моих задач это избыточно — у меня нет команды разработчиков, которым нужна эта интеграция. Платная часть Snyk стоит денег, бесплатный план имеет ограничения, и главное — он не решает задачу мониторинга инфраструктуры целиком. Это инструмент для одной задачи, а мне нужен единый дашборд.
Nessus — это уже совсем другой уровень. Профессиональный сканер уязвимостей, используется в enterprise-компаниях. Умеет очень много: сетевое сканирование, compliance-проверки, детальные отчёты. Цена — от нескольких тысяч долларов в год. И что важнее: он требует отдельной инфраструктуры для себя, настройки, обслуживания. Это инструмент для компании со штатным ИБ-специалистом. У меня таких ресурсов нет, и это нормально — не каждая компания должна иметь уровень безопасности финансового сектора.
Qualys, Prisma Cloud, Wiz — ещё дороже, ещё сложнее, ещё больше требований к инфраструктуре. Они решают проблемы компаний с сотнями серверов и тысячами контейнеров. Мой масштаб — десяток серверов.
Docker Scout — интересный вариант, встроен в Docker. Работает, интегрируется с Docker Hub. Но у него есть ограничения: бесплатный план сканирует только 3 репозитория, более широкий мониторинг платный, и главное — это опять отдельный инструмент вне моего основного дашборда. Мне важно видеть всё в одном месте.
Trivy как CLI инструмент — это именно то что нужно: бесплатный, мощный, с хорошей базой уязвимостей, легко автоматизируемый. Я просто обернул его в свой интерфейс, добавил хранение истории, алерты и удобное отображение. Не изобретал колесо — использовал хороший инструмент как компонент своего решения.
Есть один честный минус у моего подхода: время на разработку и поддержку. Когда Trivy меняет формат JSON-вывода — нужно обновить парсер. Когда появляется новая версия — нужно обновить на серверах. Это работа, которую в случае платного решения делает вендор. Я принял этот trade-off осознанно: экономлю деньги, трачу время. Для моего масштаба это оправданно.
Позволю себе отступление о том, как именно выглядит работа с этой системой в реальности — не с точки зрения архитектуры, а с точки зрения ежедневной практики.
Утром открываю дашборд. В разделе Security вижу сводку: сколько Critical, High уязвимостей найдено за последние 24 часа. Если есть новые — смотрю детали. Обычно это одно из двух: либо в upstream появилась новая CVE в образе который давно не обновлялся, либо кто-то запустил новый контейнер с устаревшим образом. В первом случае проверяю, вышел ли новый тег образа с исправлением. Если да — планирую обновление. Если нет — документирую как известную уязвимость с принятым риском.
В разделе портов смотрю на изменения с последнего скана. Новые порты — изучаю что это. Обычно объяснение находится быстро, иногда требует уточнения у команды. Раз-другой в месяц нахожу что-то неожиданное.
Раздел секретов показывает, какие из них давно не ротировались. Обычно игнорирую всё что моложе месяца, на более старые смотрю по ситуации.
Инциденты создаются автоматически только для серьёзных находок. Мне не нужно каждый день разгребать 50 уведомлений — система шумит только когда это действительно важно. Именно эта настройка сигнал-шум критически важна для того, чтобы система работала на практике, а не превращалась в ещё один источник информационного шума, который начинают игнорировать.
Один момент, который я бы сделал иначе если бы начинал сначала: приоритизация сканирования по риску. Сейчас сканирую все образы по расписанию с одинаковым приоритетом. Логичнее было бы сканировать чаще те образы, которые доступны из интернета, и реже те, которые крутятся только во внутренней сети. Это следующая итерация в плане развития.
Ещё одна вещь, которую я добавил уже в процессе эксплуатации: история CVE по образу. Не просто текущий отчёт, а трендовый график — сколько уязвимостей было месяц назад, неделю назад, сейчас. Это позволяет видеть динамику: образ становится безопаснее (команда регулярно обновляет) или хуже (накапливает CVE без обновлений). Второй сценарий — это тревожный сигнал не только с точки зрения безопасности, но и с точки зрения обслуживаемости. Образ который никто не обновляет полгода — скорее всего это либо забытый сервис, либо технический долг который нужно решать.
Хочу поднять тему, которую редко связывают с безопасностью напрямую, но которая, по моему опыту, является одним из главных источников уязвимостей — технический долг. Не в смысле "плохой код", а в смысле накопленных решений, которые перестали быть актуальными, но продолжают работать и занимать место в инфраструктуре.
Конкретный пример: на одном из серверов обнаружился работающий контейнер, который запустили примерно год назад для тестирования какой-то интеграции. Тест завершили, интеграция не пошла в production, но контейнер забыли остановить. Год он просто крутился, никому не мешал, потреблял немного ресурсов. И ровно за этот год внутри его образа накопились Critical CVE — он никогда не обновлялся, потому что о нём никто не думал. Это работающий сервер с дырой, о которой никто не знал.
CVE-мониторинг нашёл его именно потому, что сканирует все контейнеры, а не только те которые "официально" в production. Контейнер попал в дашборд с пятью Critical уязвимостями — это стало поводом разобраться что это вообще такое, и он был удалён. Это мелочь, но она иллюстрирует важный принцип: забытые сервисы — это не просто технический долг, это потенциальная точка входа.
То же самое касается старых SSH-ключей и учётных записей. У многих компаний годами хранятся ключи бывших сотрудников, подрядчиков, сервисов которые уже не используются. Кто-то уволился полтора года назад, его ключ так и висит в authorized_keys на всех серверах. Если этот бывший сотрудник недоброжелателен — это реальная угроза. Если просто потерял ноутбук — тоже. Регулярный аудит authorized_keys с проверкой кто эти люди и актуален ли их доступ — это неприятная, но необходимая работа.
Похожая история с Docker volumes. Удалённый контейнер часто оставляет за собой volume с данными. Эти данные могут содержать конфигурации, токены, пароли которые были в переменных окружения контейнера. `docker volume ls` на production-сервере иногда показывает интересную коллекцию исторических артефактов. Регулярная уборка — `docker system prune` с осторожностью — это часть гигиены, а не только экономия дискового пространства.
Я долго думал о том, как систематизировать этот аспект в своём дашборде. В итоге пришёл к простому решению: раз в неделю в отчёте появляется список контейнеров, которые работают больше 30 дней без обновления образа. Это не обязательно плохо — Nginx который работает 90 дней и регулярно обновляется это нормально. Но это повод задать вопрос: а точно ли этот сервис должен работать? Точно ли его образ актуален? Иногда ответ "да, всё нормально". Иногда — "а, точно, надо обновить". Иногда — "постой, зачем это вообще запущено?"
Этот еженедельный чекин с инфраструктурой занимает минут двадцать, но не позволяет накапливаться технического долгу незаметно. И раз в три-четыре месяца обязательно нахожу что-нибудь интересное.
Хочу честно поговорить о том, что осознанно оставил за рамками своей системы безопасности — и почему.
Двухфакторная аутентификация для дашборда. Реализовал TOTP (Google Authenticator и подобные), но в итоге отключил для себя как обязательную. Причина простая: это single-user система, она доступна только через HTTPS, и дополнительный слой трения при каждом входе мешает работе без пропорциональной пользы. Если бы системой пользовались несколько человек — 2FA была бы обязательной. Для одного пользователя это trade-off в пользу удобства.
Полный сетевой пентест. У меня нет возможности регулярно нанимать пентестеров для проверки инфраструктуры. Автоматизированные сканеры типа OpenVAS могут найти часть проблем, но настоящий пентест — это работа квалифицированного человека, который думает как атакующий. Для моего масштаба это было бы разовым мероприятием раз в год-два, и скорее всего нашло бы что-то интересное. Но пока это не стоит в приоритетах — честно признаю.
Анализ сетевого трафика и IDS. Intrusion Detection System — это следующий уровень мониторинга: смотреть не на то что слушает сервер, а на что он реально отправляет и получает. Это позволяет обнаруживать аномалии: контейнер, который вдруг начал делать DNS-запросы к странным доменам, или неожиданный исходящий трафик на нестандартный порт. Инструменты для этого есть — Falco, например, мониторит системные вызовы в контейнерах. Но это существенно сложнее в настройке и требует разбираться с тем, что является нормальным поведением, а что аномалией. Пока не готов инвестировать в это время.
Compliance-проверки. CIS Benchmarks для Docker и Linux — это подробные руководства с сотнями параметров, которые нужно проверять. Запустить docker-bench-security и получить список из 150 пунктов "нужно исправить" — несложно. Понять что из этого реально критично для твоего конкретного сценария — уже сложнее. Я пробовал работать с этим списком и в итоге понял, что треть пунктов неактуальна для моей конфигурации, ещё треть требует изменений которые создадут операционные проблемы, и только оставшаяся треть — это реально полезные улучшения. Сделал эту треть руками, закрыл список.
Это не жалобы и не оправдания. Это честное описание того, где проходит граница разумной безопасности для конкретного масштаба с конкретными ресурсами. Безопасность всегда про trade-off: между защищённостью и удобством работы, между глубиной мониторинга и стоимостью его поддержки, между идеальной безопасностью и реально достижимой. Важно делать эти выборы осознанно, а не случайно.
Я долго думал над тем, в чём главная ошибка большинства компаний среднего размера в вопросах безопасности. И пришёл к выводу, что это отношение к безопасности как к проекту, который можно завершить. "Внедрили антивирус, настроили файрвол, сделали пентест — готово, галочка стоит." Но угрозы не стоят на месте. Каждый день появляются новые CVE. Каждое изменение в инфраструктуре создаёт потенциально новую точку риска. Открытый порт появляется за секунду — и может оставаться незамеченным месяцами.
Есть у этого явления и психологическое измерение, которое стоит признать. Безопасность неблагодарна: когда всё работает хорошо, её не замечают. Никто не говорит "отлично, в этом месяце нас не взломали." Инцидент — это видимый результат, его не было — это отсутствие события. Поэтому безопасность так легко депrioritize в пользу новых фич, улучшений продукта, роста. Именно поэтому её нужно автоматизировать настолько, чтобы она не зависела от приоритетов конкретной недели. Если сканирование работает по cron и результаты видны в дашборде — им не нужен отдельный спринт в бэклоге.
Правильный подход — это непрерывный мониторинг и реагирование. Не проект, а процесс. И это не требует огромных ресурсов — требует правильно выстроенных инструментов и привычки смотреть на их результаты. В этом, собственно, и есть суть того, что я сделал в своём дашборде: автоматизировал рутинную проверку, оставил себе только принятие решений.
Есть ещё одна вещь, о которой стоит сказать напрямую. Идеальной безопасности не существует. Если кто-то достаточно мотивирован и квалифицирован — он найдёт способ войти. Цель безопасности не в том, чтобы сделать взлом невозможным, а в том, чтобы сделать его достаточно дорогим и сложным, чтобы атакующий выбрал более лёгкую цель. И в том, чтобы быстро обнаружить и локализовать инцидент если он всё-таки произошёл.
Для компании без выделенного ИБ-отдела реалистичный базовый уровень безопасности — это: регулярное обновление образов и зависимостей, мониторинг открытых портов, нормальное хранение секретов, логирование действий, резервное копирование с проверкой восстановления. Ни одна из этих вещей не требует миллионных бюджетов. Все они требуют дисциплины и правильных инструментов.
Про резервное копирование с проверкой восстановления отдельно скажу пару слов, потому что это неочевидный пункт. Большинство компаний делают бэкапы. Единицы их проверяют. А разница между "бэкап есть" и "бэкап работает" может стоить бизнеса. Я видел ситуацию, когда компания год делала бэкапы базы данных, а когда понадобилось восстановление — оказалось, что все архивы битые из-за ошибки в скрипте резервирования. Поэтому в своём мониторинге я настроил периодическую проверку свежести бэкапов в S3: если последний бэкап старше 25 часов — алерт. Это не проверка целостности файла, но хотя бы гарантия, что процесс бэкапирования продолжает работать.
Ещё один принцип, который помогает держать уровень безопасности на приемлемом уровне без постоянных специальных усилий: безопасность должна встраиваться в рабочий процесс, а не добавляться к нему. Проверка CVE при деплое новой версии образа — это не отдельная задача, это часть процесса деплоя. Аудит открытых портов после изменений в конфигурации — это не отдельный шаг, это часть процедуры выкатки. Когда безопасность встроена в workflow, она не забывается и не откладывается.
Триггером для написания этой статьи был тот supabase/postgres с тремя High CVE, которые я обнаружил случайно. Сейчас я бы обнаружил их автоматически в течение нескольких часов после появления в базе данных уязвимостей. Это и есть разница между "как-то следим за безопасностью" и реально работающей системой мониторинга. Не потому что я стал лучше как специалист — просто потому что выстроил правильный процесс, который работает независимо от того, насколько я внимателен в конкретный день.
Если вы управляете инфраструктурой похожего масштаба и задумываетесь о том, с чего начать — начните с простого. Поставьте Trivy, запустите первое сканирование образов прямо сейчас, посмотрите что найдёт. Потом пройдитесь по серверам и выполните `ss -tlnp`, сравните результат с тем, что вы ожидаете увидеть. Поищите в git-истории .env файлы или токены которые туда попали по ошибке. Скорее всего, того что вы найдёте, будет достаточно, чтобы безопасность из абстрактной темы превратилась в конкретный список задач. Дальше — дело техники и приоритетов.
Я не буду делать вид, что у меня всё идеально выстроено и моя инфраструктура неуязвима. Это было бы неправдой. Но разница между тем, что было год назад, и тем, что есть сейчас — существенная. Тогда я реагировал на проблемы когда они проявлялись. Сейчас вижу большинство из них до того, как они стали проблемами. Именно это и есть цель системы безопасности на этом уровне — не абсолютная защита, а своевременная видимость.