EnglishРусский Map

Харденинг контейнерных образов

title
Харденинг контейнерных образов
type
summary
summary
Майк Кардвелл пересобирает образ PowerDNS Recursor в два бинарника на scratch с Landlock, seccomp, ежедневной пересборкой, SBOM, VEX и cosign
tags
security, containers, supply-chain, sandboxing
created
2026-09-13
updated
2026-09-13
lang
ru
source_updated
2026-09-13
translated
2026-09-14
translator
lllm/antigravity/gemini-3.7-flash-medium

Майк Кардвелл развернул сервер на Flatcar Container Linux и решил, что каждый запускаемый контейнер должен быть защищён с самого начала. Первым подопытным стал PowerDNS Recursor. В своей статье он разбирает отличия получившегося решения от официального образа. По его словам, дело не в претензиях конкретно к PowerDNS: аналогичная картина наблюдается у большинства образов.

Меньше лишнего в образе

Официальный образ построен на Debian с добавлением pdns-recursor: это около 355 МБ и 7500 файлов. В проекте Кардвелла pdns-recursor-trimmed рекурсор компилируется статически с musl внутри сборочного контейнера Alpine, исходный архив проверяется по официальным PGP-ключам релизов PowerDNS, а получившиеся бинарники копируются в образ scratch. На старте это дало 36 МБ и всего два файла: pdns_recursor и rec_control. При компрометации процесса атакующий не найдёт ни командной оболочки, ни системных утилит, что исключает готовые бинарники, нужные для атак класса living-off-the-land.

Он также отключил при сборке поддержку SNMP и опцию url для zoneToCache. Обе тянут за собой зависимости с длинной историей CVE. По его оценке, мало кому они действительно нужны, хотя он и допускает исключения. Главной причиной он называет даже не саму поверхность атаки, а шум от сканеров уязвимостей, о котором речь пойдёт ниже.

Ограничение процесса

Точкой входа в контейнер служит небольшой launcher на Си. Перед запуском pdns он накладывает правила Landlock: доступ на чтение только к тем путям, где это необходимо (по сути только конфиг, без /proc), доступ на запись строго туда, где она происходит, а также ограничения на исходящие и входящие TCP-порты и, на достаточно свежих ядрах, на привязку UDP-портов. Это предотвращает запись и запуск вредоносного кода, а также блокирует соединения с типовыми портами управления. Подобный сброс привилегий (privilege-dropping) выполняется на уровне launcher'а, а не внутри приложения, что позволяет применить его к сторонней программе, не переписывая её код.

Системные вызовы ограничиваются отдельно - через белый список seccomp.json, который лежит вне образа и подключается из compose-файла или манифеста Kubernetes. Кардвелл сформировал его с помощью strace и проверил обширными smoke-тестами на разных архитектурах и ядрах. Вызов memfd_create исключён, так как это основной бестелесный способ обойти ограничения образа без оболочки и с запретом на исполнение.

Параметры запуска довершают изоляцию: доступная только для чтения корневая файловая система (на случай, если ядро не поддерживает Landlock), крошечный tmpfs с флагами noexec,nosuid,nodev на /tmp под единственный сокет для rec_control, сброс всех capabilities, отключённый IPC, настройка net.ipv4.ip_unprivileged_port_start в значение 53 (чтобы непривилегированный пользователь мог слушать DNS-порт) и флаг no-new-privileges. В заметке go-privilege-dropping описана реализация тех же механизмов Landlock и seccomp изнутри программы на Golang.

Ежедневная пересборка

Официальный образ фиксирует состояние Debian на момент сборки. CI в GitLab у Кардвелла отслеживает две последние стабильные ветки (5.4.x и 5.3.x на момент написания), ежедневно пересобирает самые свежие версии каждой из них и пушит изменения только при наличии реальных обновлений. Временные метки файлов зафиксированы на значении эпохи Unix, чтобы идентичная сборка не считалась изменением. Когда в OpenSSL выходит критический фикс, сборка следующего дня автоматически подтягивает его из Alpine. Поэтому тег 5.4.6 смещается между сборками, тег 5.4.6_<hash> жёстко фиксирует конкретную версию, а 5.4 следует за всей веткой.

