Эта статья содержит рекомендации по написанию Dockerfile и принципам безопасности контейнеров и некоторые другие связанные темы, например про оптимизацию образов.
Если вы знакомы с контейнеризованными приложениями и микросервисами, то скорее всего понимаете, что хотя ваши сервисы "микро", но поиск уязвимостей и устранение проблем с безопасностью способен затруднить управление вашими сервисами, уже с приставкой "макро".
К счастью, большинство потенциальных проблем мы можем решить еще на этапе разработки.
Хорошо подготовленный Dockerfile исключает необходимость использовать привилегированные контейнеры, открывать порты, в которых нет необходимости, включать лишние пакеты и избегать утечки чувствительных данных. Старайтесь решить эти проблемы сразу, это поможет в дальнейшем сократить усилия на поддержку ваших приложений.
Избегать избыточных привилегий
Этот совет следует принципу наименьших привилегий, таким образом ваше приложение получает доступ только к тем ресурсам и данным, которые ему необходимы для работы.
1. Rootless контейнеры
Отчет sysdig показал, что 58% образов выполняют процесс в контейнере от root (UID 0). Рекомендуем избегать этого. Существует очень небольшой круг задач, для решения которых нужно запускать контейнер от root, поэтому не забывайте добавлять команду USER
и менять UID пользователя на non-root.
Более того ваша среда выполнения контейнеров может по умолчанию блокировать запуск процессов в контейнере от имени root (например, Openshift требует дополнительные SecurityContextConstraints
).
Чтобы настроить non-root контейнер вам потребуется выполнить несколько дополнительных шагов в вашем Dockerfile.
-
Необходимо убедится, что пользователь, указанный в команде
USER
существует внутри контейнера. -
Предоставить необходимые разрешения на объекты файловой системы, которые процесс читает или записывает.
FROM alpine:3.12
# Create user and set ownership and permissions as required
RUN adduser -D myuser && chown -R myuser /myapp-data
# ... copy application files
USER myuser
ENTRYPOINT ["/myapp"]
Вы можете увидеть контейнеры, которые начинаются как root, а затем используют gosu или su-exec для перехода к обычному пользователю.
Если необходимо выполнить команду от имени root, вы можете использовать sudo.
Приведенные рекомендации, намного лучше запуска от имени root, но они работают не во всех окружениях, например OpenShift.
2. Не делайте привязку к определенному UID
Запускайте контейнеры без полномочий root, но также не делайте этот UID пользователя обязательным. Почему?
-
OpenShift по умолчанию использует произвольные UID при запуске контейнеров.
-
Принудительное использование определенного UID требует изменения прав при монтировании папок с хостовой системы. Если вы запустите контейнер (параметр -u в докере) с UID хоста, это может нарушить работу службы при попытке чтения или записи из папок в контейнере.
...
RUN mkdir /myapp-tmp-dir && chown -R myuser /myapp-tmp-dir
USER myuser
ENTRYPOINT ["/myapp"]
Возникнут проблемы при запуске этого контейнера с UID отличающимся от myuser, так как приложение не сможет записывать в папку/myapp-tmp-dir
folder.
Не нужно жестко задавать путь только для пользователя myuser. Вместо этого можно записать временные данные в /tmp (где любой пользователь может писать, благодаря разрешениям sticky bit). Сделайте ресурсы доступными для чтения (0644 вместо 0640) и убедитесь, что все работает, если UID измениться.
...
USER myuser
ENV APP_TMP_DATA=/tmp
ENTRYPOINT ["/myapp"]
В этом примере наше приложение будет использовать путь из переменной среды APP_TMP_DATA. Путь /tmp позволяет приложению запускаться от любого UID и продолжить записывать временные данные в папку /tmp. Наличие пути в переменной окружения не обязательно, но позволяет избежать проблем при настройке и монтировании разделов для долговременного хранения данных.
3. Назначить root владельцем исполняемых файлов и запретить изменять эти файлы
Рекомендуется назначать root владельцем каждого исполняемого файла в контейнере, даже если он выполняется пользователем без полномочий root. Также исполняемые файлы не должны быть доступны на запись всем пользователям.
Это предотвратить возможность изменения существующих бинарных файлов и скриптов, что может быть использована при различных атаках. Следуя этой рекомендации контейнер должен оставаться неизменяемым. Такой контейнер не изменяет код приложения во время выполнения, что позволяет избежать ситуации, при которой запущенное приложение случайно или злонамеренно изменяется.
Следуя этой рекомендации старайтесь избегать подобной ситуации:
...
WORKDIR $APP_HOME
COPY --chown=app:app app-files/ /app
USER app
ENTRYPOINT /app/my-app-entrypoint.sh
В большинстве случаев вы можете не использовать опцию--chown app:app
(в том числе запуск команды RUN chown
). Пользователю app требуются только права на выполнение, при этом быть владельцем файла не обязательно.
Уменьшение поверхности атаки
Старайтесь минимизировать размер образа.
Избегайте установки неиспользуемых пакетов или открытия лишних портов - это может увеличить поверхность атаки. Чем больше компонентов вы включите в контейнер, тем более уязвимой будет ваша система и тем сложнее будет ее обслуживать, особенно для компонентов, не находящихся под вашим контролем.
4. Многоступенчатые сборки
Используйте многоступенчатые сборки (multi-stage builds), чтобы компилировать ваши приложения внутри контейнеров.
При таком подходе используется промежуточный контейнер, который содержит все необходимые инструменты для компиляции артефактов (таких как бинарные файлы). После этого вы копируете в итоговый образ только необходимы артефакты без лишних инструментов, зависимостей и временных файлов.
Хорошо подготовленная многоступенчатая сборка содержит лишь необходимые бинарные файлы и зависимости в итоговом образе и не содержит инструментов для сборки и промежуточных файлов. Это уменьшает поверхность атаки, уменьшая уязвимости.
Это безопасно, а также уменьшает размер образа.
Пример многоступенчатой сборки для приложения на go:
#This is the "builder" stage
FROM golang:1.15 as builder
WORKDIR /my-go-app
COPY app-src .
RUN GOOS=linux GOARCH=amd64 go build ./cmd/app-service
#This is the final stage, and we copy artifacts from "builder"
FROM gcr.io/distroless/static-debian10
COPY --from=builder /my-go-app/app-service /bin/app-service
ENTRYPOINT ["/bin/app-service"]
В этом Dockerfile на первом этапе мы создаем контейнер из образа golang:1.15, который содержит необходимые инструменты.
FROM golang:1.15 as builder
Мы можем скопировать исходный код и выполнить компиляцию.
WORKDIR /my-go-app
COPY app-src .
RUN GOOS=linux GOARCH=amd64 go build ./cmd/app-service
Затем на втором этапе мы создаем новый контейнер основанный на образе Debian distroless (см. следующий совет).
FROM gcr.io/distroless/static-debian10
Копируем артефакты, созданные на первом шаге, добавив опцию --from=builder
.
COPY --from=builder /my-go-app/app-service /bin/app-service
Итоговый образ будет содержать оптимальный набор библиотек из образа образе Debian distroless и исполняемый файл приложения. При этом образ не содержит инструментов для компиляции, не содержит исходный код.
5. Distroless, from scratch
Использование минимального базового образа - еще одна рекомендация при создании Dockerfile.
Лучшим вариантом будет создание контейнера с нуля (scratch), но этот вариант подходит только для бинарных файлов, которые на 100% статичны.
Distroless - прекрасная альтернатива. Они разработаны, чтобы содержать только минимальный набор библиотек, необходимых для запуска Go, Python или других фреймворков.
Например, вы используете базовый образ ubuntu:xenial
FROM ubuntu:xenial-20210114
При проверке образа сканером sysdig inline scanner были обнаружены более 100 уязвимостей. При этом большинство пакетов, содержащих уязвимости, скорее всего вам никогда не потребуются.
❯ docker run -v /var/run/docker.sock:/var/run/docker.sock --rm quay.io/sysdig/secure-inline-scan:2 image-ubuntu -k $SYSDIG_SECURE_TOKEN --storage-type docker-daemon
Inspecting image from Docker daemon -- distroless-1:latest
Full image: docker.io/library/image-ubuntu
Full tag: localbuild/distroless-1:latest
…
Analyzing image…
Analysis complete!
...
Evaluation results
- warn dockerfile:instruction Dockerfile directive 'HEALTHCHECK' not found, matching condition 'not_exists' check
- warn dockerfile:instruction Dockerfile directive 'USER' not found, matching condition 'not_exists' check
- warn files:suid_or_guid_set SUID or SGID found set on file /bin/mount. Mode: 0o104755
- warn files:suid_or_guid_set SUID or SGID found set on file /bin/su. Mode: 0o104755
- warn files:suid_or_guid_set SUID or SGID found set on file /bin/umount. Mode: 0o104755
- warn files:suid_or_guid_set SUID or SGID found set on file /sbin/pam_extrausers_chkpwd. Mode: 0o102755
- warn files:suid_or_guid_set SUID or SGID found set on file /sbin/unix_chkpwd. Mode: 0o102755
- warn files:suid_or_guid_set SUID or SGID found set on file /usr/bin/chage. Mode: 0o102755
…
Vulnerabilities report
Vulnerability Severity Package Type Fix version URL
- CVE-2019-18276 Low bash-4.3-14ubuntu1.4 dpkg None http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-18276
- CVE-2016-2781 Low coreutils-8.25-2ubuntu3~16.04 dpkg None http://people.ubuntu.com/~ubuntu-security/cve/CVE-2016-2781
- CVE-2017-8283 Negligible dpkg-1.18.4ubuntu1.6 dpkg None http://people.ubuntu.com/~ubuntu-security/cve/CVE-2017-8283
- CVE-2020-13844 Medium gcc-5-base-5.4.0-6ubuntu1~16.04.12 dpkg None http://people.ubuntu.com/~ubuntu-security/cve/CVE-2020-13844
...
- CVE-2018-20839 Medium systemd-sysv-229-4ubuntu21.29 dpkg None http://people.ubuntu.com/~ubuntu-security/cve/CVE-2018-20839
- CVE-2016-5011 Low util-linux-2.27.1-6ubuntu3.10 dpkg None http://people.ubuntu.com/~ubuntu-security/cve/CVE-2016-5011
Нужен ли вам компилятор gcc или совместимость с systemd в вашем контейнере? Скорее всего нет. То же самое касается dpkg или bash.
Если ваш базовый образ gcr.io/distroless/base-debian10:
FROM gcr.io/distroless/base-debian10
Тогда он содержит только базовый набор пакетов, включая библиотеки libc, libssl и openssl.
Для компилируемых приложений, таких как Go, которым не нужна libc, можно использовать более компактный образ:
FROM gcr.io/distroless/static-debian10
6. Используйте проверенные базовые образы
Внимательно выбирайте базовые образы.
Ваши контейнеры основанные на непроверенных и неподдерживаемых образах унаследуют все проблемы и уязвимости из этих базовых образов.
Следуйте приведенным рекомендациям при выборе базового образа:
-
Официальные и проверенные образы из доверенных репозиториев всегда предпочтительнее образов неизвестного происхождения.
-
Когда вы используете неофициальный образ проверяйте источник и Dockerfile. Создавайте свои собственные базовые образы. Нет гарантий, что образ из публичного репозитория действительно создан из указанного Dockerfile. Так же нет уверенности, что он обновляется.
-
Но иногда даже официальные образы могут не подойти из соображений безопасности или большого размера. В качестве примера сравните официальный образ node и bitnami/node. Последний предлагает различные версии поверх дистрибутива minideb. Образы часто обновляются с учетом последних исправлений ошибок, подписываются с помощью Docker Content Trust и проходят сканирование безопасности для отслеживания известных уязвимостей.
7. Своевременно обновляйте образы
Используйте базовые образы, которые регулярно обновляются, так же обновляйте ваши образы, основанные на них.
Процесс обнаружение уязвимостей непрерывен, поэтому правильным подходом будет регулярное обновление с учетом последних исправлений безопасности.
При этом нет необходимости стараться всегда использовать последнюю версию, которая может содержать критические уязвимости, но следует придерживаться стратегии версионирования:
-
Придерживайтесь стабильных или long-term версий поддержки, которые быстро и часто предоставляют исправления безопасности.
-
Будьте готовы отказаться от старых версий и выполнить миграцию до того, как закончится срок поддержки текущей версии вашего базового образа, и она перестанет получать обновления.
-
Кроме того, периодически пересобирайте свои собственные образы используя аналогичную стратегию, чтобы получить последние пакеты из базового дистрибутива, Node, Golang, Python и т. Д. Большинство менеджеров пакетов или зависимостей, таких как npm или go mod, предлагают способы указать диапазоны версий для следите за последними обновлениями безопасности.
8. Открытые порты
Каждый открытый порт в вашем контейнере - это открытая дверь в вашу систему. Оставляйте открытыми только порты, которые действительно нужны вашим приложениям и избегайте таких портов как SSH (22).
Пожалуйста, обратите внимание, что хотя в Dockerfile присутствует команда EXPOSE, эта команда носит скорее информативный характер (не считая docker -P
). Открытие портов не позволяет автоматически подключаться к ним при запуске контейнера (если вы не выполняете команду docker run --publish-all).
Вам необходимо указать публикуемые порты при запуске контейнера.
Используйте команду EXPOSE в Dockerfile только чтобы обозначить и задокументировать необходимые порты, затем используйте указанные порты в процессе запуска контейнеров.
Предотвращение утечки конфиденциальных данных
Будьте осторожны с конфиденциальными данными, при работе с контейнерами.
Приведенные ниже рекомендации помогут избежать случайной утечки данных при работе с контейнерами.
9. Учетные данные и конфиденциальность
Никогда не помещайте чувствительные данные или учетные данные в Dockerfile (через переменные окружения, аргументы или жестко заданными в команде).
Будьте очень осторожны при копировании файлов внутрь контейнера. Даже если файл удален в последующих командах Dockerfile, к нему все еще можно получить доступ на предыдущих слоях, поскольку на самом деле он не удаляется, а только «скрывается» в окончательной файловой системе. При создании образа следуйте этим рекомендациям:
-
Если приложение поддерживает конфигурацию с помощью переменных окружения, используйте их для установки секретов при выполнении (параметр -e в docker run) или используйте Docker secrets, Kubernetes secrets для предоставления значений в качестве переменных среды.
-
Используйте конфигурационные файлы и монтируйте их в docker или Kubernetes secret
Кроме того, ваши образы не должны содержать конфиденциальную информацию или параметры конфигурации, которые относятся к определенному окружению (например, dev, qa, prod и т. д.).
10. ADD, COPY
Инструкции ADD и COPY предоставляют аналогичные функции в файле Dockerfile. Однако использование COPY является предпочтительным.
Используйте COPY всегда, если вам действительно не нужны возможности ADD, например, для добавления файлов из URL-адреса или из tar-файла. Процесс копирования данных будет более предсказуемым и менее подтверженым ошибкам.
В некоторых случаях предпочтительнее использовать инструкцию RUN вместо ADD, чтобы загрузить пакет с помощью curl или wget, извлечь его, а затем удалить исходный файл за один шаг, уменьшив количество слоев.
Многоступенчатые сборки также решают эту проблему и помогают следовать лучшим практикам, позволяя копировать файлы из архива, распакованного на предыдущем этапе.
11. Контекст сборки и dockerignore
Вот типичное выполнение сборки с использованием Docker с Dockerfile по умолчанию и контекстом в текущей папке:
docker build -t myimage .
Остерегайтесь!
Значок "." параметр - это контекст сборки. Используя его, вы можете скопировать в контейнер конфиденциальные или ненужные файлы, такие как файлы конфигурации, учетные данные, резервные копии, файлы блокировки, временные файлы, источники, подпапки, точечные файлы и т. д.
Представьте, что у вас есть следующая команда внутри Dockerfile:
COPY . /my-app
Это скопирует все внутри контекста сборки, что для «.». Например, включает сам Dockerfile.
В соответствии с Dockerfile best practice нужно создать подпапку, содержащую файлы, которые необходимо скопировать внутри контейнера, использовать ее в качестве контекста сборки и, когда это возможно, явно указывать инструкции COPY (избегайте подстановочных знаков). Например:
docker build -t myimage files/
Кроме того, создайте файл .dockerignore, чтобы явно исключить файлы и каталоги.
Даже если вы будете особенно осторожны с инструкциями COPY, весь контекст сборки отправляется демону докера перед запуском сборки образа. Это означает, что наличие меньшего и ограниченного контекста сборки сделает ваши сборки быстрее.
Прочие рекомендации
12. Порядок слоев
Помните, что порядок в инструкциях Dockerfile очень важен.
Поскольку RUN, COPY, ADD и другие инструкции создают новый слой контейнера, группировка нескольких команд вместе уменьшит количество слоев.
Например, вместо:
FROM ubuntu
RUN apt-get install -y wget
RUN wget https://…/downloadedfile.tar
RUN tar xvzf downloadedfile.tar
RUN rm downloadedfile.tar
RUN apt-get remove wget
Можно использовать единственную команду RUN:
FROM ubuntu
RUN apt-get install wget && wget https://…/downloadedfile.tar && tar xvzf downloadedfile.tar && rm downloadedfile.tar && apt-get remove wget
Кроме того, сначала разместите команды, которые с меньшей вероятностью будут изменены и которые легче кэшировать.
Вместо:
FROM ubuntu
COPY source/* .
RUN apt-get install nodejs
ENTRYPOINT ["/usr/bin/node", "/main.js"]
Лучше бы сделать:
FROM ubuntu
RUN apt-get install nodejs
COPY source/* .
ENTRYPOINT ["/usr/bin/node", "/main.js"]
Пакет nodejs с меньшей вероятностью изменится, чем исходный код нашего приложения.
Помните, что выполнение команды rm удаляет файл на следующем уровне, но он все еще доступен и к нему можно получить доступ, поскольку итоговый образ будет содержать все предыдущие слои.
Поэтому не стоит копировать конфиденциальные данные, даже если вы их удалите позже, они не будут видны в файловой системе контейнера, но по-прежнему будут легко доступны.
13. Metadata labels
Рекомендуется включать Dockerfile метки метаданных при создании образа.
Они помогут в управлении образами, например, включая версию приложения, ссылку на веб-сайт, как связаться с командой поддержки и многое другое.
14. Тестируйте ваши Dockerfile
Такие инструменты, как Haskell Dockerfile Linter (hadolint), могут обнаруживать ошибки в вашем Dockerfile и даже обнаружить проблемы внутри таких команд как RUN.
Рассмотрите возможность включения такого инструмента в ваши конвейеры CI.
Сканеры образов также способны обнаруживать уязвимости:
Некоторые из ошибок конфигурации, которые вы можете обнаружить - это образы, работающие с правами root, открытые порты, использование инструкции ADD, жестко запрограммированные секреты или нежелательные команды RUN.
15. Локальное сканирование образов
Локальное сканирование образов - еще один способ обнаружения потенциальных проблем перед запуском контейнеров. Чтобы следовать рекомендациям по сканированию образов, вы должны выполнять его на разных этапах жизненного цикла образа, в дополнение к тому, когда образ уже помещен в репозиторий.
Лучшей практикой безопасности является применение парадигмы «сдвиг влево» путем непосредственного сканирования ваших образов сразу после их создания в конвейерах CI перед отправкой в реестр.
Периодически проверяйте наличие новых уязвимостей.
За пределами сборки образов
До сих пор мы сосредоточились на процессе создания образа и обсудили советы по созданию оптимальных файлов Docker. Но давайте не будем забывать о некоторых дополнительных предварительных проверках и о том, что происходит после создания образа: его запуск.
16. Docker port socket and TCP protection
Докер-сокет - это большая привилегированная дверь в вашу хост-систему, которая, как недавно было замечено, может использоваться для вторжений и использования вредоносного программного обеспечения. Убедитесь, что ваш /var/run/docker.sock имеет правильные разрешения, и если докер доступен через TCP (что вообще не рекомендуется), убедитесь, что он должным образом защищен.
17. Цифровая подпись образов
Использование Docker Content Trust, Docker notary, Harbour notary или аналогичные инструменты для цифровой подписи ваших образов и последующей проверки их во время выполнения - одна из лучших практик Dockerfile.
Включение проверки подписи отличается в каждой среде выполнения. Например, в докере это делается с помощью переменной окружения DOCKER_CONTENT_TRUST: экспорт DOCKER_CONTENT_TRUST = 1
18. Изменение тэгов
Теги являются непостоянной ссылкой на конкретную версию образа в определенный момент времени и могут измениться неожиданно и в любой момент.
19. Запуск как non-root
Ранее мы говорили об использовании пользователя без полномочий root при создании контейнера. Инструкция USER установит пользователя по умолчанию для контейнера, но за оркестратором или средой выполнения (например, docker run, kubernetes и т. д.) Остается последнее слово в том, кто является пользователем запущенного контейнера.
Избегайте запуска вашей среды от имени пользователя root.
Openshift и некоторые кластеры Kubernetes по умолчанию будут применять ограничительные политики, предотвращая запуск root контейнеров. Избегайте соблазна работать с правами root, чтобы обойти проблемы с разрешениями или владением, и вместо этого устраните реальную проблему.
20. Включить проверки
При использовании Docker или Docker Swarm по возможности включайте инструкцию HEALTHCHECK в свой Dockerfile. Это критически важно для длительно работающих или постоянных служб, чтобы гарантировать их работоспособность и управлять перезапуском службы в противном случае.
Если вы запускаете образы в Kubernetes, используйте конфигурацию livenessProbe внутри определений контейнеров, поскольку инструкция Docker HEALTHCHECK не будет применяться.
Автор: Александр