Секреты сборки и giterminism

Уровень: advanced ~50 мин Практика: Vikunja (AGPL-3.0)

Что нужно знать перед уроком

  • M3.05 — Stapel-импорт — нужен опыт с werf.yaml и обеими моделями билдеров для секретов сборки

Что нужно знать перед уроком

M1 урок 4 — Secret в кластере (runtime-секреты, монтируемые в Pod). Здесь — принципиально другая категория: build-time секреты, нужные только во время сборки образа (например, токен доступа к приватному npm-registry), которые не должны попасть в итоговые слои образа.

Теория

Почему секреты нельзя передавать через build-аргументы или обычные ENV

1
2
3
# АНТИПАТТЕРН — никогда так не делайте
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN} && npm ci

Проблема: build-аргументы (ARG) и переменные окружения (ENV), даже если их значение больше не используется в последующих слоях, остаются в метаданных истории образа (docker history, docker inspect) — извлечь секрет из готового образа тривиально даже без доступа к исходному Dockerfile. Это касается и RUN export SECRET=... — переменная не попадает в файловую систему следующего слоя, но команда, её устанавливающая, видна в истории.

secrets: env/id/src/value в werf.yaml

1
2
3
4
5
6
7
8
9
image: stapel-shell
from: ubuntu:24.04
secrets:
  - env: AWS_ACCESS_KEY_ID
  - id: aws_secret_key
    env: AWS_SECRET_ACCESS_KEY
  - src: "~/.aws/credentials"
  - id: plainSecret
    value: plainSecretValue

Четыре способа объявить секрет сборки:

СпособИсточник значенияОбязателен id?
env: VAR_NAMEтекущая переменная окружения на машине, где выполняется werf buildнет, id по умолчанию = имени переменной
id: X + env: VAR_NAMEта же переменная, но с явным собственным id (для читаемости в /run/secrets/<id>)да, явно указан
src: <path>локальный файл (например, ~/.aws/credentials)нет, id по умолчанию = базовому имени файла
id: X + value: <строка>значение прямо в werf.yaml (обычно — само в свою очередь через переменную окружения на этапе рендера шаблона)да, обязателен для value

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

Как читать секрет в shell/ansible/Dockerfile

1
2
3
4
5
# Stapel shell
shell:
  setup:
    - AWS_ACCESS_KEY_ID=$(cat /run/secrets/AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(cat /run/secrets/aws_secret_key) aws s3 cp ...
    - export WERF_BUILD_SECRET=$(cat /run/secrets/plainSecret)
1
2
3
4
# Dockerfile
FROM alpine:3.22
RUN --mount=type=secret,id=plainSecret \
    export WERF_BUILD_SECRET="$(cat /run/secrets/plainSecret)"

Во всех случаях секрет доступен по пути /run/secrets/<id> только в момент выполнения конкретной инструкции, использующей монтирование секрета (--mount=type=secret,id=... в Dockerfile — тот же синтаксис BuildKit, который используется и без werf; для Stapel — секрет доступен во всех shell/ansible-инструкциях данного образа автоматически, без явного mount). Файл не сохраняется ни в одном итоговом слое.

Giterminism: почему обязательное разрешение через werf-giterminism.yaml, а не опция

Giterminism — принцип, введённый werf: конфигурация сборки и весь контекст сборки должны читаться строго из текущего Git-коммита, без скрытых внешних зависимостей (файлы вне репозитория, недекларированные переменные окружения). Любое отклонение от этого правила (использование переменной окружения, файла вне репозитория, секрета) должно быть явно разрешено в отдельном файле werf-giterminism.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
giterminismConfigVersion: 1
config:
  secrets:
    allowEnvVariables:
      - "AWS_ACCESS_KEY_ID"
      - "AWS_SECRET_ACCESS_KEY"
    allowFiles:
      - "~/.aws/credentials"
    allowValueIds:
      - plainSecret

Без соответствующей записи в werf-giterminism.yaml секрет, объявленный в werf.yaml, не будет использован — werf откажется собирать образ с ошибкой валидации. Это не опциональная строгость, а фундаментальный принцип: giterminism гарантирует воспроизводимость сборки любым членом команды или CI-раннером из одного и того же коммита, без скрытых зависимостей от локального окружения конкретной машины разработчика. Без этого принципа сборка «работает у меня локально» становится частой проблемой — если бы секреты (и любые другие внешние входы) допускались без явной декларации, разные машины могли бы собрать разный результат из одного и того же коммита.

Частые ошибки и подводные камни

  • Передавать секрет через ARG/ENV вместо secrets:+giterminism. Секрет физически остаётся в метаданных истории образа — классическая утечка, которую легко найти через docker history.
  • Забыть добавить разрешение в werf-giterminism.yaml. Секрет, объявленный только в werf.yaml, без соответствующей записи allowEnvVariables/allowFiles/allowValueIds в werf-giterminism.yaml, приведёт к ошибке сборки — это осознанное архитектурное решение, а не забытая фича.
  • Хранить value: секрета прямо в werf.yaml как строку в открытом виде. Формально это разрешённый синтаксис, но так секрет попадает в Git-историю самого репозитория — используйте value: только для переменных, вычисляемых в момент рендера Go-шаблонов werf.yaml (например, из переменной окружения CI, а не хардкод).

Практика в кластере

Практики в кластере в этом уроке нет — секреты сборки существуют только на этапе werf build, они принципиально отличаются от runtime Secret из M1 урока 4.

Практика разработки

🧩 Практика урока: Vikunja · лицензия AGPL-3.0
  1. Смоделируйте сборку frontend-образа Vikunja, которому нужен токен для приватного npm-реестра (в реальности Vikunja использует публичный registry, но упражнение иллюстративно):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
image: builder-frontend
from: node:24-alpine
git:
  - add: /frontend
    to: /app
secrets:
  - id: npm_token
    env: NPM_TOKEN
shell:
  install:
    - cd /app
    - NPM_TOKEN=$(cat /run/secrets/npm_token) npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN}
    - npm ci
  1. Создайте werf-giterminism.yaml с явным разрешением:
