Харденинг контейнеров и гейты в CI
Собрал минимальный профиль запуска и проверил каждую настройку отдельно. От обычного пользователя вместо root - смена владельца чужого файла даёт отказ. С отобранными разрешениями - тот же отказ. С файловой системой только на чтение - запись в системный файл даёт «Read-only file system», а временные файлы уезжают в отдельный кусок памяти. Каждая строчка проверяема за десять секунд.
Стержень: укрепление - это короткий список настроек по умолчанию плюс проверка, которая не даёт их обойти.
// Формулировки: «как укрепить контейнер?», «какие проверки ставить в конвейер?», «что делать с легаси, которое требует root?»
Минимальный профиль
Список короткий, и он одинаковый почти для всех сервисов. Запуск от обычного пользователя. Отобрать все разрешения root и вернуть только нужные - большинству сервисов не нужно ни одного. Запретить повышение прав, потому что в образе остаются программы, работающие от владельца файла: в базовом debian их восемь, в alpine ноль. Файловая система только на чтение, а под временные файлы - отдельный кусок памяти. Пределы по памяти и процессору, чтобы один сервис не съел машину.
Отдельно: не пробрасывать внутрь сокет демона контейнеров - это его интерфейс управления, работающий без всякого пароля, - и не ставить полный набор привилегий. Я показывал раньше, что это даёт: с сокетом обычный контейнер перечисляет и запускает любые другие контейнеры на машине, то есть равен root на машине.
// Всё это ставится один раз в шаблон и применяется ко всем новым сервисам. Проверять полезно на живом стенде, а не по документации: настройка, которая «должна работать», иногда молча не применяется - например, потому что образ ожидает root и падает при старте от другого пользователя.
- минимальный профиль
- короткий набор настроек запуска, применяемый ко всем сервисам по умолчанию
- запрет повышения прав
- программы с правами владельца перестают поднимать привилегии
Что делать с тем, что требует root
Всегда находится сервис, который «работает только от root». Разбирается это по порядку. Сначала выясняем, ЧТО именно ему нужно: обычно одно конкретное разрешение вроде открытия сетевого порта с маленьким номером или доступа к устройству. Тогда вместо root выдают именно его.
Второй случай - права на файлы: приложение пишет в каталог, принадлежащий root. Лечится сменой владельца каталога при сборке образа, а не выдачей прав в рантайме.
// Третий случай - приложение действительно требует полного root, и переписать его нельзя. Тогда оно изолируется отдельно: своя машина или свой узел, минимум соседей, отдельная сеть, усиленный мониторинг. И это фиксируется как осознанный риск с ответственным, а не тихо остаётся исключением, о котором через год никто не помнит.
- точечное разрешение
- выдать одно нужное право вместо полного root
- осознанный риск
- записанное исключение с ответственным, а не забытая настройка
Проверки в конвейере, которые работают
Конвейер - это набор шагов, которые проходит каждое изменение кода перед выкаткой, и проверки живут именно там. Работает проверка только если она блокирует, а не советует. Проверка, которая печатает предупреждение, через месяц перестаёт читаться. Но блокирующую проверку нельзя включать разом на весь код: она остановит всё, и её отключат в первый же горячий день.
Рабочий порядок такой. Сначала включить в режиме наблюдения и собрать список нарушений - обычно он неприятно длинный. Потом зафиксировать список как принятый долг и блокировать только НОВЫЕ нарушения: старое живёт, новое не проходит. Дальше разбирать долг по частям в спокойном темпе.
// Набор проверок, который окупается почти всегда: секреты в коде, известные уязвимости в зависимостях, запуск от root в описании сервиса, плавающие теги образов вместо отпечатков, открытые наружу порты. Каждая из них ловит целый класс ошибок, а не единичный случай. И у каждой должен быть понятный обход с обоснованием - без него люди начнут обходить сам конвейер.
- режим наблюдения
- проверка считает нарушения, но не блокирует; нужен для первого замера
- принятый долг
- зафиксированный список старых нарушений; блокируются только новые
Как отвечать: «Как укрепить запуск сервиса в контейнере?»
Есть короткий список, который применяется по умолчанию ко всем, и я его проверял на стенде построчно. Запуск от обычного пользователя вместо root: смена владельца чужого файла сразу становится невозможной. Отобрать все разрешения root и вернуть только нужные - большинству сервисов не нужно ни одного из четырнадцати, которые выдаются по умолчанию. Запретить повышение прав, потому что в образе остаются программы, работающие от владельца файла: в базовом debian я насчитал восемь таких, в alpine ноль. Файловая система только на чтение, а под временные файлы отдельный кусок памяти. Пределы по памяти и процессору. И отдельно: не пробрасывать внутрь сокет демона контейнеров, потому что это равносильно выдаче прав root на всю машину. Если какой-то сервис не запускается с этим профилем, разбираюсь, какое конкретно разрешение ему нужно, и выдаю только его.
Ответ даёт готовый список с проверенным эффектом каждой настройки и заканчивается тем, как поступать с исключениями. Это применимо в понедельник.
На чём валятся
- −Оставляют запуск от root по умолчанию, потому что «так заработало».
- −Пробрасывают сокет демона контейнеров ради удобства сборки.
- −Включают блокирующие проверки разом на весь код и получают их отключение в первый горячий день.
- −Ставят проверки в режиме советов и через месяц их никто не читает.
- −Оставляют исключение «этому сервису нужен root» без записи и ответственного.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Почему в образе просят прописать непривилегированного пользователя?A)Root внутри контейнера — это root на ядре хоста при побегеB)От root процессы работают медленнее из-за проверок ядраC)Иначе логи контейнера будут писаться от чужого владельцаD)Root не даёт контейнеру слушать порты выше тысячи
показать ответ и разбор
+A)Root внутри контейнера — это root на ядре хоста при побеге// разбор: Пользователи в контейнере и на хосте — это одни и те же числовые идентификаторы, если не включён user namespace. Значит, root в контейнере равен root на узле, и любая дыра в рантайме или неудачное монтирование превращается в полный доступ к машине. Плюс от root проще навредить самому себе: правки в системных каталогах образа, файлы с чужими правами на смонтированных томах.
- Какой набор ограничений ставят поду в кластере по умолчанию?A)Привилегированный режим с полным набором прав для совместимостиB)Корневая ФС на чтение, минимум прав и capabilitiesC)Доступ к сети хоста и монтирование его файловой системыD)Отключение проб и ограничений ресурсов ради скорости старта
показать ответ и разбор
+B)Корневая ФС на чтение, минимум прав и capabilities// разбор: Базовый профиль такой: контейнер не root, корневая файловая система только на чтение (данные — во временный том), запрещено повышение привилегий, набор capabilities урезан до необходимого, включён стандартный профиль seccomp. Всё это описывается в securityContext и проверяется на допуске в кластер, чтобы правила не зависели от аккуратности автора манифеста.
- Проверки безопасности в CI встроили, но команда их игнорирует: пайплайн всегда красный. Как чинить процесс?A)Отключить проверки и запускать их раз в квартал отдельноB)Оставить как есть: разработчики привыкнут к предупреждениямC)Блокировать узкий список, остальное — в отчётD)Перенести проверки на стадию после выкатки, чтобы не тормозить релизы
показать ответ и разбор
+C)Блокировать узкий список, остальное — в отчёт// разбор: Ворота работают, только пока их срабатывание означает «стоп». Практика: блокирует узкий и понятный набор — секрет в коде, критичная уязвимость с готовым исправлением, запуск от root, — а остальное копится в отчёт с ответственным и сроком. Дополнительно фиксируют базовый уровень: новые нарушения блокируют, накопленное чинят планово. Иначе красный цвет теряет смысл и вместе с ним пропадает вся польза.
- Образ собирают на полном ubuntu с компилятором, shell и кучей утилит. Чем плоха такая база для прода?A)Большая поверхность атаки; берут минимальный/distroless образB)Полный образ медленнее запускает главный процессC)Ubuntu требует платной подписки для продаD)На полном образе не работает проброс портов
показать ответ и разбор
+A)Большая поверхность атаки; берут минимальный/distroless образ// разбор: Толстая база (полный дистрибутив с компилятором, shell, пакетным менеджером, утилитами) — это большая поверхность атаки: каждый лишний пакет несёт свои уязвимости, а shell и инструменты помогают атакующему после пробоя (разведка, скачивание, эскалация). Берут минимальную базу (distroless, alpine, scratch) — только рантайм и приложение, без shell и лишнего. Меньше компонентов — меньше CVE и меньше возможностей для атаки внутри контейнера.
- Контейнер работает с дефолтным набором Linux capabilities. Как сузить его права по харденингу?A)Запустить контейнер с флагом --privileged для стабильностиB)Выдать все capabilities, чтобы не ловить отказы в правахC)Сбросить все capabilities и добавить обратно лишь нужныеD)Оставить дефолт: он и так уже безопасен
показать ответ и разбор
+C)Сбросить все capabilities и добавить обратно лишь нужные// разбор: Linux capabilities дробят права root на отдельные (NET_ADMIN, SYS_TIME и т.д.). По умолчанию контейнеру выдаётся набор, где часть возможностей ему не нужна. Харденинг: drop ALL и добавить обратно только реально необходимые (например, NET_BIND_SERVICE для порта <1024). Плюс no-new-privileges, seccomp-профиль, read-only rootfs, непривилегированный USER. Так поверхность привилегий сжимается до минимума, а --privileged делает ровно противоположное.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.