From 44a96a49ef2aad30ac33d6ffd9d4c47b814cf5c7 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Fri, 23 Oct 2026 09:54:05 +0300 Subject: [PATCH] docs(build, stapel): add a step-by-step guide to signing files inside the image The sign directive was documented only as a reference section in the stapel imports page, which left the setup around it to guesswork: what a Vault role needs, how the key and the certificate reach the build, which commands need the certificate and which the key, how to check the signature and how to move signing out of a build stage. Add a guide that walks through it end to end, link it from the reference, and describe before/after as optional in the werf.yaml reference. The guide also states two things the reference left out: a local key file has to be the encrypted delivery-kit PEM, and a build that passes a key logs in to Vault at startup even when the sign stage is cached. The reference example now signs a generic archive, matching the guide. Signed-off-by: Radmir Khurum --- docs/_data/sidebars/_documentation.yml | 6 + docs/_data/werf_yaml.yml | 8 +- docs/pages_en/usage/build/signing_files.md | 229 ++++++++++++++++++++ docs/pages_en/usage/build/stapel/imports.md | 19 +- docs/pages_ru/usage/build/signing_files.md | 229 ++++++++++++++++++++ docs/pages_ru/usage/build/stapel/imports.md | 19 +- 6 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 docs/pages_en/usage/build/signing_files.md create mode 100644 docs/pages_ru/usage/build/signing_files.md diff --git a/docs/_data/sidebars/_documentation.yml b/docs/_data/sidebars/_documentation.yml index f5e1b0ecc7..9fcc55826a 100644 --- a/docs/_data/sidebars/_documentation.yml +++ b/docs/_data/sidebars/_documentation.yml @@ -64,6 +64,9 @@ entries: - title: SBOM roles and workflow url: /usage/build/sbom_workflow.html + - title: Signing files inside the image + url: /usage/build/signing_files.html + - title: Deploy f: - title: Overview @@ -234,6 +237,9 @@ entries: - title: Роли и процесс работы со SBOM url: /usage/build/sbom_workflow.html + - title: Подписание файлов внутри образа + url: /usage/build/signing_files.html + - title: Развертывание f: - title: Обзор diff --git a/docs/_data/werf_yaml.yml b/docs/_data/werf_yaml.yml index d9243cad7e..44feeec4ee 100644 --- a/docs/_data/werf_yaml.yml +++ b/docs/_data/werf_yaml.yml @@ -946,13 +946,13 @@ sections: - name: before value: "string" description: - en: "The stage name before which to sign the file. At present, only install and setup stages are supported" - ru: "Стадия, перед которой подписать файл. Поддерживаются только стадии install и setup" + en: "The stage name before which to sign the file. At present, only install and setup stages are supported. Optional: with neither before nor after the file is signed at the end of the image" + ru: "Стадия, перед которой подписать файл. Поддерживаются только стадии install и setup. Необязательна: без before и after файл подписывается в конце образа" - name: after value: "string" description: - en: "The stage name after which to sign the file. At present, only install and setup stages are supported" - ru: "Стадия, после которой подписать файл. Поддерживаются только стадии install и setup" + en: "The stage name after which to sign the file. At present, only install and setup stages are supported. Optional: with neither before nor after the file is signed at the end of the image" + ru: "Стадия, после которой подписать файл. Поддерживаются только стадии install и setup. Необязательна: без before и after файл подписывается в конце образа" - name: dependencies description: en: "Dependencies images for current image" diff --git a/docs/pages_en/usage/build/signing_files.md b/docs/pages_en/usage/build/signing_files.md new file mode 100644 index 0000000000..d2f6fc4815 --- /dev/null +++ b/docs/pages_en/usage/build/signing_files.md @@ -0,0 +1,229 @@ +--- +title: Signing files inside the image +permalink: usage/build/signing_files.html +--- + +The `sign` directive of a Stapel image signs files the image produces — a release archive, a configuration or policy bundle, a firmware blob — and puts a detached signature next to each of them as `.p7s`. The private key never enters a build container: werf reads the file out of the image, signs it on the build host and adds the signature as a new layer. + +Use it when: + +- an image ships a file whose consumer checks a signature, for example an agent that loads a policy bundle only when its signature chains to a trusted CA; +- the signing key lives in a KMS (Vault Transit) and the build containers must not receive its credentials; +- [SBOM]({{ "/usage/build/sbom.html" | true_relative_url }}) is enabled, so shell stages run without network and cannot reach the KMS themselves. + +The full list of options is in the [reference section]({{ "/usage/build/stapel/imports.html#signing-files-inside-the-image" | true_relative_url }}). This page walks through setting signing up end to end. + +## How it works + +1. You list the files to sign in `werf.yaml` and, for each of them, the point of the build where it is complete. +2. At that point werf adds a sign stage. The stage reads the file out of the image built so far, signs it with the key passed on the command line and writes `.p7s` next to it. +3. The stage container always runs with `network: none`, and none of the image's commands run in it. +4. The stages that follow, and the images that import from this one, see the file together with its signature. + +The signature is a detached PKCS#7 (CMS) `SignedData`: SHA-256, RSASSA-PKCS1-v1_5, no authenticated attributes, the signing certificate embedded. This is the shape `systemd-keyutil pkcs7` produces, and OpenSSL or any other CMS implementation verifies it. + +## Prerequisites + +- **An RSA key.** Either a Vault Transit key of an RSA type or a local key file. Other key types are rejected. A local key file must be an encrypted PEM with an empty passphrase (`ENCRYPTED DELIVERY-KIT PRIVATE KEY`); a plain OpenSSL key fails with `decrypt: ...`. One way to convert an existing RSA key is cosign: + + ```shell + COSIGN_PASSWORD="" cosign import-key-pair --key rsa.pem --output-key-prefix files-signing + sed 's/ENCRYPTED SIGSTORE PRIVATE KEY/ENCRYPTED DELIVERY-KIT PRIVATE KEY/' files-signing.key > files-signing.dk.key + ``` +- **The certificate of that key** (the leaf, PEM). werf does not fetch it from a KMS or a PKI: the pipeline obtains it and passes it as a path or base64. The certificate is public, so it can also be kept in the repository. +- **For Vault Transit, a role allowed to use the key.** werf signs through `/sign//sha2-256` and reads the public key from `/keys/` to check that it matches the certificate, so the policy needs both: + + ```hcl + path "transit/sign/files-signing/sha2-256" { + capabilities = ["update"] + } + + path "transit/keys/files-signing" { + capabilities = ["read"] + } + ``` + +## Step 1. Declare the files in `werf.yaml` + +Add a `sign` list to the image that produces the file: + +```yaml +image: policies +from: alpine:3.20 +git: +- add: /policies + to: /src/policies +shell: + install: + - mkdir -p /out + - tar -C /src/policies -czf /out/policies.tar.gz . +sign: +- path: /out/policies.tar.gz +``` + +Without `before` and `after` the file is signed at the end of the image, once its filesystem is complete. This is the right choice in most cases: the signature covers exactly what the image ships, and every image that imports from this one gets it. + +To sign at a stage boundary instead, set one of `before` or `after` to `install` or `setup`: + +| Entry | The file is signed | The `.p7s` is visible in | +|---|---|---| +| no `before`/`after` | at the end of the image | images importing from this one | +| `before: install` | before `install` | `install`, `beforeSetup`, `setup` | +| `after: install` | after `install` | `beforeSetup`, `setup` | +| `before: setup` | before `setup` | `setup` | +| `after: setup` | after `setup` | images importing from this one | + +Rules: + +- `path` is an absolute path to a regular file that exists in the image at that point. A directory, a relative path or `/` is an error. +- Each `path` appears once in the list. +- `before` and `after` cannot be set together. +- An entry cannot sign the `.p7s` of another entry at the same position. To sign a signature, give the second entry a later position. +- The directive exists only in Stapel images. + +## Step 2 (optional). Check the signature during the build + +Sign at a stage boundary and verify the signature in the next stage, where a failure is cheap to diagnose: + +```yaml +sign: +- path: /out/policies.tar.gz + after: install +shell: + beforeInstall: + - apk add --no-cache openssl + install: + - mkdir -p /out + - tar -C /src/policies -czf /out/policies.tar.gz . + setup: + - openssl smime -verify -inform der -in /out/policies.tar.gz.p7s -content /out/policies.tar.gz -noverify -out /dev/null +``` + +`-noverify` checks the signature over the content but not the certificate chain. To check the chain too, put the CA certificate into the image and pass it with `-CAfile` instead. + +## Step 3. Pass the signature to other images + +An image that imports a signed file has to import its signature as well: + +```yaml +image: agent +from: alpine:3.20 +import: +- image: policies + add: /out + to: /etc/agent + after: install + includePaths: + - policies.tar.gz + - policies.tar.gz.p7s +``` + +With the default position the signature is already in the imported image. With `before`/`after`, the file and its signature are in the image as soon as that position is passed. + +## Step 4. Provide the key and the certificate + +The key and the certificate are never part of `werf.yaml`. They come from the command line or the environment, separately from `--sign-key`/`--sign-cert` that sign manifests and SBOMs. + +With a PEM key: + +```shell +export WERF_SIGN_BUILD_FILE_KEY="$PWD/files-signing.dk.key" +export WERF_SIGN_BUILD_FILE_CERT="$PWD/files-signing.pem" +werf build --repo registry.example.com/project +``` + +With a Vault Transit key and a JWT login, for example from GitLab CI: + +```yaml +build: + id_tokens: + VAULT_ID_TOKEN: + aud: https://vault.example.com + variables: + WERF_SIGN_BUILD_FILE_KEY: hashivault://files-signing + WERF_SIGN_BUILD_FILE_CERT: ci/files-signing.pem + WERF_SIGN_BUILD_FILE_VAULT_ADDR: https://vault.example.com + WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH: transit + WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH: gitlab + WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE: files-signer + script: + - export WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT="$VAULT_ID_TOKEN" + - werf build --repo "$WERF_REPO" +``` + +If the certificate is issued by Vault PKI, fetch it before the build, for example `vault read -field=certificate pki/cert/ > ci/files-signing.pem`. + +How the Vault options combine: + +- `--sign-build-file-vault-addr` and `--sign-build-file-vault-transit-path` fall back one by one to `VAULT_ADDR` and `TRANSIT_SECRET_ENGINE_PATH`. +- The authentication is taken as a whole. With `--sign-build-file-vault-auth-jwt`, werf logs in with that JWT, the role from `--sign-build-file-vault-auth-role` and the mount from `--sign-build-file-vault-auth-path` (`jwt` when unset). Each of them can come from its flag or its `WERF_SIGN_BUILD_FILE_VAULT_AUTH_*` variable, but none falls back to the authentication `--sign-key` uses. +- Without a JWT, werf logs in the same way `--sign-key` does, from the environment. A role or a mount without a JWT is an error. + +All `sign` entries of a build share this one signer. + +## Step 5. Run the other commands + +The certificate takes part in the digest of the sign stage, and so in the image tag. Every command that works out the stages of an image with `sign` needs it — not only `build`, but also `converge`, `render`, `plan`, `run`, `compose`, `kube-run`, `export`, `bundle publish`, `lint`, `sbom get`, `stages copy`, `stage image` and `helm get-autogenerated-values`. Without it they fail before any stage runs. + +The private key is needed only where a signature is produced, that is, where the sign stage is not in the registry yet. A deploy job that uses images built earlier needs the certificate only: + +```shell +export WERF_SIGN_BUILD_FILE_CERT="$PWD/files-signing.pem" +werf converge --require-built-images --repo registry.example.com/project +``` + +When none of the processed images and none of their dependencies declare `sign`, neither the certificate nor the key is needed. + +A declared `sign` is a requirement of the image: there is no option that builds it unsigned. + +## Check the result + +Take the file and its signature out of the built image and verify them on any host: + +```shell +crane export registry.example.com/project: - | tar -x out/policies.tar.gz out/policies.tar.gz.p7s +openssl smime -verify -inform der -in out/policies.tar.gz.p7s -content out/policies.tar.gz -CAfile ca.pem -purpose any -out /dev/null +``` + +`openssl asn1parse -inform der -in out/policies.tar.gz.p7s` shows the structure: `sha256`, `rsaEncryption`, no signed attributes, one embedded certificate. + +## Caching and key rotation + +The sign stage is cached like any other. Its digest covers the stages before it, the signed paths, the position, the certificate and the signature format: + +- a rebuild with the same inputs and the same certificate reuses the cached signature and sends no signing request. When a key is passed, werf still logs in to Vault and reads the public key at startup to match it against the certificate, so leave the key out of jobs that only use images built earlier; +- a different certificate rebuilds the sign stage and every stage after it, so the image gets a new tag; +- a different JWT, Vault address or key reference does not change the tag. + +To rotate the key, pass the new key and its certificate together. A certificate that does not match the key fails the build with `public key in certificate does not match the provided public key`. Images built earlier keep their old signatures. + +## Moving signing out of a build stage + +If an image signs a file in a shell stage today, by logging in to a KMS from the container: + +1. Replace the login and signing commands with a `sign` entry for the file. +2. Move any verification of the `.p7s` to a stage after the signing position (see step 2): the stage that produces the file no longer sees its signature. +3. Remove the secrets and the mounts that only served signing (KMS address, tokens, the KMS client binary) from the image, and the matching `secrets.allowEnvVariables` and `mount.allowFromPaths` entries from `werf-giterminism.yaml` once nothing else uses them. +4. In CI, pass the key, the certificate and the Vault options as in step 4, and the certificate to every other job that runs werf with this `werf.yaml`. + +## Troubleshooting + +| Message | Cause and fix | +|---|---| +| `images declare files to sign: specify the signing certificate with --sign-build-file-cert` | The command processes an image with `sign` and has no certificate. Pass `--sign-build-file-cert`/`WERF_SIGN_BUILD_FILE_CERT`, including to deploy jobs. | +| `image "" declares files to sign and this stage is not built yet: specify the private key with --sign-build-file-key` | The sign stage has to be built and there is no key. Pass the key, or use images built earlier (`--require-built-images`). | +| `read to sign from image "": ...` | The file does not exist at the signing position. Move the entry to a later position or drop `before`/`after`. | +| `... is a directory, a regular file is required` or `not a regular file` | `path` points to a directory or a special file. Sign a regular file. | +| `public key in certificate does not match the provided public key` | The certificate belongs to another key. Pass the certificate of this key. | +| `signing certificate carries a ... public key, only RSA is supported for detached PKCS#7` | The key is not RSA. Use an RSA key. | +| `the signing certificate does not verify the signature the key produced` | The KMS signed with another key or key version than the certificate describes. Check the key name and the certificate. | +| `--sign-build-file-vault-auth-role and --sign-build-file-vault-auth-path qualify --sign-build-file-vault-auth-jwt: ...` | A role or a mount is set without a JWT. Pass the JWT as well, or drop them. | +| Vault `permission denied` on `/keys/` or `/sign//sha2-256` | The role policy lacks `read` on the key or `update` on signing. See the prerequisites. | +| `specify only one stage using ...`, `duplicated ... for sign!`, `conflicting ... for sign: ...` | The `sign` list breaks one of the rules of step 1. | + +## Limitations + +- Only Stapel images; the positions are the boundaries of `install` and `setup`, and the end of the image. +- Only RSA keys, SHA-256 and a detached PKCS#7 signature in a separate `.p7s`. A signature embedded into the file itself is not produced. +- One signer per build for all `sign` entries. +- Only the signing certificate is embedded; `--sign-build-file-intermediates` does not take part in the signature or in the digest. diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index 4d1ae8304b..ab1b849a2e 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -97,25 +97,28 @@ A trailing slash in `to` (anything other than `to: /`) is a configuration error ## Signing files inside the image -The `sign` directive signs a file the image produces and lays the signature down next to it, without giving the build stage access to the signing key. +The `sign` directive signs a file the image produces and lays the signature down next to it, without giving the build stage access to the signing key. For a step-by-step setup, including CI and deployment, see [Signing files inside the image]({{ "/usage/build/signing_files.html" | true_relative_url }}). With neither `before` nor `after`, the file is signed at the end of the image, once its filesystem is complete — that is what a signed artifact of the image usually means, and it is what another image importing the result gets. Naming a stage puts the signature earlier: it fits the same place in the stage chain as `import`, the file must exist after the stage named by `before` or `after`, and the signature is in place for the stages that follow. Setting both is a configuration error. ```yaml -image: sysext -from: registry.example.com/base/ubuntu:22.04 +image: policies +from: alpine:3.20 +git: +- add: /policies + to: /src/policies shell: install: - - mkfs.erofs /out/ext.raw /src - - veritysetup format --root-hash-file=/out/ext.roothash /out/ext.raw /out/ext.verity + - mkdir -p /out + - tar -C /src/policies -czf /out/policies.tar.gz . sign: -- path: /out/ext.roothash +- path: /out/policies.tar.gz after: install ``` Entries at the same signing position must not use another entry's `.p7s` as an input. For example, `path: /out/x` and `path: /out/x.p7s` at the same position are a configuration error, regardless of their order in the list. To sign a generated signature, put the second entry at a later signing position. -werf reads the file out of the image built so far, signs it on the build host and adds `.p7s` as a new layer. The signature is a detached PKCS#7 (CMS) `SignedData` over the file content — SHA-256, RSASSA-PKCS1-v1_5, no authenticated attributes, the signing certificate embedded — the same shape `systemd-keyutil pkcs7` produces, so systemd and the Linux kernel accept it as a dm-verity root hash signature. The key therefore has to be RSA. +werf reads the file out of the image built so far, signs it on the build host and adds `.p7s` as a new layer. The signature is a detached PKCS#7 (CMS) `SignedData` over the file content — SHA-256, RSASSA-PKCS1-v1_5, no authenticated attributes, the signing certificate embedded — the same shape `systemd-keyutil pkcs7` produces, verified by OpenSSL or any other CMS implementation. The key therefore has to be RSA. The key and the certificate are never part of `werf.yaml`; they come from the command line only, separately from the manifest and SBOM signing identity: @@ -132,7 +135,7 @@ The key and the certificate are never part of `werf.yaml`; they come from the co The `--sign-build-file-vault-*` options exist so that the files signer and the `--sign-key` signer can live in different Vault mounts and log in under different roles within one `werf build`. -The address and the transit mount fall back one by one: either left unset takes what `--sign-key` would use (`VAULT_ADDR`, `TRANSIT_SECRET_ENGINE_PATH`). The authentication does not work that way — it is taken as a whole. With a JWT given, the files signer logs in with that JWT, the role of `--sign-build-file-vault-auth-role` (empty when unset) and the mount of `--sign-build-file-vault-auth-path` (`jwt` when unset); none of the three falls back to the environment. Without a JWT the whole authentication comes from the environment, and a role or a mount passed on their own are rejected. The authentication is validated as soon as werf resolves a declared `sign` — including a command that only has the certificate and never logs in to Vault at all. +The address and the transit mount fall back one by one: either left unset takes what `--sign-key` would use (`VAULT_ADDR`, `TRANSIT_SECRET_ENGINE_PATH`). The authentication does not work that way — it is taken as a whole. With a JWT given, the files signer logs in with that JWT, the role of `--sign-build-file-vault-auth-role` (empty when unset) and the mount of `--sign-build-file-vault-auth-path` (`jwt` when unset); none of the three falls back to the authentication `--sign-key` uses. Without a JWT the whole authentication comes from the environment, and a role or a mount passed on their own are rejected. The authentication is validated as soon as werf resolves a declared `sign` — including a command that only has the certificate and never logs in to Vault at all. There is no way to build such an image unsigned: a declared `sign` is a requirement of the image, not a build-time choice. diff --git a/docs/pages_ru/usage/build/signing_files.md b/docs/pages_ru/usage/build/signing_files.md new file mode 100644 index 0000000000..5e0aeb2077 --- /dev/null +++ b/docs/pages_ru/usage/build/signing_files.md @@ -0,0 +1,229 @@ +--- +title: Подписание файлов внутри образа +permalink: usage/build/signing_files.html +--- + +Директива `sign` Stapel-образа подписывает файлы, которые производит образ, — архив релиза, пакет конфигурации или политик, прошивку — и кладёт рядом с каждым отсоединённую подпись `.p7s`. Приватный ключ не попадает в сборочный контейнер: werf читает файл из образа, подписывает его на сборочном хосте и добавляет подпись новым слоем. + +Директива нужна, когда: + +- образ поставляет файл, подпись которого проверяет потребитель, например агент, который загружает пакет политик, только если его подпись выстраивается в цепочку до доверенного CA; +- ключ подписи хранится в KMS (Vault Transit), а сборочные контейнеры не должны получать доступ к нему; +- включён [SBOM]({{ "/usage/build/sbom.html" | true_relative_url }}), поэтому shell-стадии выполняются без сети и сами до KMS не доберутся. + +Полный список опций — в [справочном разделе]({{ "/usage/build/stapel/imports.html#подписание-файлов-внутри-образа" | true_relative_url }}). На этой странице — настройка подписания от начала до конца. + +## Как это работает + +1. В `werf.yaml` перечисляются подписываемые файлы и для каждого — момент сборки, когда он готов. +2. В этот момент werf добавляет стадию подписания. Стадия читает файл из собранного к этому моменту образа, подписывает его ключом из командной строки и кладёт рядом `.p7s`. +3. Контейнер стадии всегда запускается с `network: none`, и команды образа в нём не выполняются. +4. Последующие стадии и образы, которые импортируют из этого, видят файл вместе с подписью. + +Подпись — отсоединённый PKCS#7 (CMS) `SignedData`: SHA-256, RSASSA-PKCS1-v1_5, без аутентифицированных атрибутов, со встроенным сертификатом подписанта. Именно такую форму производит `systemd-keyutil pkcs7`, и её проверяет OpenSSL или любая другая реализация CMS. + +## Что понадобится + +- **RSA-ключ.** Ключ Vault Transit RSA-типа или локальный файл ключа. Ключи других типов отвергаются. Локальный файл ключа должен быть зашифрованным PEM с пустым паролем (`ENCRYPTED DELIVERY-KIT PRIVATE KEY`); обычный ключ OpenSSL не принимается — ошибка `decrypt: ...`. Сконвертировать существующий RSA-ключ можно, например, через cosign: + + ```shell + COSIGN_PASSWORD="" cosign import-key-pair --key rsa.pem --output-key-prefix files-signing + sed 's/ENCRYPTED SIGSTORE PRIVATE KEY/ENCRYPTED DELIVERY-KIT PRIVATE KEY/' files-signing.key > files-signing.dk.key + ``` +- **Сертификат этого ключа** (leaf, PEM). werf не запрашивает его ни из KMS, ни из PKI: пайплайн получает его сам и передаёт путём или в base64. Сертификат публичный, его можно хранить и в репозитории. +- **Для Vault Transit — роль с доступом к ключу.** werf подписывает через `/sign//sha2-256` и читает открытый ключ из `/keys/`, чтобы сверить его с сертификатом, поэтому политике нужны оба права: + + ```hcl + path "transit/sign/files-signing/sha2-256" { + capabilities = ["update"] + } + + path "transit/keys/files-signing" { + capabilities = ["read"] + } + ``` + +## Шаг 1. Объявить файлы в `werf.yaml` + +Добавьте список `sign` в образ, который производит файл: + +```yaml +image: policies +from: alpine:3.20 +git: +- add: /policies + to: /src/policies +shell: + install: + - mkdir -p /out + - tar -C /src/policies -czf /out/policies.tar.gz . +sign: +- path: /out/policies.tar.gz +``` + +Без `before` и `after` файл подписывается в конце образа, когда его файловая система уже собрана. В большинстве случаев это правильный выбор: подпись покрывает ровно то, что поставляет образ, и её получает каждый образ, импортирующий из этого. + +Чтобы подписать на границе стадии, задайте `before` или `after` со значением `install` или `setup`: + +| Запись | Файл подписывается | `.p7s` виден в | +|---|---|---| +| без `before`/`after` | в конце образа | образах, импортирующих из этого | +| `before: install` | перед `install` | `install`, `beforeSetup`, `setup` | +| `after: install` | после `install` | `beforeSetup`, `setup` | +| `before: setup` | перед `setup` | `setup` | +| `after: setup` | после `setup` | образах, импортирующих из этого | + +Правила: + +- `path` — абсолютный путь к обычному файлу, который существует в образе в этот момент. Директория, относительный путь или `/` — ошибка. +- Каждый `path` встречается в списке один раз. +- `before` и `after` одновременно задавать нельзя. +- Запись не может подписывать `.p7s` другой записи на той же позиции. Чтобы подписать подпись, задайте второй записи более позднюю позицию. +- Директива есть только у Stapel-образов. + +## Шаг 2 (необязательный). Проверить подпись во время сборки + +Подпишите на границе стадии и проверьте подпись в следующей стадии, где ошибку легко разобрать: + +```yaml +sign: +- path: /out/policies.tar.gz + after: install +shell: + beforeInstall: + - apk add --no-cache openssl + install: + - mkdir -p /out + - tar -C /src/policies -czf /out/policies.tar.gz . + setup: + - openssl smime -verify -inform der -in /out/policies.tar.gz.p7s -content /out/policies.tar.gz -noverify -out /dev/null +``` + +`-noverify` проверяет подпись над содержимым, но не цепочку сертификатов. Чтобы проверить и цепочку, положите сертификат CA в образ и передайте его через `-CAfile` вместо `-noverify`. + +## Шаг 3. Передать подпись в другие образы + +Образ, который импортирует подписанный файл, должен импортировать и его подпись: + +```yaml +image: agent +from: alpine:3.20 +import: +- image: policies + add: /out + to: /etc/agent + after: install + includePaths: + - policies.tar.gz + - policies.tar.gz.p7s +``` + +При позиции по умолчанию подпись уже есть в импортируемом образе. С `before`/`after` файл и подпись есть в образе, как только эта позиция пройдена. + +## Шаг 4. Передать ключ и сертификат + +Ключ и сертификат никогда не попадают в `werf.yaml`. Они передаются через командную строку или окружение, отдельно от `--sign-key`/`--sign-cert`, которыми подписываются манифесты и SBOM. + +С ключом в PEM-файле: + +```shell +export WERF_SIGN_BUILD_FILE_KEY="$PWD/files-signing.dk.key" +export WERF_SIGN_BUILD_FILE_CERT="$PWD/files-signing.pem" +werf build --repo registry.example.com/project +``` + +С ключом Vault Transit и входом по JWT, например из GitLab CI: + +```yaml +build: + id_tokens: + VAULT_ID_TOKEN: + aud: https://vault.example.com + variables: + WERF_SIGN_BUILD_FILE_KEY: hashivault://files-signing + WERF_SIGN_BUILD_FILE_CERT: ci/files-signing.pem + WERF_SIGN_BUILD_FILE_VAULT_ADDR: https://vault.example.com + WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH: transit + WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH: gitlab + WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE: files-signer + script: + - export WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT="$VAULT_ID_TOKEN" + - werf build --repo "$WERF_REPO" +``` + +Если сертификат выпускает Vault PKI, получите его до сборки, например `vault read -field=certificate pki/cert/ > ci/files-signing.pem`. + +Как сочетаются опции Vault: + +- `--sign-build-file-vault-addr` и `--sign-build-file-vault-transit-path` по отдельности подхватывают `VAULT_ADDR` и `TRANSIT_SECRET_ENGINE_PATH`, если не заданы. +- Аутентификация берётся целиком. С `--sign-build-file-vault-auth-jwt` werf входит с этим JWT, ролью из `--sign-build-file-vault-auth-role` и mount из `--sign-build-file-vault-auth-path` (`jwt`, если не задан). Каждое из значений можно задать флагом или своей переменной `WERF_SIGN_BUILD_FILE_VAULT_AUTH_*`, но ни одно не подхватывается из аутентификации `--sign-key`. +- Без JWT werf входит так же, как `--sign-key`, — из окружения. Роль или mount без JWT — ошибка. + +Все записи `sign` в одной сборке используют этого одного подписанта. + +## Шаг 5. Запустить остальные команды + +Сертификат участвует в дайджесте стадии подписания, а значит, и в теге образа. Он нужен каждой команде, которая вычисляет стадии образа с `sign`, — не только `build`, но и `converge`, `render`, `plan`, `run`, `compose`, `kube-run`, `export`, `bundle publish`, `lint`, `sbom get`, `stages copy`, `stage image` и `helm get-autogenerated-values`. Без него они падают до запуска стадий. + +Приватный ключ нужен только там, где создаётся подпись, то есть где стадии подписания ещё нет в registry. Джобе деплоя, которая использует собранные ранее образы, достаточно сертификата: + +```shell +export WERF_SIGN_BUILD_FILE_CERT="$PWD/files-signing.pem" +werf converge --require-built-images --repo registry.example.com/project +``` + +Если ни обрабатываемые образы, ни их зависимости не объявляют `sign`, не нужны ни сертификат, ни ключ. + +Объявленная директива `sign` — требование образа: опции, которая собрала бы его без подписи, нет. + +## Проверить результат + +Достаньте файл и подпись из собранного образа и проверьте их на любом хосте: + +```shell +crane export registry.example.com/project: - | tar -x out/policies.tar.gz out/policies.tar.gz.p7s +openssl smime -verify -inform der -in out/policies.tar.gz.p7s -content out/policies.tar.gz -CAfile ca.pem -purpose any -out /dev/null +``` + +`openssl asn1parse -inform der -in out/policies.tar.gz.p7s` показывает структуру: `sha256`, `rsaEncryption`, без подписанных атрибутов, один встроенный сертификат. + +## Кэширование и ротация ключа + +Стадия подписания кэшируется как любая другая. Её дайджест включает предыдущие стадии, подписываемые пути, позицию, сертификат и формат подписи: + +- пересборка с теми же входными данными и тем же сертификатом переиспользует закэшированную подпись и не отправляет запросов на подписание. Если ключ передан, werf всё равно входит в Vault и читает открытый ключ при запуске, чтобы сверить его с сертификатом, поэтому в джобы, которые только используют собранные ранее образы, ключ не передавайте; +- другой сертификат пересобирает стадию подписания и все стадии после неё, поэтому образ получает новый тег; +- другой JWT, адрес Vault или ссылка на ключ тег не меняют. + +Чтобы сменить ключ, передайте новый ключ вместе с его сертификатом. Сертификат, который не соответствует ключу, роняет сборку с ошибкой `public key in certificate does not match the provided public key`. Собранные ранее образы сохраняют старые подписи. + +## Перенос подписания из сборочной стадии + +Если сейчас образ подписывает файл в shell-стадии, входя в KMS из контейнера: + +1. Замените команды входа и подписания записью `sign` для этого файла. +2. Перенесите проверку `.p7s` в стадию после позиции подписания (см. шаг 2): стадия, которая производит файл, его подпись больше не видит. +3. Уберите из образа секреты и монтирования, которые нужны были только для подписания (адрес KMS, токены, бинарник клиента KMS), а из `werf-giterminism.yaml` — соответствующие записи `secrets.allowEnvVariables` и `mount.allowFromPaths`, если они больше нигде не используются. +4. В CI передайте ключ, сертификат и опции Vault, как в шаге 4, а сертификат — каждой джобе, которая запускает werf с этим `werf.yaml`. + +## Решение проблем + +| Сообщение | Причина и что делать | +|---|---| +| `images declare files to sign: specify the signing certificate with --sign-build-file-cert` | Команда обрабатывает образ с `sign`, а сертификата нет. Передайте `--sign-build-file-cert`/`WERF_SIGN_BUILD_FILE_CERT`, в том числе в джобы деплоя. | +| `image "" declares files to sign and this stage is not built yet: specify the private key with --sign-build-file-key` | Стадию подписания нужно собрать, а ключа нет. Передайте ключ или используйте собранные ранее образы (`--require-built-images`). | +| `read to sign from image "": ...` | На позиции подписания файла нет. Перенесите запись на более позднюю позицию или уберите `before`/`after`. | +| `... is a directory, a regular file is required` или `not a regular file` | `path` указывает на директорию или специальный файл. Подписывайте обычный файл. | +| `public key in certificate does not match the provided public key` | Сертификат принадлежит другому ключу. Передайте сертификат этого ключа. | +| `signing certificate carries a ... public key, only RSA is supported for detached PKCS#7` | Ключ не RSA. Используйте RSA-ключ. | +| `the signing certificate does not verify the signature the key produced` | KMS подписал другим ключом или другой версией ключа, чем описывает сертификат. Проверьте имя ключа и сертификат. | +| `--sign-build-file-vault-auth-role and --sign-build-file-vault-auth-path qualify --sign-build-file-vault-auth-jwt: ...` | Роль или mount заданы без JWT. Передайте и JWT или уберите их. | +| `permission denied` от Vault на `/keys/` или `/sign//sha2-256` | В политике роли нет `read` на ключ или `update` на подписание. См. раздел «Что понадобится». | +| `specify only one stage using ...`, `duplicated ... for sign!`, `conflicting ... for sign: ...` | Список `sign` нарушает одно из правил шага 1. | + +## Ограничения + +- Только Stapel-образы; позиции — границы `install` и `setup` и конец образа. +- Только RSA-ключи, SHA-256 и отсоединённая подпись PKCS#7 в отдельном `.p7s`. Подпись, встроенная в сам файл, не создаётся. +- Один подписант на сборку для всех записей `sign`. +- Встраивается только сертификат подписанта; `--sign-build-file-intermediates` не участвует ни в подписи, ни в дайджесте. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index ff563a444e..3a1c2c31a9 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -97,25 +97,28 @@ werf создаёт только родительский каталог для ## Подписание файлов внутри образа -Директива `sign` подписывает файл, который производит образ, и кладёт подпись рядом с ним, не давая сборочной стадии доступ к ключу подписи. +Директива `sign` подписывает файл, который производит образ, и кладёт подпись рядом с ним, не давая сборочной стадии доступ к ключу подписи. Пошаговая настройка, включая CI и деплой, — в разделе [Подписание файлов внутри образа]({{ "/usage/build/signing_files.html" | true_relative_url }}). Если не указаны ни `before`, ни `after`, файл подписывается в конце образа, когда его файловая система уже собрана, — обычно именно это и означает «подписанный артефакт образа», и именно это получает другой образ, импортирующий результат. Указание стадии переносит подпись раньше: тогда директива занимает в цепочке стадий то же место, что и `import`, файл должен существовать после стадии из `before` или `after`, а подпись доступна последующим стадиям. Указывать обе — ошибка конфигурации. ```yaml -image: sysext -from: registry.example.com/base/ubuntu:22.04 +image: policies +from: alpine:3.20 +git: +- add: /policies + to: /src/policies shell: install: - - mkfs.erofs /out/ext.raw /src - - veritysetup format --root-hash-file=/out/ext.roothash /out/ext.raw /out/ext.verity + - mkdir -p /out + - tar -C /src/policies -czf /out/policies.tar.gz . sign: -- path: /out/ext.roothash +- path: /out/policies.tar.gz after: install ``` Записи с одинаковой позицией подписания не могут использовать `.p7s` другой записи как входной файл. Например, `path: /out/x` и `path: /out/x.p7s` на одной позиции — ошибка конфигурации независимо от их порядка в списке. Чтобы подписать созданную подпись, задайте второй записи более позднюю позицию подписания. -werf читает файл из собранного к этому моменту образа, подписывает его на сборочном хосте и добавляет `.p7s` новым слоем. Подпись — отсоединённый PKCS#7 (CMS) `SignedData` над содержимым файла: SHA-256, RSASSA-PKCS1-v1_5, без аутентифицированных атрибутов, со встроенным сертификатом подписанта — ровно та форма, которую производит `systemd-keyutil pkcs7`, поэтому systemd и ядро Linux принимают её как подпись dm-verity root hash. Ключ, соответственно, должен быть RSA. +werf читает файл из собранного к этому моменту образа, подписывает его на сборочном хосте и добавляет `.p7s` новым слоем. Подпись — отсоединённый PKCS#7 (CMS) `SignedData` над содержимым файла: SHA-256, RSASSA-PKCS1-v1_5, без аутентифицированных атрибутов, со встроенным сертификатом подписанта — ровно та форма, которую производит `systemd-keyutil pkcs7`; её проверяет OpenSSL или любая другая реализация CMS. Ключ, соответственно, должен быть RSA. Ключ и сертификат никогда не являются частью `werf.yaml`; они передаются только через командную строку, отдельно от ключа подписи манифеста и SBOM: @@ -132,7 +135,7 @@ werf читает файл из собранного к этому момент Опции `--sign-build-file-vault-*` нужны, чтобы подписант файлов и подписант `--sign-key` могли жить в разных mount Vault и входить под разными ролями в рамках одного `werf build`. -Адрес и transit-mount подхватываются по отдельности: незаданный берёт то, что использовал бы `--sign-key` (`VAULT_ADDR`, `TRANSIT_SECRET_ENGINE_PATH`). С аутентификацией так не работает — она берётся целиком. Если задан JWT, подписант файлов входит именно с этим JWT, ролью из `--sign-build-file-vault-auth-role` (пустой, если не задана) и mount из `--sign-build-file-vault-auth-path` (`jwt`, если не задан); ни одно из трёх не подхватывается из окружения. Без JWT вся аутентификация берётся из окружения, а роль или mount сами по себе отвергаются. Аутентификация проверяется, как только werf разрешает объявленную директиву `sign`, — в том числе командой, у которой есть только сертификат и которая вообще не входит в Vault. +Адрес и transit-mount подхватываются по отдельности: незаданный берёт то, что использовал бы `--sign-key` (`VAULT_ADDR`, `TRANSIT_SECRET_ENGINE_PATH`). С аутентификацией так не работает — она берётся целиком. Если задан JWT, подписант файлов входит именно с этим JWT, ролью из `--sign-build-file-vault-auth-role` (пустой, если не задана) и mount из `--sign-build-file-vault-auth-path` (`jwt`, если не задан); ни одно из трёх не подхватывается из аутентификации `--sign-key`. Без JWT вся аутентификация берётся из окружения, а роль или mount сами по себе отвергаются. Аутентификация проверяется, как только werf разрешает объявленную директиву `sign`, — в том числе командой, у которой есть только сертификат и которая вообще не входит в Vault. Собрать такой образ без подписей нельзя: объявленная директива `sign` — требование образа, а не опция сборки.