1
2
3
4
5
giterminismConfigVersion: 1
config:
  secrets:
    allowEnvVariables:
      - "NPM_TOKEN"
  1. Попробуйте собрать без этого файла (временно удалите/переименуйте) и убедитесь, что werf отказывается собирать с понятной ошибкой о неразрешённом секрете; затем восстановите файл и убедитесь, что сборка проходит:
1
2
3
4
5
export NPM_TOKEN="demo-token-value"
mv werf-giterminism.yaml werf-giterminism.yaml.bak
werf build builder-frontend    # ожидаем ошибку giterminism
mv werf-giterminism.yaml.bak werf-giterminism.yaml
werf build builder-frontend    # теперь должно собраться
  1. Проверьте, что секрет не попал в итоговый образ:
1
werf run builder-frontend -- sh -c "cat ~/.npmrc 2>/dev/null | grep -i token || echo 'токен не найден в образе — как и ожидалось'"

Шпаргалка команд урока

1
2
3
4
5
6
7
secrets:
  - env: <VAR>
  - id: <id>
    env: <VAR>
  - src: <path>
  - id: <id>
    value: <string>
1
2
3
4
5
6
7
# werf-giterminism.yaml
giterminismConfigVersion: 1
config:
  secrets:
    allowEnvVariables: ["<VAR>"]
    allowFiles: ["<path>"]
    allowValueIds: ["<id>"]

Вопросы для самопроверки

Почему нельзя передавать секреты через обычные ARG/ENV в Dockerfile?

ARG/ENV сохраняются в метаданных истории слоёв — секрет можно извлечь из готового образа без доступа к исходному Dockerfile.

Источник: Werf — документация — Using build secrets

Где физически доступен секрет сборки werf во время выполнения инструкции?

Секрет монтируется как временный файл /run/secrets/<id>, не сохраняющийся в итоговых слоях образа.

Источник: Werf — документация

Что произойдёт, если секрет объявлен в werf.yaml, но не разрешён в werf-giterminism.yaml?

Giterminism требует явного разрешения любого отклонения (включая секреты) — без записи в werf-giterminism.yaml сборка завершится ошибкой.

Источник: Werf — документация — Configure Giterminism for Build Secrets

Какова главная цель принципа giterminism?

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

Источник: Werf — документация

Обязателен ли id для секрета, заданного через value: в werf.yaml?

Для env/src id по умолчанию берётся из имени переменной/файла; для value id должен быть указан явно.

Источник: Werf — документация — Using secrets in Stapel shell instructions

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Nigel Poulton. Docker Deep Dive. 2023. ISBN 978-1916585206.

Связанные материалы