Repository navigation
feat(build, stapel): sign files inside the image with a key the build container never sees - #438
reyreavman wants to merge 11 commits into
Conversation
Verification
Review focus
Follow-up
|
Code Review ReportBase: Verdict
DoD Criteria
IssuesCritical
Major
Minor
Risks
Not verified
|
A `sign` entry names a file of the image whose detached PKCS#7 signature werf is to write next to it, and the user stage it follows or precedes. The entries are validated like imports: an absolute clean path, exactly one of before/after naming install or setup, no two entries for the same path. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
SignDetachedPKCS7 wraps an RSASSA-PKCS1-v1_5/SHA-256 signature of the content into a CMS SignedData with the leaf certificate embedded and no authenticated attributes — the profile systemd-keyutil writes and the kernel dm-verity signature check reads. The signature comes from the configured signer, so a Vault Transit key never leaves Vault, and it is checked against the leaf certificate before it is returned. smallstep/pkcs7 was already in the module graph through buildah; it is now a direct dependency. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
The files signer has its own key, certificate and intermediates: the trust domain of a file signature — a dm-verity root hash a node checks against its own certificate store — is not that of the SBOM attestation or the manifest, and the two must rotate independently. When an image declares files to sign and no key is given the build stops before any stage runs and names the images; --sign-files=false builds them unsigned on purpose. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
A signFiles stage follows the user stage the directive names. It reads the file out of the previous built image, signs it on the host with the files signer and lays the detached PKCS#7 down next to it as <path>.p7s in a new layer. The stage container runs nothing of its own and never needs network, so a build with SBOM enabled keeps every shell stage offline while a KMS-backed signature still lands in the image. The stage digest covers the paths, the position and the signer's certificate and chain, so rotating the key rebuilds from the stage on. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
The e2e suite builds an image with a sign directive, verifies the .p7s carries the systemd-keyutil profile, checks the stage is reused on a rebuild with the same key and that a build with SBOM enabled still signs while the shell stages stay offline. Dedicated RSA fixtures are added because the inhouse e2e keys are ECDSA, which the producer rejects. The directive is documented in the werf.yaml reference and next to the import directive in both languages, with a pointer from the SBOM page. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
…ag alone The sign directive accepted before: install but no stage was generated for it, so the file silently stayed unsigned while the key was still demanded. A signFilesBeforeInstall stage now mirrors the dependencies one. Commands that never register --sign-files (lint, sbom get, stages copy, helm get-autogenerated-values) treated the unset flag as true and refused any werf.yaml with a sign directive; the unset flag is false now. The e2e suite also runs the native Buildah path and asserts the cached rebuild positively; a unit test pins the stage digest to paths and certificate. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
… host The Buildah reader joined the container path onto the root mount and read it, so an absolute symlink in the image pointed at the host filesystem. The sign directive made that path user-controlled. The reader now resolves symlinks against the root mount as the write side already does and accepts regular files only, matching Docker. The stage digest no longer includes the intermediates, which never take part in the signature; the Vault signing error is wrapped; the stage unit test uses Ginkgo like the rest of the suite. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
The signFiles stages were neither shell stages nor marked as needing network, so the SBOM network override skipped them and the legacy builder ran the signature copy in a networked container. The stage now sets network none itself, SBOM or not, and the unit test asserts the resolved mode rather than the NeedsNetwork hint. The SBOM e2e fixture probed network with wget, which the base image does not ship, so the probe never ran; it uses ping and fails when the probe tool is missing. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
…ogin A hashivault:// files key resolved the Vault address, transit mount and JWT login from the same environment the --sign-key signer reads, so one werf process could hold only one Vault identity. The --sign-files-vault-addr, -transit-path, -auth-jwt, -auth-role and -auth-path options give the files signer its own; every option left unset still falls back to the environment, so existing setups are unchanged. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
6a9ac72 to
80735a9
Compare
|
Thanks — all items addressed in Critical 1 — confirmed: Major 1 — added Major 2 — docs and description now say the certificate is not fetched from Vault PKI; the pipeline passes the PEM as path/base64. Major 3 — Major 4 — documented in Minor 1 — stage test rewritten in Ginkgo. Minor 2 — one Also from the second review: the Buildah reader resolved Verification on a Linux host (Docker + Buildah): |
Code Review Report — iteration 2Base: Every claim in the author's reply was re-derived from the code and by running it, not taken from the comment text. Verdict
Verified claims
IssuesMinor
Risks
Not verified
Bottom line: no blockers. Every Critical/Major from the first iteration is closed and confirmed by mutation. Suggest leaving draft once e2e is green in CI; Minor 1–2 are at the author's discretion and can be follow-ups. |
The stage log printed the image-level network for every stage, so a sign stage and a shell stage under SBOM both read network: default while running with none. The log now takes the stage's resolved mode, with the --network option still winning as it does for the build. --sign-files-vault-auth-role or -path without -auth-jwt is rejected up front instead of silently falling back to the environment login and failing at the wrong mount later. Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
|
Iteration 2 minors, in Minor 1 — Minor 2 — Minor 3 — agreed; the docs already state intermediates take no part in the signature or digest. Left as a pre-flight chain check. Linux host after the push: |
Summary
A stapel image can now declare files that werf signs after a user stage and lays the detached PKCS#7 signature down next to them inside the image, with the key never entering a build container. This is what a dm-verity sysext build needs when
build.sbom.enable: trueturns off network in every shell stage: the roothash still gets signed by Vault Transit, but from the build host, not from a container that would need network and the Vault credentials.What
werf.yaml
sign:list; each entry haspath(absolute) and exactly one ofbefore/after∈ {install,setup}, mirroringimport:.path,/, a duplicatepath, both or neither ofbefore/after, or a stage other thaninstall/setupis a config error naming the image.schemas/werf.jsondescribes the directive; documented indocs/pages_*/usage/build/stapel/imports.mdand the werf.yaml reference.Build
signFiles{BeforeInstall,AfterInstall,BeforeSetup,AfterSetup}stage follows the matchingdependencies*stage; it readspathout of the previous stage image, signs on the host and adds<path>.p7sas a new layer. The following stages see the.p7s.SignedData, SHA-256, RSASSA-PKCS1-v1_5, no authenticated attributes, signer certificate embedded — the output ofsystemd-keyutil pkcs7, accepted by systemd (/etc/verity.d) and the kernel (root_hash_sig_key_desc). A non-RSA key fails the stage.network: none, SBOM enabled or not, and none of the image's commands run in it; withbuild.sbom.enable: trueshell stages stay offline and the.p7sstill lands. VERIFIED: e2e under both Docker and Buildah native-rootless with a shell stage that fails ifping 1.1.1.1reaches the network and fails differently ifpingis missing; the probe was checked against the fixture's base image with and without network.--sign-files-intermediatesis accepted for symmetry with--sign-intermediatesbut takes no part in the signature or the digest; only the leaf is embedded.pathin the previous stage fails the build withread <path> to sign from image "<name>": … file does not exist.from/import/packages, not from a filesystem scan, so SBOM content is unchanged.CLI
--sign-files(WERF_SIGN_FILES, defaulttruewhere registered),--sign-files-key(WERF_SIGN_FILES_KEY: PEM path, base64 PEM orhashivault://[KEY]),--sign-files-cert,--sign-files-intermediateson every image-building command that has--sign-key(build,converge,export,compose *,bundle publish,kube-run,run,plan,render);attest signis excluded.--sign-key/--sign-cert; no fallback between them for the key or the certificate.--sign-files-vault-addr,--sign-files-vault-transit-path,--sign-files-vault-auth-jwt,--sign-files-vault-auth-role,--sign-files-vault-auth-path(WERF_SIGN_FILES_VAULT_*) point ahashivault://files key at its own Vault address, transit mount and JWT login, so the files signer and the--sign-keysigner can use different mounts and roles in one build. Each option left unset falls back to the environment the--sign-keysigner reads (VAULT_ADDR,TRANSIT_SECRET_ENGINE_PATH,WERF_VAULT_AUTH_*). VERIFIED: unit test against a fake Vault asserts the login path, role, JWT and transit path used.sign:built with--sign-fileson and no key fails before any stage runs:images <names> declare files to sign: specify the signing key with --sign-files-key or pass --sign-files=false to build them unsigned; with a key but no certificate:… specify the signing certificate with --sign-files-cert.pathis resolved inside the image root: a symlink in the image never reads a host file, and anything but a regular file fails withnot a regular file. Docker already behaved this way.--sign-files=falsebuilds the image with nosignFiles*stage and no.p7s.lint,sbom get,stages copy,helm get-autogenerated-values) treat it as off and accept a werf.yaml withsign:;stages copytherefore copies a stage chain that containssignFiles*as is.werf.yaml.Why
With
build.sbom.enable: truewerf removes network from every shell stage so that nothing enters the image except whatfrom,importandpackagesdeclare — that is the only enforcement behind the SBOM, since stapel SBOMs are not a filesystem scan. The Deckhouse sysext build signs an erofs roothash by calling Vault Transit from a shell stage, which both needs network and mounts the job's Vault JWT into an arbitrary build container. Neither can be reconciled with the SBOM guarantee: a per-stage "allow network" opt-in would make the SBOM honest only when the author remembers to not tick the box, and an egress allowlist still lets any payload through an allowed HTTPS host. Moving the signature to the host keeps the stage offline and the credentials out of the container, and the fixed systemd profile means the consumer needs no further tooling.Rejected: a key reference in
werf.yaml— the key identity would then live in git and be governed by giterminism; the project keeps all signing identity on the CLI, as--sign-keyalready does.