[Перевод] Повышаем безопасность контейнеров Docker

d3a9931febfd4aa093d434b5d60a885c.jpeg
— Сударь, каким образом вас взломали?
 — Не образом, а контейнером.
Старинный анекдот

Все лишние компоненты компьютерной системы могут оказаться источником совершенно необязательных уязвимостей. Поэтому образы контейнеров должны по возможности содержать только то, что нужно приложению. И их размер имеет значение не только с точки зрения удобства дистрибуции, но также стоимости владения и безопасности. В этой статье мы поговорим о методах минимизации размера и поверхности атаки образов Docker, а также об инструментах их сканирования на предмет наличия уязвимостей.


Тот, кто хоть немного работал с Docker, наверняка слышал про образ alpine. Он создан на основе дистрибутива Alpine Linux, который по сравнению, например, с Debian или Ubuntu при размере базового образа в 5 Мб оставляет взломщикам гораздо меньше возможностей для атаки. Если приложение сможет работать в alpine, это будет отличным способом оптимизации.


А что насчет бинарных файлов? Может ли приложение работать автономно? Если да, то есть основания рассчитывать на дополнительное уменьшение размера. В качестве базового для таких образов, как Debian и Ubuntu, обычно используется scratch, но в нем также может заработать приложение на golang. Gianluca Arbezzano создал репозиторий с готовыми бинарными файлами минимального размера. Давайте попробуем linux_386.


8aa74680d97b44b59e97bf4cf8dc6ff2.jpeg
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.


71bd0c1602064835bd96169a5bf22a90.png

Они также используют оповещения для автоматического запуска процедуры сборки/пересборки в случае изменения базовых образов. Такая функциональность есть и в 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

Сгенерированный отчет выглядит вот так:


898023548f8e4f5b89d2a0c5e0b96aff.jpeg

Удаление потенциально уязвимых сборочных зависимостей 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), которые также удобны для добавления в образы динамических метаданных.


00336c40abaf43ab9c60ba9d3bb389f2.jpeg

Заключение


Итак, мы коротко рассмотрели способы минимизации образов Docker для приложений на Go и Ruby, научились запускать контейнеры под обычным пользователем, настроили сканирование безопасности с помощью Clair и немного поговорили об автоматической пересборке. Надеюсь, эти простые шаги помогут повысить безопасность ваших контейнеров Docker. На этом пока все. Спасибо за внимание!


Список источников:


  1. https://medium.com/microscaling-systems/dockerfile-security-tuneup-166f1cdafea1#.a24qq9tv7
  2. http://gianarb.it/blog/about-your-images-security-tips

Комментарии (0)

© Habrahabr.ru