— Сударь, каким образом вас взломали?
— Не образом, а контейнером.
Старинный анекдот
Все лишние компоненты компьютерной системы могут оказаться источником совершенно необязательных уязвимостей. Поэтому образы контейнеров должны по возможности содержать только то, что нужно приложению. И их размер имеет значение не только с точки зрения удобства дистрибуции, но также стоимости владения и безопасности. В этой статье мы поговорим о методах минимизации размера и поверхности атаки образов Docker, а также об инструментах их сканирования на предмет наличия уязвимостей.
Тот, кто хоть немного работал с Docker, наверняка слышал про образ alpine. Он создан на основе дистрибутива Alpine Linux, который по сравнению, например, с Debian или Ubuntu при размере базового образа в 5 Мб оставляет взломщикам гораздо меньше возможностей для атаки. Если приложение сможет работать в alpine, это будет отличным способом оптимизации.
А что насчет бинарных файлов? Может ли приложение работать автономно? Если да, то есть основания рассчитывать на дополнительное уменьшение размера. В качестве базового для таких образов, как Debian и Ubuntu, обычно используется scratch
, но в нем также может заработать приложение на golang. Gianluca Arbezzano создал репозиторий с готовыми бинарными файлами минимального размера. Давайте попробуем linux_386.
curl -SsL https://github.com/gianarb/micro/releases/download/1.0.0/micro_1.0.0_linux_386 > micro
Мы можем включить этот бинарный файл в scratch-образ с помощью вот такого Dockerfile:
FROM scratch
ADD ./micro /micro
EXPOSE 8000
CMD ["/micro"]
docker build -t micro-scratch .
docker run -p 8000:8000 micro-scratch
В итоге нам удалось запустить http-приложение в образе размером всего 5 Мб, то есть мы уменьшили его более чем в два раза по сравнению 12 Мб, которые занимает образ, созданный на основе alpine.
Scratch-образ невозможно использовать с любыми приложениями, но ради снижения накладных расходов стоит попытаться.
Для Ruby в качестве базового можно использовать ruby:2.3-alpine. Ruby в нем устанавливается из исходников, а не из пакета Alpine. С учетом семантического версионирования релиз 2.3 будет получать обновления безопасности напрямую от разработчиков.
В противном случае пришлось бы самостоятельно устанавливать Ruby из исходников и отслеживать выход новых версий или использовать пакет из состава Alpine и следить за его обновлением силами разработчиков дистрибутива.
Уведомления и веб-хуки
Если для базового образа выпущено обновление безопасности, необходимо обновить те образы, которые на нем основаны. Здесь могут помочь уведомления MicroBadger, которые можно отправлять, например, в Slack, как это делают ребята из Microscaling для официальных образов alpine и ruby.
Они также используют оповещения для автоматического запуска процедуры сборки/пересборки в случае изменения базовых образов. Такая функциональность есть и в Docker Hub, но Microscaling утверждает, что MicroBadger лучше, так как может использоваться с любой системой, поддерживающей веб-хуки (например, CI или сканером безопасности).
Обычный пользователь
Одним из ключевых отличий виртуальных машин от контейнеров является использование последними ядра основной системы. По умолчанию контейнеры Docker запускаются с привилегиями root, что может привести к серьезным проблемам в случае прорыва изоляции, так как запущенный под рутом скомпрометированный контейнер может получить root-доступ к основной системе.
Однако риски можно уменьшить, запустив контейнер под обычным пользователем. Вот как это сделать для Rails-приложения:
# Создаем рабочий каталог
WORKDIR /app
# Копируем код Rails-приложения в образ
COPY . ./
# Создаем обычного пользователя, устанавливаем права и меняем юзера
RUN addgroup rails && adduser -D -G rails rails \
&& chown -R rails:rails /app
USER rails
Сканирование безопасности
Помимо непосредственно хранения реестры контейнеров могут сканировать загружаемые в них образы на наличие уязвимостей. Например, Docker проводит сканирование безопасности официальных, а также пользовательских образов, загруженных в Docker Cloud.
Для сканирования безопасности образов реестр Quay.io использует Clair — продукт с открытым исходным кодом от CoreOS. Совсем недавно в Clair была добавлена поддержка Alpine, что на самом деле очень здорово. Будем надеяться, что эта функциональность скоро будет доступна и в Quay. Помимо Clair существуют сканеры TwistLock и Aqua, но в большинстве случаев за их использование надо платить.
Clair — это приложение на Golang, которое реализует набор HTTP API для выгрузки, загрузки и анализа образов. Данные об уязвимостях загружаются из различных источников, таких как Debian Security Tracker или RedHat Security Data, и сохраняются в Postgres. Clair работает по принципу статического анализатора, поэтому, чтобы просканировать контейнер, его не надо запускать — проверяется лишь файловая система образа.
docker run -it -p 5000:5000 registry
С помощью этой команды мы запустили собственный реестр, чтобы использовать его в качестве источника образов для сканирования. Давайте попробуем загрузить в него образ micro
от Gianluca Arbezzano:
docker pull gianarb/micro:1.0.0
docker tag gianarb/micro:1.0.0 localhost:5000/gianarb/micro:1.0.0
docker push localhost:5000/gianarb/micro:1.0.0
Далее установим Clair.
mkdir $HOME/clair-test/clair_config
cd $HOME/clair-test
curl -L https://raw.githubusercontent.com/coreos/clair/v1.2.2/config.example.yaml -o clair_config/config.yaml
curl -L https://raw.githubusercontent.com/coreos/clair/v1.2.2/docker-compose.yml -o docker-compose.yml
Пропишите в $HOME/clair_config/config.yml
ваши настройки подключения к базе данных postgresql://postgres:password@postgres:5432?sslmode=disable
Для запуска Postgres и Clair нужно выполнить следующую команду:
docker-compose up
Чтобы облегчить процедуру тестирования, воспользуемся CLI под названием Hyperclair (это клиент для работы с Clair). Ниже приведены команды для Mac OS (если вы используете другую ОС, см. https://github.com/wemanity-belgium/hyperclair/releases):
curl -SSl https://github.com/wemanity-belgium/hyperclair/releases/download/0.5.2/hyperclair-darwin-386 > ~/hyperclair
chmod 755 ~/hyperclair
Теперь у нас в ~/hyperclair есть исполняемый файл:
~/hyperclair pull localhost:5000/gianarb/micro:1.0.0
~/hyperclair push localhost:5000/gianarb/micro:1.0.0
~/hyperclair analyze localhost:5000/gianarb/micro:1.0.0
~/hyperclair report localhost:5000/gianarb/micro:1.0.0
Сгенерированный отчет выглядит вот так:
Удаление потенциально уязвимых сборочных зависимостей Rails-приложения
Поскольку Ruby является интерпретируемым языком, при использовании написанного на нем приложения у нас набирается приличное количество зависимостей. Надо установить все Ruby-гемы, которые нужны для нашего приложения, и все пакеты операционной системы, необходимые для этих гемов.
Предположим, что сканирование выявило критические уязвимости в libxml2 и libxslt. Это buildtime-зависимости гема Nokogiri, который является XML- и JSON-парсером. С целью увеличения производительности этот гем использует написанные на Си расширения, требующие компиляции. Но после того как гем установлен, libxml2 и libxslt больше не нужны.
Давайте удалим все buildtime-зависимости:
# Кеш для установки гемов
WORKDIR /tmp
ADD Gemfile* /tmp/
# Обновляем и устанавливаем все необходимые пакеты
# В конце удаляем используемые для сборки пакеты и apk-кеш
RUN apk update && apk upgrade && \
apk add --no-cache $RUBY_PACKAGES && \
apk add --no-cache --virtual build-deps $BUILD_PACKAGES && \
bundle install --jobs 20 --retry 5 && \
apk del build-deps
За счет кеширования Gemfile и Gemfile.lock в /tmp команда bundle install
запустится только в случае изменения Gemfile. В противном случае будет использован кеш Docker. Такая оптимизация позволяет уменьшить время выполнения и нагрузку на сеть, которые при установке гемов могут быть достаточно велики.
Заметьте, что команда run
многострочная, и поэтому в образ добавляется только один слой. Необходимые для сборки пакеты устанавливаются с ключом --virtual, и их легко удалить после завершения процесса.
Автоматизированная сборка
С точки зрения безопасности контейнеров крайне важно пересобирать их каждый раз, когда появляется обновление самого образа или одного из тех, что лежат в его основе. Автоматизация этой процедуры может быть основана на привязке к git-репозиторию: в этом случае сборка запускается после создания нового коммита в отслеживаемой ветке. Как было упомянуто выше, сборку можно также запустить по событию изменения базового образа.
В случае Ruby ситуация упрощается, так как мы можем взять те же самые файлы Dockerfile, которые использовали в процессе создания. Для программ на Go сначала нужно скомпилировать бинарный файл, а затем уже добавлять его в образ. Локально для этого можно использовать makefile.
Альтернативным вариантом будет компиляция бинарного файла по событию в docker-контейнере. Рекомендую посмотреть на пару golang-builder-образов от CenturyLinkLabs и Prometheus.
Для запуска процесса сборки можно использовать сборочные хуки (build hooks), которые также удобны для добавления в образы динамических метаданных.
Заключение
Итак, мы коротко рассмотрели способы минимизации образов Docker для приложений на Go и Ruby, научились запускать контейнеры под обычным пользователем, настроили сканирование безопасности с помощью Clair и немного поговорили об автоматической пересборке. Надеюсь, эти простые шаги помогут повысить безопасность ваших контейнеров Docker. На этом пока все. Спасибо за внимание!
Список источников: