Образ контейнера состоит из двух частей: корневой файловой системы и конфигурации.
при запуске контейнера на основе его образа создается экземпляр контейнера, а образ включает корневую файловую систему. Если выполнить команду docker run -it alpine sh и сравнить с тем, что находится внутри созданного вами вручную контейнера, то общая схема каталогов и файлов окажется той же, а если версия Alpine такая же, то они совпадут в точности.
Часть команд Dockerfile (например, FROM, ADD, COPY и RUN) изменяет содержимое корневой файловой системы, включенной в образ. Другие команды,например USER, PORT и ENV, влияют на хранимую в образе параллельно с корневой файловой системой информацию о настройках. Просмотреть ее можно, выполнив для нужного образа команды docker inspect. Эти настройки задают Docker значения параметров времени выполнения, используемые по умолчанию при запуске образа. Например, переменные среды задаются для выполняемого процесса контейнера с помощью команды ENV в Dockerfile.
### Переопределение настроек во время выполнения
В Docker можно переопределять конфигурацию во время выполнения, воспользовавшись параметрами командной строки. Например, изменить переменную среды или задать новую можно с помощью команды docker run -e <имя_переменной>=<новое_значение> ....
### Сборка без использования демона
При выполнении команды docker сама по себе вызываемая утилита командной строки (docker) делает очень немного. Она просто преобразует команду в запрос API, отправляемый далее демону Docker с помощью сокета Docker. Любая программа, имеющая доступ к сокету Docker, может отправлять демону запросы API.
Демон Docker представляет собой выполняемый на протяжении длительного времени процесс, который фактически и производит в действительности всю работу по запуску и управлению контейнерами и их образами. Как вы видели в главе 4, чтобы создать контейнер, демону необходима возможность создавать пространства имен, поэтому он должен выполняться от имени суперпользователя.
Представьте, что вы хотели бы выделить машину (или виртуальную машину) для сборки образов контейнеров и сохранения их в реестре. В случае использования подхода Docker на вашей машине должен быть запущен демон, возможности которого выходят далеко за пределы создания образов и взаимодействия с реестром. При отсутствии дополнительных средств безопасности любой пользователь, который может выполнить на этой машине команду docker build, может выполнить и docker run для запуска на этой машине любых команд, которые только пожелает.
Причем дело не только в возможности выполнять любые команды, но и в том, что если он воспользуется этими полномочиями для злонамеренных действий, то выследить виновного будет нелегко. Можно поддерживать журнал аудита определенных выполняемых пользователями действий, но — как ясно демонстрирует Дэн Уолш (Dan Walsh) (https://oreil.ly/TszBl) — в данном журнале будет фиксироваться идентификатор процесса демона, а не пользователя.
Во избежание этих рисков для безопасности лучше воспользоваться одной из альтернативных утилит сборки образов контейнеров, не полагающихся на демон Docker.
Например - podman (https://podman.io/) или Утилита Bazel (https://oreil.ly/Jz_30) от компании Google
### Слои образов
Вне зависимости от используемой утилиты абсолютное большинство сборок образов контейнеров описывается в Dockerfile. Он позволяет описать ряд инструкций, в результате каждой из которых либо получается слой файловой системы, либо меняется конфигурация образа.
Все слои хранятся отдельно, поэтому нужно быть осторожными, чтобы не сохранить случайно конфиденциальные данные, даже если последующий слой их удаляет.
Не включайте в слои ничего, что не готовы продемонстрировать любому, у кого есть доступ к образу.
### Хранение образов
Образы сохраняются в реестрах контейнеров. Если вы используете Docker, то, вероятно, вам случалось использовать реестр Docker Hub (https://hub.docker.com/).
Все слои хранятся по отдельности, в виде больших двоичных объектов в реестре, идентифицируемых по хешу их содержимого. В целях экономии места желательно хранить только одну копию каждого объекта, на которую могут ссылаться различные образы. В реестре также хранится манифест образа, в котором указывается набор больших двоичных объектов слоев, составляющих образ. Хеш манифеста образа служит уникальным идентификатором всего образа и называется дайджестом (digest) образа. В случае повторной сборки образа и изменения любых его характеристик этот хеш также поменяется.
### Идентификация образов
Первая часть ссылки на образ — URL реестра, в котором он хранится. (Если адрес реестра опущен, значит, образ хранится либо на локальной машине, либо в Docker Hub, в зависимости от контекста команды.)
Следующая часть ссылки на образ представляет собой название учетной записи пользователя или организации — владельца этого образа. За ним следует имя образа, а далее — либо дайджест-идентификатор его содержимого, либо удобный для восприятия человека тег.
Вместе получаются адреса примерно следующего вида:
```
<URL реестра>/<Имя пользователя или название организации>/<репозиторий>
@sha256:<дайджест>
<URL реестра>/<Имя пользователя или название организации>/<репозиторий>:<тег>
```
Если URL реестра пропущен, то по умолчанию используется адрес Docker Hub, docker.io.
Для человека неудобно ссылаться на образ по хешу, поэтому часто используются теги — произвольные метки, присваиваемые образам. Одному образу можно присвоить произвольное число тегов, а любой тег можно перенести с одного образа на другой. С помощью тегов часто указывают версии содержащегося в образе ПО — как в приведенном выше примере с версией 0.2.1.
Теги можно переносить с образа на образ, поэтому обращение к образу по тегу не гарантирует одинаковый результат сегодня и завтра. А вот при ссылке по хешу вы всегда получаете один и тот же образ, поскольку хеш определяется содержимым образа. Любое изменение образа приводит к изменению хеша.
### Безопасность образов
Основной вопрос безопасности образов — их целостность, то есть гарантия того, что используются именно те образы, которые должны применяться. Злоумышленник, которому удалось запустить какой-то нежелательный образ в развернутой системе, сможет выполнять любой нужный ему код. Технологический процесс содержит множество потенциально слабых звеньев, от сборки и сохранения образа до его запуск
Разработчики приложений могут влиять на безопасность через создаваемый ими код. Статические и динамические средства анализа, проверка коллегами и тестирование помогают находить уязвимости, появившиеся во время разработки. И к контейнеризованным приложениям это относится в той же степени, что и к обычным. Но поскольку данная книга посвящена контейнерам, то посмотрим на уязвимости, которые могут появиться на этапе сборки образа контейнера.

Векторы атак на образы
### Безопасность этапа сборки
На этапе сборки Dockerfile превращается в образ контейнера. Количество возможных рисков для безопасности на данном этапе весьма велико.
### Происхождение Dockerfile
Инструкции для сборки образа контейнера берутся из Dockerfile. Каждый этап сборки включает выполнение одной из этих инструкций, и если злоумышленник сумеет изменить Dockerfile, то сможет предпринять вредоносные действия, в том числе:
* включить в образ вредоносное программное обеспечение или утилиты для майнинга криптовалют;
* получить доступ к секретным данным сборки;
* получить полную информацию о топологии сети, доступной из инфраструктуры сборки;
* атаковать хост-компьютер, на котором производится сборка.
Повторим очевидное: Dockerfile (как и любой другой исходный код) должен располагать средствами контроля доступа, чтобы иметь защиту от внесения злоумышленниками вредоносных шагов в сборку.
Содержимое Dockerfile также сильно влияет на безопасность полученного в результате сборки образа контейнера. Рассмотрим несколько мер, которые можно предпринять в Dockerfile на практике для повышения безопасности образа.
### Практические рекомендации по безопасности Dockerfile
Все эти рекомендации повышают безопасность образа и сокращают шансы на то, что контейнеры, запущенные из этого образа, будут взломаны злоумышленником.
* Базовые образы. Первая строка Dockerfile представляет собой инструкцию FROM, в которой задается базовый образ для создания нового образа.
* Должна ссылаться на образ из доверенного реестра (см. раздел «Безопасность хранилищ образов» на с. 112).
* Произвольно выбранные сторонние базовые образы могут включать вредоносный код, поэтому некоторые организации требуют использования только заранее одобренных («золотых») базовых образов.
* Чем меньше базовый образ, тем менее вероятно наличие в нем ненужного кода, а значит, меньше и поверхность атаки. Лучше создать образ с нуля (с совершенно пустого образа, подходящего для автономных исполняемых файлов) или использовать минимальный базовый образ, например distroless (https://oreil.ly/kaUEc). Преимущество меньших образов, помимо всего прочего, заключается еще и в более быстрой передаче по сети.
* Задумайтесь, что использовать для ссылки на базовый образ: тег или дайджест. Во втором случае воспроизводимость сборки будет выше,
но снизится вероятность получения новых версий базового образа с обновлениями безопасности. (С другой стороны, вы должны получать недостающие обновления с помощью сканирования собранного образа на уязвимости.)
* Многоэтапная сборка (https://oreil.ly/k34z-) — способ исключения ненужного содержимого из итогового образа. На начальном этапе в образ могут включаться все пакеты и набор программных средств, необходимых для сборки образа, но во время выполнения многие из этих инструментов не нужны. Приведу такой пример: чтобы создать исполняемую программу, написанную на языке Go, нужен компилятор Go. Но контейнеру, в котором выполняется эта программа, доступ к компилятору Go не требуется. В данном примере имеет смысл разбить сборку на несколько этапов: на одном производится компиляция и создается двоичный исполняемый файл; а у следующего этапа просто есть доступ к этому автономному исполняемому файлу. В итоге поверхность атаки развертываемого образа значительно уменьшается; помимо пользы для безопасности, сам образ также будет меньше, а значит, сократится время, требуемое на его извлечение.
* Несуперпользователь. Инструкция USER в Dockerfile указывает, что учетная запись пользователя, с помощью которой по умолчанию запускаются контейнеры, основанные на этом образе, не является суперпользователем. Укажите эту опцию во всех своих Dockerfile, если не хотите, чтобы ваши контейнеры запускались от имени суперпользователя.
* Команды RUN. Отмечу для полной ясности: команда RUN Dockerfile позволяет выполнить любую произвольную команду. Если злоумышленнику удастся внести изменения в Dockerfile, содержащий настройки безопасности по умолчанию, то он сможет выполнять любой код, какой только пожелает. Если у вас есть хотя бы минимальные причины не доверять людям, запускающим сборки контейнеров в вашей системе, то скажу прямо: вы фактически предоставили им полномочия для удаленного выполнения кода. Убедитесь, что полномочия на редактирование Dockerfile
* есть только у доверенных членов вашей команды, и внимательно следите за кодом, меняющим эти настройки. Возможно, имеет смысл даже производить проверку или заносить информацию в журнал аудита при внесении любых новых или изменении существующих команд RUN в Dockerfile.
* Точки монтирования томов. Каталоги хоста часто монтируются к контейнеру с помощью точек монтирования томов, особенно для демонстрации и тестов. Как вы увидите в главе 9, важно проверять, чтобы Dockerfile не монтировали к контейнеру каталоги с конфиденциальными данными, например /etc или /bin.
* Не включайте в Dockerfile конфиденциальные данные. Более подробно конфиденциальные данные и секреты мы обсудим в главе 12, а пока просто учтите, что включение учетных данных, паролей и прочих секретных данных в образ упрощает злоумышленникам доступ к ним.
* Избегайте исполняемых файлов с установленным битом setuid. Как обсуждалось в главе 2, желательно избегать включения в образ исполняемых файлов с установленным битом setuid, поскольку они потенциально могут открывать путь к повышению полномочий.
* Избегайте ненужного кода. Чем меньше в контейнере кода, тем меньше поверхность атаки. Избегайте включения в образ пакетов, библиотек и исполняемых файлов, за исключением абсолютно необходимых. По той же причине желательно использовать в качестве базового образа пустой образ или один из вариантов distroless, ведь это резко уменьшит объем кода — а значит, и уязвимого кода — в вашем образе.
* Включайте все, что действительно нужно контейнеру. Если в предыдущем пункте я призывала вас исключать из сборки лишний код, то здесь вывод иной: включайте в нее все, что требуется вашему приложению для работы. Если разрешить контейнеру устанавливать дополнительные пакеты во время выполнения, то как проверить, что все они не содержат вредоносного кода? Гораздо лучше выполнять установку и проверку всех пакетов при сборке образа контейнера и создать неизменяемый образ. Подробнее о том, почему этот вариант предпочтительнее, рассказывается в подразделе «Неизменяемые контейнеры» на с. 124.
### Атаки на машину сборки
поскольку машины сборки генерируют код, который в дальнейшем будет выполняться в кластере, работающем в промышленной эксплуатации, надежная защита их от атак столь же важна, как и для самого кластера. Старайтесь уменьшить поверхность атаки, удаляя ненужные утилиты из машин сборки. Ограничивайте прямой доступ пользователей к этим машинам и защищайте их от несанкционированного доступа по сети, задействуя VPC и брандмауэры.
Имеет смысл также выполнять сборку на отдельной (от работающего в промышленной эксплуатации кластера) машине или кластере машин, чтобы ограничить возможный эффект атаки на хост из сборки. Ограничьте доступ по сети и к облачным сервисам с этого хоста, чтобы не открыть злоумышленнику доступ к остальным элементам развертываемой системы.
### Безопасность хранилищ образов
Собранный образ необходимо сохранить в реестре. Если злоумышленник сможет заменить или модифицировать образ, то сможет и выполнять любой код, какой пожелает.
#### Запуск собственного реестра
Собственный реестр (или свой экземпляр управляемого реестра) предоставляет больше возможностей для контроля над тем, кто может помещать и извлекать образы. Кроме того, он сокращает вероятность DNS-атаки, позволяющей злоумышленнику подделать адрес реестра.
### Безопасность развертывания образов
Развертывание правильного образа
Как вы видели в разделе «Идентификация образов» на с. 106, теги образов контейнеров не являются неизменяемыми — они могут переноситься на другие версии того же образа. Если же ссылаться на образ по дайджесту, а не тегу, то можно быть уверенными, что версия образа именно та, которая нужна. Однако обычно достаточно, чтобы система сборки присваивала образам теги на основе семантического контроля версий, если это строго соблюдается, поскольку не нужно будет обновлять ссылки на образы при каждом мелком обновлении.
Если же ссылаться на образы по тегу, то необходимо всегда извлекать последнюю версию перед запуском, на случай возможного обновления. К счастью, это требует не слишком большого расхода ресурсов, поскольку сначала извлекается манифест, так что можно извлекать слои образа только в случае их изменений.
В Kubernetes это поведение задается параметром imagePullPolicy. Задавать стратегию извлечения образов всякий раз не нужно, если вы ссылаетесь на них по дайджесту, поскольку любое обновление означает необходимость изменения дайджеста.
В зависимости от профиля риска может понадобиться также проверить происхождение образа, проанализировав его подпись, за которую отвечает утилита наподобие вышеупомянутой Notary.
#### Подписывание образов
Многие реестры реализуют подписывание образов на основе реализации Notary спецификации TUF (The Update Framework) (https://oreil.ly/fMD6d).