open atlas
↑ К треку
Docker: контейнеры как система DOCK · 05 · 04

Воспроизводимые сборки: SOURCE_DATE_EPOCH, базы запиненные по дайджесту и почему пляшущий дайджест слоя ломает подпись

Воспроизводимая сборка даёт байт-идентичные дайджесты слоёв из тех же исходников на любой машине. Встроенные таймстампы, незапиненные теги баз и mtime чекаута её ломают — пляшут дайджесты и подрывается подпись. Рычаги: SOURCE_DATE_EPOCH и базы по дайджесту.

DOCK Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Аудит цепочки поставок задал простой вопрос, а команда не смогла ответить: «Пересоберите релиз v2.4.1 из тегированного коммита и покажите тот же дайджест образа, который вы подписали». Они выкатили точный тег, запустили точную команду сборки — и получили совершенно другой дайджест образа. Каждый хеш слоя отличался. Подписанный образ в реестре и пересобранный не делили ни единого слоя, так что не было способа доказать, что развёрнутый артефакт соответствует проаудированным исходникам. Причина была обыденной: Dockerfile использовал FROM node:22 (незапиненный тег, сдвинувшийся с релиза), COPY . . записал mtime файлов чекаута в tarball слоя, а сборка штамповала BUILD_TIME=$(date) в env-переменную. Три источника недетерминизма, каждого достаточно, чтобы сменить дайджест. Образ был совершенно функционален оба раза — тот же код, то же поведение, — но не воспроизводим, а воспроизводимость — это свойство, которое подпись должна была гарантировать. Подпись над дайджестом, который не воспроизвести, не доказывает ничего об исходниках.

Что значит «воспроизводимая» и почему дайджесты пляшут

Если команда подписывает релизные образы, задай себе вопрос: можешь ли ты прямо сейчас пересобрать тот же дайджест из тегированного коммита? Если нет — подпись доказывает меньше, чем кажется. Воспроизводимая значит: те же исходники, собранные где угодно и в любой день, дают образ с теми же дайджестами слоёв до байта. Поскольку всё в OCI контентно-адресуемо, единственный отличающийся байт где угодно в tarball-е слоя меняет sha256 этого слоя и каждый дайджест выше. Три обычных подозреваемых впрыскивают эти отличающиеся байты:

  • Встроенные таймстампы. BUILD_TIME=$(date), build-info-файл с текущим временем или компилятор, штампующий дату сборки — любое пишет значение, меняющееся каждую секунду, так что слой никогда не воспроизводится.
  • Mtime файлов в архиве. Когда COPY . . пишет файлы в слой, tarball слоя записывает время модификации каждого файла. Свежий git checkout ставит mtime в сейчас, так что те же исходники, скопированные в два дня, дают два разных tarball-а с идентичным содержимым, но разными таймстампами — разный дайджест.
  • Незапиненные базовые образы. FROM node:22 резолвится в тот дайджест, на который тег указывает сегодня; тег двигается на каждом минорном патче, так что пересборка спустя месяцы тянет другую базу и наследует её другие слои.

Лекарство от каждого прямое. Пиньте базу по дайджесту: FROM node:22@sha256:abc..., чтобы база не двигалась. Управляйте таймстампами через SOURCE_DATE_EPOCH — стандартизированную env-переменную с фиксированным Unix-временем (обычно дата автора коммита), которую инструменты сборки чтут вместо настенных часов. И заставьте BuildKit нормализовать mtime, которые он пишет.

# syntax=docker/dockerfile:1
FROM node:22@sha256:1a2b3c...      # запинено: база молча не сдвинется
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Подаём фиксированное время и просим BuildKit переписать таймстампы слоёв на него:
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \
  docker buildx build \
    --output type=image,name=app:v2.4.1,rewrite-timestamp=true .

rewrite-timestamp=true велит BuildKit выставить mtime каждого файла в производимых слоях в SOURCE_DATE_EPOCH, стирая разброс времени чекаута. С запиненной базой, запиненными часами и нормализованными mtime две сборки того же коммита на двух машинах дают те же дайджесты.

Викторина

Две сборки одного и того же коммита на двух машинах дают образы с разными дайджестами слоёв. Код и поведение идентичны. Что из этого НЕ является вероятной причиной?

Почему это работает

