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

Все лишние компоненты компьютерной системы могут оказаться источником совершенно необязательных уязвимостей. Поэтому образы контейнеров должны по возможности содержать только то, что нужно приложению. И их размер имеет значение не только с точки зрения удобства дистрибуции, но также стоимости владения и безопасности. В этой статье мы поговорим о методах минимизации размера и поверхности атаки образов 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. На этом пока все. Спасибо за внимание!


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


  1. https://medium.com/microscaling-systems/dockerfile-security-tuneup-166f1cdafea1#.a24qq9tv7
  2. http://gianarb.it/blog/about-your-images-security-tips
Поделиться с друзьями
-->

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


  1. grossws
    31.01.2017 12:00
    +1

    Предположим, что сканирование выявило критические уязвимости в libxml2 и libxslt. Это buildtime-зависимости гема Nokogiri, который является XML- и JSON-парсером. С целью увеличения производительности этот гем использует написанные на Си расширения, требующие компиляции. Но после того как гем установлен, libxml2 и libxslt больше не нужны.

    В современном nokogiri libxml2 забандлен, но до этого (сейчас только с --use-system-libraries) умеет линковаться с системной libxml2 и libxslt. И, естественно, использует их в рантайме, иначе нафига они сдались?


    proof
    gross@unterwelt [11:54:50] [~/experiments] 
    -> % gem install nokogiri -v1.6.7.2 -- --use-system-libraries
    Building native extensions with: '--use-system-libraries'
    This could take a while...
    Successfully installed nokogiri-1.6.7.2
    Parsing documentation for nokogiri-1.6.7.2
    Installing ri documentation for nokogiri-1.6.7.2
    Done installing documentation for nokogiri after 4 seconds
    1 gem installed
    gross@unterwelt [11:55:12] [~/experiments] 
    -> % ldd ~/.rvm/gems/ruby-2.3.0/gems/nokogiri-1.6.7.2/ext/nokogiri/nokogiri.so
            linux-vdso.so.1 (0x00007fff385a1000)
            libruby.so.2.3 => /home/gross/.rvm/rubies/ruby-2.3.0/lib/libruby.so.2.3 (0x00007f16a44bc000)
            libxml2.so.2 => /usr/lib/libxml2.so.2 (0x00007f16a4107000)
            libxslt.so.1 => /usr/lib/libxslt.so.1 (0x00007f16a3ec6000)
            libz.so.1 => /usr/lib/libz.so.1 (0x00007f16a3caf000)
            liblzma.so.5 => /usr/lib/liblzma.so.5 (0x00007f16a3a89000)
            libm.so.6 => /usr/lib/libm.so.6 (0x00007f16a3785000)
            libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f16a357f000)
            libexslt.so.0 => /usr/lib/libexslt.so.0 (0x00007f16a3369000)
            libgcrypt.so.20 => /usr/lib/libgcrypt.so.20 (0x00007f16a305a000)
            libgpg-error.so.0 => /usr/lib/libgpg-error.so.0 (0x00007f16a2e46000)
            libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f16a2c29000)
            libgmp.so.10 => /usr/lib/libgmp.so.10 (0x00007f16a2996000)
            libcrypt.so.1 => /usr/lib/libcrypt.so.1 (0x00007f16a275c000)
            libc.so.6 => /usr/lib/libc.so.6 (0x00007f16a23be000)
            /usr/lib64/ld-linux-x86-64.so.2 (0x000055d2993e0000)


  1. Wolverine
    04.02.2017 17:11

    По умолчанию контейнеры Docker запускаются с привилегиями root, что может привести к серьезным проблемам в случае прорыва изоляции, так как запущенный под рутом скомпрометированный контейнер может получить root-доступ к основной системе.

    USER rails


    Расскажите пожалуйста каким образом это может произойти? Зашли мы в наш контейнер через docker exec и сидим из под root, что мы можем сделать деструктивного с хост-машиной?


    1. gekk0
      04.02.2017 22:50
      +1

      «В выпуске cистемы управления контейнерной виртуализацией Docker 1.12.6 устранена опасная уязвимость (CVE-2016-9962), позволяющая получить доступ к хост-системе из изолированного контейнера. Уязвимость вызвана недоработкой в runtime runC, который используется по умолчанию начиная с ветки Docker 1.11, и также применяется в некоторых других системах»
      https://www.opennet.ru/opennews/art.shtml?num=45848


      1. Wolverine
        05.02.2017 15:12

        Спасибо. В целом получается, что лучше пользователя сменить, на случай вот таких багов.