Каждый прогон запускает smoke-тесты для четырёх образов (amd64 и arm64 для текущей и предыдущей веток). В ядрах GitLab runner'ов Landlock не поддерживается, поэтому сокращённый набор тестов дополнительно запускается на пяти виртуальных машинах под каждую архитектуру с разными ядрами, подтверждая корректность работы Landlock и seccomp.

Поддержка сканеров уязвимостей

Trivy, Grype и OSV Scanner в основном опираются на пакетные базы дистрибутивов, а в образе scratch их нет, поэтому сканеры не находят ничего. При сборке генерируется SBOM на основе данных линковщика в формате CycloneDX и зашивается внутрь образа, а копия в формате SPDX прикрепляется как аттестация вместе с подтверждением происхождения SLSA (где фиксируются git-ревизия, Dockerfile и сборочные аргументы).

Кардвелл отмечает, что официальный образ также невидим для сканеров, но иначе: pdns-recursor не входит в список пакетов Debian, поэтому уязвимости PowerDNS останутся незамеченными, как и уязвимости во встроенных Rust-крейтах и стороннем JavaScript. Его SBOM включает их все. В итоге сканеры сразу выдали критические и высокие CVE во встроенном Handlebars.js. Рассудив, что в текущем сценарии использования эти уязвимости неприменимы, он зафиксировал это в файле OpenVEX. Этот файл хранится в git, передаётся сканерам в CI и прикрепляется отдельной аттестацией. После каждой сборки пайплайн сканирует свежие образы и завершается ошибкой при любых находках, не покрытых VEX-файлом, что позволяет оперативно узнавать о новых критичных CVE.

Подпись

В процессе сборки GitLab генерирует краткосрочный токен OIDC, который обменивается через Fulcio на сертификат подписи. Утилита cosign подписывает дайджест мультиархитектурного индекса (что также покрывает SBOM, метаданные происхождения и аттестации VEX), после чего подпись фиксируется в журнале прозрачности Rekor. Любой желающий может проверить, что образ собран пайплайном из ветки main:

cosign verify \
  --certificate-identity \
    'https://gitlab.com/grepular/pdns-recursor-trimmed//.gitlab-ci.yml@refs/heads/main' \
  --certificate-oidc-issuer https://gitlab.com \
  grepular/pdns-recursor-trimmed:5.4

Скомпрометированный аккаунт на Docker Hub не сможет выложить поддельный образ без сбоя верификации. Компрометация аккаунта GitLab к этому бы привела, но там включена 2FA. Это пример применения аттестаций в рамках безопасности цепочки поставок (supply-chain-security) сторонним сборщиком без постоянных ключей подписи, по схеме короткоживущих учётных данных (ephemeral-credentials).

Два небольших дополнения

Поскольку в образе нет ни dig, ни curl, стандартной проверке работоспособности нечего вызвать. Поэтому команда /launcher --healthcheck отправляет настоящий DNS-запрос и возвращает успех при получении любого корректного ответа. А на случай, если ежедневный пайплайн тихо остановится (например, закончатся минуты runner'ов), финальным шагом отправляется пинг в healthchecks.io, поднимающий тревогу, если за сутки сигнал не поступил.

Ограничения внешней поддержки

Итог Кардвелла: меньше зависимостей и они свежее, меньше возможностей для повышения привилегий при взломе и быстрое обнаружение известных уязвимостей. При этом он отмечает цену внешней поддержки проекта. Новый релиз может добавить зависимость в формате, который не распознает генератор SBOM, или внедрить возможность, требующую системного вызова, заблокированного списком seccomp и пропущенного smoke-тестами. Единственная защита - вычитывать каждый релиз, чему и посвящена его статья. Давление шума от сканеров, вынуждающее мейнтейнеров постоянно обновлять зависимости, описано в unmaintained-scanner-pressure, а ежедневная пересборка представляет собой альтернативу подходу с фиксацией зависимостей (dependency-vendoring): отказ от неизменного аудированного набора зависимостей ради автоматического получения исправлений безопасности.