20 лучших практик для Dockerfile

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

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

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

Большую часть этих накладных расходов можно решить, «сместив» решение вопросов защиты как можно ближе к более ранним этапам процесса разработки..

Хорошо написанный Dockerfile позволит избежать необходимости в запуске привилегированных контейнеров, публикации наружу ненужных портов, установки ненужных пакетов, утечек данных и тд и тп — короче всего того что может быть использовано для атаки. Заблаговременное избавление от известных рисков помогает снизить операционные издержки и затраты на обеспечение безопасности.

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

Однако помните, что использование набора “хороших практик” — всего лишь часть из вашего процесса разработки. Уязвимости никуда не денутся.

Раздел 1 — избегайте ненужных привилегий

Эти советы следуют принципу  “наименьших привилегий” так чтобы ваше приложение или сервис имели доступ только к тем ресурсам, которые ему реально необходимы.

К сожалению про этот принцип очень часто забывают на этапе разработки, пренебрегая им для ускорения. Аля — ну давайте сейчас дадим полные права, чтобы не разбираться чего нам не хватает а потом аккуратно составим полный список. Нет ничего более постоянного чем временное — “потом” не наступает никогда, либо до первой серьезной проблемой с безопасностью. Когда система начинает выезжать в прод выясняется что список не сделан, разработчику этим заниматься некогда, у него на рабочем месте/стенде все работает, поэтому “дай там полные права и не парься”. А потом к этому привыкают и считают нормой.

Пункт 1.1. Контейнеры без root

Вот этот отчет показывает что больше половины всех образов (58%), запущенных в виде контейнеров, используют внутри себя учетную запись root (UID 0) для запуска процессов. Между прочим, хорошие практики Docker предупреждают нас не делать этого! (там прям явно написано, англицким по белому “If a service can run without privileges, use USER to change to a non-root user.”)

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

Более того, ваша среда запуска контейнеров може по умолчанию блокировать запуск таких контейнеров, работающих от root (например Openshift потребует от вас дополнительных настроек SecurityContextConstraints) (и тут я считаю что это правильно и привет отличиям энтерпрайз-реди опеншифта от ванильного кубера где свобода и анархия. Это как Openldap и Active Directory — первый позволяет смотреть список объектов без авторизации, а второй шлет лесом на любую операцию без авторизации).

Запуск не рутовых контейнеров может потребовать от вас набора дополнительных шагов в при создании Dockerfile. Как минимум:

  • убедитесь что у вас определен выделенный пользователь через директиву USER внутри вашего контейнера
  • предоставить необходимые права доступа на файловой системе для того чтобы этот пользователь мог читать и писать то что ему нужно (например если у вас контейнер с nginx и статикой — убедитесь что nginx запущенный не из под root может читать те файлы, которые ему нужно будет отдавать пользователям. Но в целом все как и вне контейнеров)

Вам могут встретиться контейнеры, которые запускаются из под root а затем используют gosu(https://github.com/tianon/gosu) или su-exec(https://github.com/ncopa/su-exec) чтобы переключиться на отдельного пользователя.

Если же вам ну кровь из носу необходимо выполнять специфические команды от root внутри контейнера, воспользуйтесь sudo(https://wiki.archlinux.org/index.php/Sudo_(%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9)). Я даже более того добавлю — sudo позволяет вам внутри sudoers (https://www.opennet.ru/man.shtml?topic=sudoers&category=5&russian=0) файла задавать оч строгие ограничения вида — какой пользователь, какой командой к каким файлам может обращаться или какие аргументы использовать. тем самым вплоне возможно “обрезать” привилегии.

Эти альтернативы так или иначе лучше чем запуск всего контейнера с привилегиями root по умолчанию. Плюс помним что такая конфигурация (root) может не работать в некоторых окружениях, например Openshift.

Кстати, небольшая ремарка от меня, не от автора. Может конечно это я странный, но когда мне говорят “не делай так, это плохо”, у меня всегда (ну или почти всегда) возникает вопрос — а почему это плохо? Кто это сказал? А почему он так думает? Так и в этом вопросе. В принципе, с одной стороны, любой человек, знакомый с администрированием Linux понимает, что запуск сетевой службы из под root это плохо. Ну у вас блин в сеть торчит нечто, что будучи взломанным даст врагу возможность хозяйничать в системе! Но контейнеры это как мы помним “другое”. А вот не совсем))) В данном случае смысл сохраняется. Можете почитать вот эти две статьи от bitnami и redhat. Но основной смысл кмк раскрыт в этой фразе “Because anyone who accesses your container running as root can start undesirable processes in it, such as injecting malicious code. And running a process in your container as root makes it possible to change the user id (UID) or group id (GID) when starting the container, which makes your application vulnerable.

