Не используйте git clone для скачивания кода в Dockerfile
В этой статье повествуется о том, почему использование команд git
, а именно git clone --branch
или git clone && git checkout
, при загрузке исходных кодов во время сборки образов контейнеров Docker, не только не желательно, но и может представлять серьёзную угрозу безопасности. Мы постарались подробнейшим образом описать и рассмотреть проблему, а также дать конкретные рекомендации для её решения. В конце статьи приводятся альтернативные способы для достижения идентичного или приемлемого результата.
Предпосылки
Предположим, что нам необходимо создать образ контейнера Docker для некоего приложения, назовём его HelloApp
. Приложение написано на POSIX shell, последняя версия приложения — v1.0
, исходный код приложения хранится на GitHub.
Общее описание проблемы
Предположим, что для создания образа контейнера Docker для приложения HelloApp
, был подготовлен файл Dockerfile
следующего содержания:
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/przmv/HelloApp.git --branch v1.0 --single-branch /app
RUN apt-get autoremove -y --purge git
WORKDIR /app
ENTRYPOINT ["sh", "hello"]
Перечислим кратко, что описывает этот Dockerfile
:
- В качестве базового образа используется дистрибутив GNU/Linux Ubuntu 24.04
- В систему устанавливается пакет
git
- В директорию /app клонируется репозиторий с программой HelloApp в версии
v1.0
- Пакет
git
удаляется из системы - Рабочая директория устанавливается в значение /app
- В качестве точки входа используется
sh hello
- стандартный способ запуска скриптовPOSIX shell
.
Если собрать образ из такого файла и запустить контейнер, то мы увидим приветствие “Hello world”.
$ sudo docker build -t hello_app:v1.0 .
$ sudo docker run --rm hello_app:v1.0
Hello world
По крайней мере так было на момент написания этой статьи. Сейчас же, если очистить кэш сборки, собрать образ из того же самого файла Dockerfile
и запустить контейнер, то мы увидим совсем другое сообщение:
$ sudo docker builder prune -a
$ sudo docker build -t hello_app:v1.0 .
$ sudo docker run --rm hello_app:v1.0
Goodbye cruel world
Что произошло?
А произошло следующее. В репозиторий был добавлен ещё один коммит, меняющий поведение приложения. Затем тег v1.0
был удалён, но новый тег с точно таким же идентификатором — v1.0
был вновь добавлен, но в этом случае он указывал уже на другой коммит. Таким образом, без какого-либо изменения в Dockerfile
, мы получили ситуацию, когда собранный образ контейнера отличается от того, что изначально предполагал автор. И хорошо, если эти изменения были санкционированы непосредственными разработчиками программного обеспечения. А что, если доступ к репозиторию был скомпрометирован и какой-то вредоносный код был внедрён в исходники? Используя подход с git clone
/ git checkout
обнаружение подмены может произойти слишком поздно.
Использовать хэши коммитов, а не теги
Первое решение, которое приходит в голову: если теги можно удалить, то давайте будем опираться на неудаляемые сущности — коммиты!
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/przmv/HelloApp.git /app && \
cd /app && \
git checkout 3499c4fcb4849c2ca84a96d6a88dc6420dbdc9b6
RUN apt-get autoremove -y --purge git
WORKDIR /app
ENTRYPOINT ["sh", "hello"]
Действительно, при сборке образа из такого файла мы можем быть уверены что получим именно то, что предполагаем, и никакие дальнейшие изменения в репозитории не изменят наш образ при пересборке. Кажется, что вот она удача!
Подводные камни
- На самом деле коммиты тоже можно удалять.
- Поддерживать такой
Dockerfile
в актуальном состоянии не очень-то и удобно. Так же возникает вопрос, а как тегировать образы контейнеров? Использовать хэши? Использовать теги из git-репозитория, которые указывали на коммиты в момент создания образа? Хрупко и неудобно.
Решение – загружать архивы из GitHub
Платформа GitHub даёт нам возможность загружать срезы репозиториев в виде файловых архивов. Используя такой подход, мы можем значительно упростить наш Dockerfile
, сделать итоговый образ немного меньше (за счёт удаления ненужных зависимостей), а так же сократить время сборки. Взгляните на наш Dockerfile
(для этого примера мы опять изменили тег в репозитории, и он снова указывает на коммит с Hello world
):
FROM ubuntu:24.04
WORKDIR /app
ADD https://github.com/przmv/HelloApp/archive/refs/tags/v1.0.tar.gz /tmp/app.tar.gz
RUN tar xzf /tmp/app.tar.gz --strip-components=1 -C /app && rm /tmp/app.tar.gz
ENTRYPOINT ["sh", "hello"]
Всё, что он делает:
- Всё также использует дистрибутив GNU/Linux Ubuntu 24.04 в качестве базового образа
- Устанавливает директорию
/app
в качестве рабочей - Сохраняет архив с кодом приложения во временную директорию.
Обратите внимание, что загрузку файлов из интернета можно производить средствами
docker
. Никакие дополнительные пакеты, напримерcurl
илиwget
устанавливать не требуется! - Извлекает файлы из архива в директорию
/app
- Удаляет архив из временной директории
- Устанавливает уже известную нам точку входа контейнера
Внимательный читатель заподозрит, что этот подход с точки зрения проверки подлинности архива с кодом абсолютно ни чем не отличается от критикуемого автором подхода с использованием git clone
/ git checkout
.
Финальные штрихи
Мы будем использовать методику сверки контрольных сумм. Для этого в командной оболочке (подробности в сноске) выполним следующее:
$ curl -sL https://github.com/przmv/HelloApp/archive/refs/tags/v1.0.tar.gz | sha256sum
Поздравляю - вы получили хеш-сумму файла. Если при загрузке файла возникли сетевые ошибки - просто выполните эту команду повторно.
Немного модифицируем наш Dockerfile, чтобы добавить проверку:
FROM ubuntu:24.04
WORKDIR /app
ADD --checksum=sha256:64eaa95a461ded5b1422ba942821f2c572c948e4b72f93112343dc0deec43e5b https://github.com/przmv/HelloApp/archive/refs/tags/v1.0.tar.gz /tmp/app.tar.gz
RUN tar xzf /tmp/app.tar.gz --strip-components=1 -C /app && rm /tmp/app.tar.gz
ENTRYPOINT ["sh", "hello"]
Теперь, если файл изменится, наш образ просто не соберётся и выдаст сообщение об ошибке
ERROR: failed to solve: digest mismatch.
Вывод редакции
Далеко не всегда очевидные решения являются самыми оптимальными. Чтобы избежать неожиданного поведения вашего кода порой требуется глубокое понимание принципов его работы и большой опыт, который всегда можно наработать в рамках курсов Devman!
Сноски:
- Это может быть как локальная машина под управлением Unix-подобной операционной системы, так и контейнер с одним из дистрибутивов GNU/Linux, запущенный в интерактивном режиме псевдо-терминала. Microsoft Windows с WSL2 тоже сгодится.
- Dockerfile reference. ADD --checksum
Автор текста: Пётр Разумов
Редактор: Евгений Федякин
Попробуйте бесплатные уроки по Python
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.