Менеджер по развитию

Безопасность и Docker-контейнеры

Содержание:

1. Конфигурация сервера с Docker Engine

2. Написание безопасных образов в Docker

3. Аудит безопасности Docker-окружения на сервере

1 Конфигурация сервера с Docker Engine

1.1 Запрещаем контейнерам менять свои привилегии

В /etc/docker/daemon.json (если нет — создать):

"no-new-privileges": true

1.2 Запретить удалённый доступ к Docker Daemon

Если удалённый доступ необходим, то обязательно использовать сертификаты для любого соединения извне. Как сгенерировать сертификат и использовать его можно прочитать здесь.

1.3 Использовать SELinux или AppArmor

AppArmor и SELinux — это модули безопасности ядра Linux, ограничивающий программам доступы с помощью индивидуальных профилей, политик безопасности.

SELinux для CentOS, AppArmor для Debian,

  • Для SELinux в /etc/docker/daemon.json (если нет — создать):

{
  "selinux-enabled": true
}

  • Для Debian при запуске контейнера добавить директиву --security-opt :

$ docker run \
  --interactive \
  --tty \
  --rm \
  --security-opt apparmor=docker-default \
  myapp

2 Написание безопасных образов в Docker

2.1 Не хранить чувствительные данные в образе приложения

Чтобы этого избежать, достаточно передавать переменные во время запуска контейнера

$ docker run \\
      --env-file .env_local \\
      ...

Хранить их лучше в GitLab CI/CD variables и вытаскивать во время сборки, либо положить их на хостовой машине в .env файл.

2.2 Использовать минимальные образы в качестве источника

openjdk:<version> и openjdk:<version>-slim вместе с -alpine могут отличаться друг от друга в размере на десятки и сотни мегабайт, они собираются быстрее, но не стоит забывать и о том, что в таких образах зачастую меньше сторонних сервисов, соответственно и меньше уязвимостей.

2.3 Использовать непривилегированного пользователя

Если в Dockerfile не указана директива USER, то используется по умолчанию root, который при грамотном написании Dockerfile ему совсем и не нужен. Рискованно использовать root из-за потенциального доступа контейнера к хосту и эскалации привилегий. Перестраховаться можно, приведя ваш Dockerfile к такому виду:

FROM something:10-alpine
RUN mkdir /app
RUN groupadd -r  && useradd -r -s /bin/false -g testuser testuser
WORKDIR /app
COPY . /app
RUN chown -R lirantal:lirantal /app
USER testuser
CMD sleep 1000

Выше мы создаём пользователя без пароля, без домашней директории и без оболочки bash, даем ему права на нужную нам директорию с приложением и меняем текущего пользователя root на нашего testuser.

2.4 Использовать тэги для источника в Dockerfile

По-умолчанию в Dockerfile будет использоваться :latest - что зачастую может вызвать конфликт версий, неожиданное поведение сборки, новые баги и прочее. Следует проверять сборку перед тем, как перейти на новую версию и всегда указывать её в Dockerfile. Посмотреть существующие теги можно на hub.docker.com

2.5 Использовать COPY вместо ADD

Функционально эти команды очень похожи - они нужны для копирования файла с хоста в образ приложения для дальнейшей сборки, но существуют некоторые различия, почему COPY использовать безопаснее.

ADD принимает для значения удалённые URLs, автоматически распаковывает принимаемые архивы, что является приемлемым, но при широком использовании может послужить уязвимостью для zip-бомб и man-in-the-middle атак.

2.6 Использовать multi-stage во время сборки Dockerfile

С помощью multi-stage сборки можно не только сократить размер итогового образа, но и избавить его от чувствительной информации, используя её только в процессе сборки.

FROM: ubuntu as intermediate

WORKDIR /app
COPY secret/key /tmp/
RUN scp -i /tmp/key build@acme/files .

FROM ubuntu
WORKDIR /app
COPY --from=intermediate /app .

2.7 Использовать .dockerignore

Чтобы случайно не скопировать ненужные файлы в образ приложения, например, приватные ключи или локальные настройки - достаточно добавить в корень приложения рядом с Dockerfile файл .dockerignore с именами файлов. Также можно использовать маску, например, *.key

3 Аудит безопасности Docker-окружения на сервере

3.1 Смотрим на Docker Bench Security

Это набор скриптов, проверяющий запущенные контейнеры на соблюдение лучших практик, стартануть его можем из консоли:

$ docker run -it --net host --pid host \
       --userns host --cap-add audit_control \
       -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
       -v /etc:/etc:ro \
       -v /usr/bin/docker-containerd:/usr/bin/docker-containerd:ro \
       -v /usr/bin/docker-runc:/usr/bin/docker-runc:ro \
       -v /usr/lib/systemd:/usr/lib/systemd:ro \
       -v /var/lib:/var/lib:ro \
       -v /var/run/docker.sock:/var/run/docker.sock:ro \
       --label docker_bench_security \
       docker/docker-bench-security

Более подробно можно почитать тут: Docker Bench Security

3.2 Исправляем частые ошибки

Во время выполнения контейнера в терминал будут сыпаться многочисленные ворнинги, разберём самые частые из них.

[WARN] PIDs limit not set

Необходимо для защиты от форк-бомб, решается флагом во время запуска контейнера:

$ docker container run --pids-limit 100 example-image

[WARN] No Healthcheck found

В Dockerfile:

HEALTHCHECK CMD curl --fail <http://localhost:5000/> || exit 1

[WARN] Running as root

В Dockerfile:

RUN useradd -u 8877 maxim
USER maxim

[INFO] Container in docker0 network

Через сеть docker0, которая задаётся докером по умолчанию, контейнеры могут взаимодействовать между собой. Чтобы этого избежать, можно использовать флаг —icc=false при запуске докер сервиса, либо запускать ваш контейнер в отдельной сети:

$ docker run -itd --network=busybox-net busybox

Заключение

Думаю, все эти и остальные ошибки можно загуглить за пару минут и впасть в кататонический ступор от того, как всё работает неправильно. Такие заморочки нужны далеко не всегда, но когда стоит задача обезопасить сервис, значит, предстоит долгий путь по исправлению.

Всё вернётся сторицей опыта и пониманием того, как оно должно работать.