Однако, продолжу свою мысль — я согласен с тем, что это справедливо для сетевых служб. Если у вас контейнер с некоторым запакованным внутрь инструментом для CI/CD (например Terrafrom, Ansible, etc)  — то есть он не открывает и не слушает порт, такой явной проблемы для вас нет и использование root я думаю позволительно, особенно для процессов сборки, где вам может потребоваться делать много разных действий и всех их не перечислишь в sudoers (ну или перечислишь но поддержка sudoers файла станет дополнительной операционной нагрузкой.

Пункт 1.2. Не привязывайтесь к специфичному UID

Запуская контейнер не из под root, убедитесь что вы не привязались к определенному UID ( не делайте наличие этого UID обязательным). В чем тут дело?

  • Openshift по умолчанию использует случайный UID при запуске контейнера
  • Форсирование использования определенного UID (по умолчанию первый обычный пользователь в системе имеет UID 1000) требует сразу настраивать привилегии для любых точек монтирования, например папки на хосте для хранения персистентных данных. С другой стороны, если вы запускаете контейнер (для докера это опция -u) с определенным на хост системе UID, это может поломать сервис внутри контейнера, когда он попытается считать что то с файловой системы контейнера.

Вот это контейнер точно будет иметь проблемы, если окажется запущенным с UID отличным от пользователя myuser, так как приложение не сможет писать в каталог /myapp-tmp-dir

Не используйте жестко заданные пути, доступные только для учетной записи myuser. Вместо этого например пишите временные данные в общий /tmp (где благодаря sticky bit, каждый пользователь может и писать и читать). И сделайте мир ресурсов вашего контейнера с возможностью читать, используя права 0644 вместо 0640. Конечно же не забывайте убедиться что все работает при смене UID.

В этом примере, наше приложение будет использовать путь, определенный в переменной окружения APP_TMP_DATA. По умолчанию, значение /tmp позволяет приложение писать временные данные в каталог /tmp ве зависимости от установленного UID. 

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

Пункт 1.3. Сделайте root владельцем всех исполняемых файлов и запретите в них запись

Это одно из базовых “хороших практик Dockerfile” — для каждого исполняемого файла в контейнере, владельцем назначить root, даже если исполняется он не под рутом (заметьте — владельцем сделать, а не дать право только root — чувствуйте разницу с этими магическими битами доступа) и не позволяйте кому угодно их перезаписывать.

Это не позволит пользователям, исполняющим эти файлы (запускающим), вносить изменения в бинарники и скрипты, что в свою очередь может быть одним из способов атаки на нашу систему. Следование этой практике даст вам уверенность в неизменности вашего контейнера (Имеется ввиду в неизменности приложения внутри контейнера, внутри образа. А данные там и так и так храниться не должны, если мы только про статику речь не ведем). Неизменяемые контейнеры не обновляют свой код автоматически во время выполнения, и таким образом вы можете предотвратить случайное или злонамеренное изменение запущенного приложения.

Чтобы следовать этой практике, старайтесь избегать вот такого:

Большую часть времени вы можете просто не использовать опцию “—chown app:app” (или команду “RUN chown …”). Еще раз — пользователь приложения должен иметь права на исполнения файлов не но должен быть их владельцем!

Раздел 2 — уменьшайте поверхность атаки

Эта практика посвящена тому, чтобы минимизировать размер вашего Dockerfile

если вы будете стараться избегать привычки включать ненужные пакеты и открывать ненужные порты, тем самым вы уменьшите поверхность для атаки (да, на первый взгляд звучит капитанисто, но черт побери, давайте честно — а у вас не было такого что “а давайте это на всякий случай сюда запихнем?”, “зачем этот порт — а не знаю, давай оставим, он всегда был” и тп)

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

Пункт 2.1 Многоступенчатые сборки 

Используйте возможности многоступенчатые сборок чтобы получить воспроизводимую сборку внутри контейнеров.

При многоступенчатой сборке вы создаете промежуточный контейнер-этап, со всеми необходимыми инструментами для компиляции или другого способа получения финального артефакта (то есть окончательного исполняемого файла или набора оных). Затем вы копируете полученный артефакт в конечный образ, отсекая дополнительные зависимости разработки, временные сборочные файлы и тд.

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

Так безопаснее и заодно мы еще и размер образа уменьшаем.

Как пример, давайте рассмотрим многоступенчатую сборку приложения на GO:

Согласно указанным инструкциям, мы вначале создаем этап сборки, используя контейнер с образом  golang:1.15, который включает в себя весь необходимый инструментарий для GO

Затем мы определяем следующий этап, основываясь на образе Debian

Мы копируем (используя инструкцию copy) полученный в результате сборки исполняемый файл, используя опцию —from=builder flag

Конечный образ буде включать в себя только минимальный набор библиотек из образа distroless/static-debian-10и наше исполняемый файл приложения.

Никаких сборочных инструментов, никакого исходного кода.

Рекомендуется так же ознакомиться с примером для приложения на Nodejs и с эффективной многоступенчатой сборкой Python-Django приложения.

Пункт 2.2 Бездистрибутивыне образы и образы “с нуля”

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

В идеале нам бы конечно вообще создавать образ “с нуля”, однако в таком случае будут работать только те исполняемые файлы, которые на 100% статически скомпилированные.

“Бездистрибутивность”  (сорри, я только так смог перевести Distroless — если у кого то есть идеи получше- присылайте). Этот подход спроектирован так, чтобы содержать только минимальны набор библиотек, необходимых для запуска исполняемых Go, Python и раличных фреймворков.

Например, если в используете для своих контейнеров базовый образ ubuntu:xenial

Вы автоматически засовываете в него более 100 уязвимостей, согласно результатам  Sysdig inline scanner, большинство из которых относится к пакетам, которые просто идут в нагрузку и вы никогда не будете их использовать:

Вам действительно необходим компилятор gcc или совместимость systemd с SysV? Я думаю что скорее всего нет. То же самое для dpkg или bash

Если же вы создаете свой контейнер на базе “бездистриутивного” образа gcr.io/distroless/base-debian10:

Тогда он будет включать набор только самых базовых пакетов, включая такие библиотеки как glipc, libssl и openssl.

Для статически компилируемых приложений, например GO, вам обычно не нужен libc поэтому вы можете использовать еще более упрощенный вариант:

Пункт 2.3 Используйте только проверенные базовые образы

Осторожно выбирайте базовый образ для ваших контейнеров (имеется ввиду инструкция FROM).

Создавая ваши образы поверх непроверенных или неподдерживаемых сторонних образов, вы наследуете все их проблемы и уязвимости.

Следуйте вот этим рекомендациям для Dockerfile по выбору базового образа:

  1. Вам следует отдавать предпочтение тольк опроверенным и официальным образам из доверенных источников, в отличии от образовы, созданных непонятно кем.
  2. Когда вы используете “кастомный” образ, проверьте его исходники и Dockerfile  а так же потрудитесь на их основе собрать свой собственный базовый образ. Ведь нет гарантии что образ, опубликованный в публичном реестре реально создан на основании этого Dockerfile. Равно Как и нет уверенности в том что он регулярно обновляется.
  3. Временами, официальный образ может быть не лучшим выбором с точки зрения безопасности и минимализма образа. Например, сравнивая официуальный образ nodejs с образом bitnami/node, последний предоставляет кастомизируемые версии, созданные на основе minideb. Они чаще предоставляют исправление последних ошибок, отмечены “Docker Content Trust” и проходят сканирование безопасности для отслеживания известных уязвимостей.

Пункт 2.4 Почаще обновляйте свои образы

Используйте базовые образы которые часто обновляются и перестаривайте свои на основе последних версий.

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

Не стоит конечно бездумно прыгать на последнюю версию, тк как она может содержать какие то очень резкие изменения, ломающие совместимость (сорри, ну не могу я кратко и емко перевести “ breaking changes”), но старайтесь следовать следующей стратегии:

  • Придерживайтесь стабильных или версий с длительным периодом поддержки, которые быстро и часто предоставляют исправления безопасности.
  • Планируйте заранее. Будьте готовы отказаться от старых версий и мигрировать до того, как ваша базовая версия образа достигнет конца своего срока службы и перестанет получать обновления.
  • Кроме того, периодически перестраивайте свои собственные образы и используйте аналогичную стратегию, чтобы получить последние пакеты из базового дистрибутива, Node, Golang, Python и т. Д. Большинство менеджеров пакетов или зависимостей, таких как npm или go mod, предлагают способы указания диапазонов версий, чтобы не отставать от последних обновлений безопасности.

Пункт 2.5 Почаще обновляйте свои образы

Каждый открытый порт в вашем контейнере-это открытая дверь в вашей системе. Публикуйте наружу только те порты, которые нужны вашему приложению, и избегайте таких портов, как SSH (22).

Обратите внимание, что хотя Dockerfile предлагает команду EXPOSE, эта инструкция несет скорее информационную смысловую нагрузку и предназначена для документирования. Предоставление доступа к порту не позволяет автоматически подключаться ко всем ПУБЛИКУЕМЫМ портам при запуске контейнера (если только вы не используете docker run —publish-all). Вы должны указать публикуемые для работы порты, на этапе запуска контейнера.

Используйте EXPOSE для документирования только необходимых портов в файле Dockerfile, а затем придерживайтесь этих данных при запуске и эксплуатации.

Раздел 3 — Предотвратите утечку конфиденциальных данных

Будьте по настоящему осторожны с вашими конфиденциальными данными когда речь идет о контейнерах

Пункт 3.1 Креды и конфиденциальность

Никогда не помещайте секреты или креды (короче не хочу я переводить эти термины тк они довольно комплексные и уже намертво вошли в жаргон — ключи, токены, пароли, сертификаты и тп) в инструкции Dockerfile.

Будьте очень осторожны с файлами которые были скопированы в контейнер. Даже если в дальнейшем файл будет удален последующей инструкцией, он все равно останется доступен через предыдущий уровень (образа) а не удален на самом деле, лишь “скрыт” в финальном состоянии файловой системы. Так что руководствуйтесь следующими принципами при сборке контейнеров:

  • Если приложение поддерживает конфигурацию через переменные окружения, используйте их для настройки секретов во время запуска контейнера (опция -e в команде docker run), или использовать Docker secrets, Kubernetes secrets чтобы заполнять значения этих переменных окружения.
  • Используйте конфигурационные файлы и точки монтирования (bind mount points) чтобы пробросить их в контейнер Docker или монтируйте их через Kubernetes secrets.

Ну и естественно ваши образы не должны содержать конфиденциальную информацию или конфигурационные значения так как это намертво прибивает их к конкретному окружению (например продакшен, стейджинг и тп).

Вместо этого, позвольте образам быть изменяемыми, за счет возможности задания значений при запуске (в оригинале — в рантайме), особенно секреты. Вам следует включать в состав образа только те конфигурационные файлы, которые содержат не секретные, крайне редко изменяемые либо шаблонные значения (значения по умолчанию)).

Пункт 3.2 ADD, COPY

Обе инструкции, и ADD, и COPY предоставляют схожий функционал при создании Dockerfile. Хотя команда COPY является более очевидной по смыслу.

Используйте COPY, если вам действительно не нужна функция ADD, например для добавления файлов из URL-адреса или из файла tar. COPY более предсказуема и менее подвержена ошибкам. (Условно — результат работы copy более предсказуем — например вы хотите скопировать архив внутрь образа, однако если copy просто скопирует как есть, то ADD, если она распознает тип архива, она может его распаковать и вы обнаружите в итоге не то, что ожидали. Получается что ADD добавляет некоторую неочевидность, некоторый уровень “магии”, что не есть хорошо)

В некоторых случаях предпочтительнее использовать инструкцию RUN вместо ADD для загрузки пакета с помощью curl или wget, распаковать его, а затем удалить исходный файл за один шаг, сократив количество слоев (чем проделывать это через ADD, опять же для большей предсказуемости).

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

Пункт 3.3 Контекст сборки и dockerignore

Вот типовая инструкция по сборке образа с docker, использующая по умолчанию Dockerfile и контекст текущего каталога:

Будьте осторожны!

Параметр “.” это сборочный контекст. Использование “.” в качестве контекста опасно так как вы можете скопировать конфиденциальные или не нужные файлы в контейнер, например файлы конфигурации, креды, бекапы, файлы блокировок, временные файлы, исходники, подпапки, скрытые файлы и тп.

Представьте что вы выполняете следующую инструкцию в вашем Dockerfile

Она скопирует абсолютно все что входит в контекст сборки за счет “.” включая и сам Dockerfile.

С точки зрения лучших практик Dockefile вам следует создать подкаталог, содержащий файлы, которые должны быть скопированы внутрь контейнера и использовать его в качестве контекста сборки, и если это возможно, указывать явные значения в инструкциях COPY (избегая масок), например:

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

Даже если вы были очень осторожны при задании инструкций COPY, весь контекст сборки все равно отправляется демону docker перед началом сборки образа. Это означает, что наличие меньшего и ограниченного контекста сборки сделает ваши сборки быстрее.

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

Раздел 4 — Прочее

Пункт 4.1 Здравомыслие и слои

Помните, что порядок инструкций в Dockerfile очень важен. Каждая из RUN, COPY, ADD и других инструкций создает новый слой, в то же время как объединение команд в группы позволит уменьшить их число.

Например вместо того чтобы писать:

Лучше будет сделать:

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

Лучше будет:

Пакет nodejs скорее всего будет меняться и обновляться намного реже чем исходный код вашего приложения.

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

Поэтому Ни в коем случае не копируйте конфиденциальные данные в контейнер, в надежде после удалить их — они останутся не видимы в финальном образе но все еще легко доступны.

Пункт 4.2 Метки метаданных

Хорошей практикой построения Dockerfile является включение различных меток в метаданные во время создания образа.

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

Вы можете взглянуть на список предопределенных аннотаций из спецификации образов OCI, который замещает собой устаревший черновик стандарта схемы меток.

—-

для справки — OCI это Open Container Initiative — проект от Linux Foundation, цель которого — ввести стандартизацию в изначально хаотичный мир контейнеров Linux. Если кому интересно — можете посетить их сайт: https://opencontainers.org/

Пункт 4.3 Линтинг

Небольшая справка:

Lint — первоначально — статический анализатор для языка программирования Си, который сообщал о подозрительных или непереносимых на другие платформы выражениях. В начале XXI века термин стал нарицательным для всех программ такого типа. Как инструмент программа лишь анализирует статический исходный код, не скомпилированный в отличие от отладчиков.

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

—-

Инструменты на подобие Haskell Dockerfile Linter (hadolint) могут обнаруживать использование плохих практик в ваших Dockerfile и даже выявлять проблемы внутри shell команд, выполняемых при помощи RUN.

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

Например вы можете обнаружить такие примеры неправильных конфигураций, как —  образы, работающие от имени root, открытые порты, использование инструкции ADD, жестко заданные секреты или не очень удачные варианты команды RUN.

—-

Добвлю от себя- линтинг вообще полезная штука. Сейчас почти для всего естьлинтеры- сиди выбирай. Есть линтер для Ansible, для Terraform, линтер Python у меня вообще в IDE встроен, а линтер для bash я ставил отдельным плагином.

Пункт 4.4 Локальное сканирование образов во время разработки