Почему подпись заботится именно о воспроизводимости? Подпись (cosign, Notary) вычисляется над дайджестом образа. Обещание, которого хочет потребитель: «этот работающий образ соответствует тем проаудированным исходникам». Если вы не можете пересобрать проаудированные исходники в тот же дайджест, подпись вырождается в «кто-то с ключом подписал какие-то байты» — она больше не привязывает артефакт к инспектируемым исходникам. Воспроизводимость — это то, что делает подпись проверяемой: аудитор (или SLSA-проверка provenance) пересобирает из исходников, получает идентичный дайджест и подтверждает, что подписанный артефакт — ровно эти исходники без впрыснутого. Невоспроизводимые сборки также тихо бьют межстадийный и реестровый кеш — пляшущий дайджест значит, что COPY —from и запушенные слои промахиваются каждый раз, — так что воспроизводимость окупается и в безопасности, и в скорости сборки.

Детерминизм за пределами трёх больших рычагов

Пиньте базу, часы и mtime — и это покрывает большинство случаев, но несколько более тонких недетерминизмов стоят знания. Установки пакетов без пина версий (apt-get install curl без =version, pip install без lockfile) тянут что текуще, так что фиксируйте версии или используйте lockfile. Шаги сборки, выдающие вывод в порядке итерации ФС, могут варьироваться; инструменты всё чаще сортируют детерминированно, но кастомный скрипт может потребовать явной сортировки. Встроенные пути сборки (компилятор, записывающий /home/runner/...) различаются между машинами; многие тулчейны принимают remap префикса пути. А сетевой доступ во время сборки по природе невоспроизводим — тот же curl https://... может вернуть другие байты завтра, — поэтому герметичные сборки тянут лишь запиненные, контрольно-суммированные входы. Senior-позиция: трактуйте каждый вход сборки как нечто, что должно быть запинено или нормализовано, и считайте, что любой незапиненный вход в итоге сдвинется и сломает ваш дайджест, когда вы меньше всего хотите.

Викторина

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

Вспомните перед уходом
  1. 01
    Назови три классических источника недетерминизма сборки и рычаг, чинящий каждый.
  2. 02
    Объясни, почему воспроизводимость — это то, что делает подпись образа осмысленной, и что ещё ломает пляшущий дайджест.
Итог

Воспроизводимая значит: те же исходники, собранные на любой машине в любой день, дают образ с байт-идентичными дайджестами слоёв. OCI контентно-адресуемо, так что один отличающийся байт в tarball-е слоя меняет sha256 этого слоя и каждый дайджест, сложенный выше, и три обычных подозреваемых впрыскивают этот байт. Встроенные таймстампы — BUILD_TIME=$(date), build-info-файл, компилятор, штампующий дату — меняются каждую секунду. Mtime файлов, записанные в слой через COPY . ., различаются, потому что свежий git checkout ставит их в сейчас, так что идентичное содержимое воспроизводится как разные tarball-ы. А незапиненные теги баз вроде FROM node:22 резолвятся в то, на что тег указывает сегодня, и двигаются под вами между сборками. Рычаги прямые: пиньте базу по дайджесту (FROM node:22@sha256:…), управляйте каждым таймстампом сборки через SOURCE_DATE_EPOCH (SOURCE_DATE_EPOCH — стандартизированная env-переменная с фиксированным Unix-временем, которую инструменты сборки чтут вместо настенных часов), выставленный в дату автора коммита, и запускайте BuildKit с rewrite-timestamp=true, чтобы он нормализовал mtime каждого файла в производимых слоях в этот epoch; за пределами большой тройки пиньте версии пакетов или используйте lockfile, сортируйте вывод в порядке итерации, ремапьте встроенные пути сборки и избегайте сетевых шагов сборки, тянущих незапиненные байты. Это важнее всего для цепочки поставок: подпись (cosign) вычисляется над дайджестом образа, так что если вы не можете пересобрать проаудированный коммит в тот же дайджест, подпись больше не доказывает, что развёрнутый артефакт соответствует инспектируемым исходникам — ровно тот аудит, что команда провалила, когда v2.4.1 пересобрался в другой хеш. Воспроизводимость — это то, что делает подпись проверяемой, позволяет SLSA-проверкам provenance подтвердить артефакт пересборкой и бонусом держит межстадийный COPY —from и реестровые кеши слоёв в попаданиях вместо пляски свежего слоя на каждую функционально идентичную сборку. Теперь, когда выстраиваешь релизный пайплайн, сделай «можем ли мы воспроизвести этот дайджест?» обязательным гейтом: запини базу, передай SOURCE_DATE_EPOCH, добавь rewrite-timestamp=true — и верифицируй пересборкой тегированного коммита, прежде чем доверять подписи.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.