Не используйте 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:

  1. В качестве базового образа используется дистрибутив GNU/Linux Ubuntu 24.04
  2. В систему устанавливается пакет git
  3. В директорию /app клонируется репозиторий с программой HelloApp в версии v1.0
  4. Пакет git удаляется из системы
  5. Рабочая директория устанавливается в значение /app
  6. В качестве точки входа используется 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"]

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

Подводные камни

  1. На самом деле коммиты тоже можно удалять.
  2. Поддерживать такой 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"]

Всё, что он делает:

  1. Всё также использует дистрибутив GNU/Linux Ubuntu 24.04 в качестве базового образа
  2. Устанавливает директорию /app в качестве рабочей
  3. Сохраняет архив с кодом приложения во временную директорию. Обратите внимание, что загрузку файлов из интернета можно производить средствами docker. Никакие дополнительные пакеты, например curl или wget устанавливать не требуется!
  4. Извлекает файлы из архива в директорию /app
  5. Удаляет архив из временной директории
  6. Устанавливает уже известную нам точку входа контейнера

Внимательный читатель заподозрит, что этот подход с точки зрения проверки подлинности архива с кодом абсолютно ни чем не отличается от критикуемого автором подхода с использованием 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"]

Теперь, если файл изменится, наш образ просто не соберётся и выдаст сообщение об ошибке

Вывод редакции

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

Сноски:

  1. Это может быть как локальная машина под управлением Unix-подобной операционной системы, так и контейнер с одним из дистрибутивов GNU/Linux, запущенный в интерактивном режиме псевдо-терминала. Microsoft Windows с WSL2 тоже сгодится.
  2. Dockerfile reference. ADD --checksum

Автор текста: Пётр Разумов
Редактор: Евгений Федякин


Попробуйте бесплатные уроки по Python

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

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.