Сканирование образов это еще один путь поиска потенциальных проблем прежде, чем вы запустили контейнеры (и встретились с проблемой лицом к лицу). Чтобы следовать рекомендациям по сканированию образов, вам следует выполнять его на разных этапах жизненного цикла образа (этапах разработки и эксплуатации), в дополнение к тому, когда образ уже помещен в реестр.

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

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

Раздел 5 — За пределами сборки образов

До сих пор мы фокусировались на процессе создания образа и обсуждали советы по созданию оптимальных Dockerfiles. Но давайте не будем забывать о некоторых дополнительных предварительных проверках и о том, что происходит после создания вашего образа: его запуск.

Пункт 5.1 Сокет порта Docker и защита со стороны TCP соединения

Сетевой сокет Docker это большая привилегированная дверь в систему вашего хоста, причем как мы можем увидеть она может использоваться для загрузки вредоносного софта и выполнения команд. Удостоверьтесь, что ваш /var/run/docker.sock обладает правильными разрешениями (речь про права доступа на уровне файловой системы) и если вы публикуете демон Docker в сеть через TCP (что абсолютно не рекомендуется), убедитесь что он защищен.

Пункт 5.2 Подпись образа и проверка сигнатур

Одна из лучших практик Dockerfile это использовать docker content trust, Docker notary, Harbor notary или аналогичные инструменты для цифровой подписи ваших образов и последующей проверки их во время выполнения.

Процесс включения проверки подписи отличается для каждой среды выполнения. Нарпимер в docker это можно сделать через переменную окружения DOCKER_CONTENT_TRUST:

Пункт 5.3 Изменение тегов

В мире контейнеров теги-это изменяемая ссылка на конкретную версию образа в определенный момент времени.

Теги могут меняться неожиданно, в любой момент времени. рекомендуется прочитать статью “атака изменяемых тегов” чтобы узнать об этом по больше (в оригинале было — атака тегов мутантов но я решил перевести поскромнее чтобы не накручивать лишних смыслов).

Пункт 5.4 Запуск не от root

Ранее мы говорили об использовании не root пользователя при создании контейнера. Инструкция USER установит для запущенного контейнера (точнее процесса в нем) пользователя по умолчанию, но оркестратор или среда выполнения (например, docker run, kubernetes и т. Д.) имеет последнее слово о том, кто является результирующим пользователем запущенного контейнера.

На самом деле, серьезно, избегайте запуска вашей среды от имени root.

Openshift и некоторые кластеры Kubernetes будут применять ограничительные политики по умолчанию, предотвращая запуск таких контейнеров. Избегайте соблазна запуска от имени root, чтобы обойти проблемы с разрешениями или правами доступа, и вместо этого устраните реальную проблему.

Пункт 5.5 Включайте проверки здоровья/жизнеспособности ваших контейнеров

Когда вы используете просто Docker или Docker Swarm включитай инструкцию HEALTHCHECK в свой Dockerfile, когда это возможно. Это очень важно для длительно или постоянно работающих служб, чтобы обеспечить их работоспособность и иметь возможность перезапускать их в случае чего (имеется ввиду знать что надо перезапускать и перезапускать).

Если вы запускаете свои образы в Kubernetes, используйте конфигурацию livenessProbe внутри определений контейнеров, так как инструкция docker HEALTHCHECK не применима в данном случае.

Пункт 5.6 Отсечение возможностей

Кроме того, при выполнении вы можете ограничить возможности приложения минимально необходимым набором, используя флаг —cap-drop  в Docker или securityContext.capabilities.drop в Kubernetes. Таким образом, в случае, если ваш контейнер скомпрометирован, диапазон действий, доступных злоумышленнику, ограничен.

Кроме того, посмотрите вот эти статьи чтобы узнать, как применить AppArmor и Seccomp в качестве дополнительных механизмов ограничения привилегий контейнера:

Заключение

Заключение уже от меня ибо у авторов статьи там пошла сплошная реклама и маркетинг. Так вот — надеюсь что вам нравится контент в таком формате — переводы с коментариями. Авторский тоже будет но как видите, получается не часто.