Skip to content

feat(build, stapel): sign files inside the image with a key the build container never sees - #438

Draft
reyreavman wants to merge 11 commits into
mainfrom
feat/sbom/workflow-for-stronghold
Draft

reyreavman wants to merge 11 commits into
mainfrom
feat/sbom/workflow-for-stronghold

Conversation

@reyreavman

@reyreavman reyreavman commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

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: true turns 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

  • A stapel image accepts a sign: list; each entry has path (absolute) and exactly one of before / after ∈ {install, setup}, mirroring import:.
  • A relative path, /, a duplicate path, both or neither of before/after, or a stage other than install/setup is a config error naming the image.
  • schemas/werf.json describes the directive; documented in docs/pages_*/usage/build/stapel/imports.md and the werf.yaml reference.

Build

  • A signFiles{BeforeInstall,AfterInstall,BeforeSetup,AfterSetup} stage follows the matching dependencies* stage; it reads path out of the previous stage image, signs on the host and adds <path>.p7s as a new layer. The following stages see the .p7s.
  • The signature is a detached CMS SignedData, SHA-256, RSASSA-PKCS1-v1_5, no authenticated attributes, signer certificate embedded — the output of systemd-keyutil pkcs7, accepted by systemd (/etc/verity.d) and the kernel (root_hash_sig_key_desc). A non-RSA key fails the stage.
  • The stage container always runs with network: none, SBOM enabled or not, and none of the image's commands run in it; with build.sbom.enable: true shell stages stay offline and the .p7s still lands. VERIFIED: e2e under both Docker and Buildah native-rootless with a shell stage that fails if ping 1.1.1.1 reaches the network and fails differently if ping is missing; the probe was checked against the fixture's base image with and without network.
  • The stage digest covers the signed paths, the position and the signer certificate: same key → cached, rotated key → the stage and everything after it rebuilds. --sign-files-intermediates is accepted for symmetry with --sign-intermediates but takes no part in the signature or the digest; only the leaf is embedded.
  • A missing path in the previous stage fails the build with read <path> to sign from image "<name>": … file does not exist.
  • The signature file is not an SBOM component: stapel SBOMs are derived from from/import/packages, not from a filesystem scan, so SBOM content is unchanged.

CLI

  • --sign-files (WERF_SIGN_FILES, default true where registered), --sign-files-key (WERF_SIGN_FILES_KEY: PEM path, base64 PEM or hashivault://[KEY]), --sign-files-cert, --sign-files-intermediates on every image-building command that has --sign-key (build, converge, export, compose *, bundle publish, kube-run, run, plan, render); attest sign is excluded.
  • This is a separate signer from --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 a hashivault:// files key at its own Vault address, transit mount and JWT login, so the files signer and the --sign-key signer can use different mounts and roles in one build. Each option left unset falls back to the environment the --sign-key signer 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.
  • The certificate is not fetched from Vault PKI: the pipeline retrieves the leaf PEM itself and passes it as a path or base64.
  • An image with sign: built with --sign-files on 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.
  • Under Buildah, path is resolved inside the image root: a symlink in the image never reads a host file, and anything but a regular file fails with not a regular file. Docker already behaved this way.
  • --sign-files=false builds the image with no signFiles* stage and no .p7s.
  • Commands that do not register the flag (lint, sbom get, stages copy, helm get-autogenerated-values) treat it as off and accept a werf.yaml with sign:; stages copy therefore copies a stage chain that contains signFiles* as is.
  • Keys and certificates are never read from werf.yaml.

Why

With build.sbom.enable: true werf removes network from every shell stage so that nothing enters the image except what from, import and packages declare — 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-key already does.

@reyreavman

reyreavman commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verification

  • task test:e2e paths=./test/e2e/build labelFilter=sign-files parallel=1 on a Linux host with Docker and Buildah: 4/4 specs — Docker (legacy builder, volume + install path), Buildah native-rootless (AddDataArchive path), SBOM-enabled offline build, and the no-key gate / --sign-files=false pair.
  • Hand-run on macOS/Docker: openssl smime -verify -inform der -in x.roothash.p7s -content x.roothash -noverify succeeds on the extracted files; a tampered content file fails with PKCS7_verify:signature failure; openssl asn1parse shows sha256, rsaEncryption, no messageDigest/signingTime attributes, one embedded certificate.
  • Mutation: sd.Detach() removed in files_signing.go → e2e expectSystemdProfile fails on p7.Content being non-empty; unit SignDetachedPKCS7 "detached" assertion fails.
  • Mutation: digest OID swapped to SHA-1 → unit OpenSSL oracle fails.
  • Mutation: cert/chain dropped from SignFilesStage.digest() → TestSignFilesStage_DigestChangesWithPathsAndCertificate fails.
  • Mutation: path.IsAbs inverted in Sign.validate() → config "sign relative path" entry fails.
  • Probe: the SBOM fixture's ping -c1 -W2 1.1.1.1 probe was run in the fixture's base image (container-factory@sha256:7aac…) with --network bridge → network is reachable, exit 1; with --network none → passes. wget, the previous probe, is absent from that image (command -v → 127), so the earlier probe never ran.
  • Mutation: SetNetworkOverride("none") removed from generateSignFilesStage → runs the sign stages without network / build.sbom absent fails (the sbom entry still passes through the shell-stage loop, as expected).
  • Mutation: the AuthJWT → VaultAuth mapping disabled in VaultOptions.sdkOpts → NewSigner with hashivault / logs in and reads the key where the Vault options say fails on the login path.
  • Not run: task test:unit end to end on the development host — the pkg/true_git, pkg/git_repo and pkg/build/image LFS specs need git-lfs, which that host lacks; they fail identically on main. Everything else passed locally; CI runs the full suite.

Review focus

  • pkg/build/stage/sign_files.go: the legacy-builder branch mounts the host signature dir read-only into the stage container and copies with the stapel install binary — same shape as git_archive.go; confirm nothing else from the host is exposed.
  • pkg/build/signing/signer.go VaultOptions.sdkOpts: an unset JWT keeps the SDK's environment authentication even when --sign-files-vault-auth-role/-path are set; those two only qualify the JWT. Decide whether that should be an error instead.
  • hashivault:// against a real Vault is still not exercised; the fake in signer_vault_test.go implements only /v1/auth/<mount>/login and /v1/<transit>/keys/<name>.
  • cmd/werf/common/signature.go GetSignFiles: unregistered flag → false, registered flag → default true. Both are intended; check the gate in conveyor_options.go reads well with that.
  • Buildah ReadFileFromImage follows symlinks (os.ReadFile on the root mount), Docker's reader rejects non-regular files — a symlinked sign.path behaves differently per backend. Pre-existing helper; left as is.

Follow-up

  • deckhouse/deckhouse: migrate .werf/defines/engine-sysext.tmpl from the in-container d8 stronghold … transit sign script to sign: + WERF_SIGN_FILES_KEY=hashivault://dh-engine-2026-sep, WERF_SIGN_FILES_VAULT_TRANSIT_PATH=dh-engine-signer, WERF_SIGN_FILES_VAULT_AUTH_PATH=fox, WERF_SIGN_FILES_VAULT_AUTH_ROLE=$WERF_OLCEDAR_VAULT_AUTH_ROLE, WERF_SIGN_FILES_VAULT_AUTH_JWT=<job id_token>; the CI job reads dh-engine-root-pki/cert/<serial> itself into WERF_SIGN_FILES_CERT. Then drop VAULT_*/WERF_VAULT_AUTH_JWT from secrets.allowEnvVariables in werf-giterminism.yaml once no image mounts them.

@reyreavman

Copy link
Copy Markdown
Collaborator Author

Code Review Report

Base: origin/main (merge-base 6c93b138b5; main has since moved to 236b3b8963, merge conflicts not checked)
Diff: 51 files, +1494/−3 (11 of them generated CLI reference pages)
Head: 6a9ac72025, built and run locally (task build, scoped task test:unit, task test:e2e labelFilter="sign-files" on Docker backend, mutation loop on the three load-bearing tests)

Verdict

  • Technical: Compiles, scoped unit tests are green, and all three mutations (drop Cert from the stage digest, drop sd.Detach(), drop the duplicate-path check) are killed by their tests. The main gap: the signFiles* stage is neither a shell stage nor marked NeedsNetwork, so under build.sbom.enable it is skipped by the SetNetworkOverride("none") loop and runs with network: default (visible in the e2e log). In the non-legacy path the stage container executes nothing, so there is no practical exposure, but the "runs no network" claim in the description is not what the code does.
  • Product: For the engine-sysext.tmpl case the PR moves the roothash signature out of the shell stage — but it does not close the case end to end. hashivault:// in the SDK reads VAULT_ADDR, WERF_VAULT_AUTH_JWT, WERF_VAULT_AUTH_ROLE, WERF_VAULT_AUTH_PATH, TRANSIT_SECRET_ENGINE_PATH from the host environment — the same variables --sign-key already consumes for SBOM/VEX signing. One werf process therefore has one Vault session and one transit path for both signers; "the two must rotate independently" (commit 3) holds for Vault keys only as far as the hashivault://KEY name differs. In Deckhouse the engine signer logs in through auth/fox/login with its own transit mount (dh-engine-signer), while vex.tmpl uses VAULT_AUTH_PATH=github and its own TRANSIT_SECRET_ENGINE_PATH — these cannot coexist in one process today.
  • Risk: The sbom e2e scenario (state_sbom) fails locally and cannot pass without access to the base image's SBOM in registry.deckhouse.io; worse, its "no network" probe does not probe anything — the base image has no wget, so if wget …; then exit 1; fi is always a no-op. The "VERIFIED: e2e … with a shell stage that fails if wget reaches the network" claim is not backed by the test.

DoD Criteria

Criteria Inferred? Met? Evidence
sign: is parsed and validated like import: yes ✅ pkg/config/sign.go:26-43, raw_stapel_image.go:199-211; duplicate-path mutation killed
.p7s is detached CMS, SHA-256, PKCS#1 v1.5, no authenticated attrs, leaf embedded yes ✅ files_signing.go:53-75; unit test + openssl smime -verify; Detach() mutation killed
Key never enters the stage container yes ✅ non-legacy / ⚠️ legacy sign_files.go:138-142 adds the signature as a tar layer; legacy path :146 mounts only the finished .p7s
Stage works offline under build.sbom.enable yes ⚠️ Not a shell stage → SetNetworkOverride not applied (stapel.go:164-174); e2e log: signFilesAfterInstall … network: default
Digest covers paths + certificate chain yes ✅ sign_files.go:89-96; Cert mutation killed by TestSignFilesStage_DigestChangesWithPathsAndCertificate
Missing file in previous image → clear error yes ✅ sign_files.go:111-114 + docker_image_reader.go:86-88 (fs.ErrNotExist)
No key → fail before any stage; --sign-files=false → build without .p7s yes ✅ signature.go:189-209; "refuses to build…" e2e passed locally
Solves the engine-sysext.tmpl case under sbom.enable no (explicit in the request) ⚠️ See Major 1, 2
e2e covers offline + signing yes ❌ See Critical 1

Issues

Critical

  1. test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml:14 — the "network is unreachable" check checks nothing. The base image container-factory@sha256:7aac… has no wget (sh: wget: not found; only busybox, bash, ping are present). With the binary missing the condition exits 127, the then branch never runs, and the stage always passes. In addition, under build.sbom.enable the build needs the base image's SBOM from registry.deckhouse.io (locally: must have an SBOM artifact attached … artifact not found), so the test is not reproducible outside infrastructure with referrers access. Recommendation: call busybox wget explicitly or use ping -c1 -W1 1.1.1.1, and fail with a distinct message when the probe tool is absent; alternatively, pin the offline property in a unit test that asserts the network override is actually applied to signFiles* (the existing stapel_test.go:173-184 only checks NeedsNetwork()==false, not that the override landed).

Major

  1. cmd/werf/common/signature.go:189-209 — Vault authentication is not separated between --sign-key and --sign-files-key. signing.NewSigner → signver.NewSignerVerifier with KeyOpts{KeyRef, PassFunc} and no SignerVerifierOpts.VaultOpts, so the SDK resolves VAULT_ADDR, WERF_VAULT_AUTH_JWT/ROLE/PATH, TRANSIT_SECRET_ENGINE_PATH from env (hashivault/auth_factory.go:14-45, config_vault.go). One werf process cannot hold two WERF_VAULT_AUTH_PATH / TRANSIT_SECRET_ENGINE_PATH values. Either (a) document that both signers share one Vault session and transit mount and that this is a limitation for the engine-sysext case, or (b) accept --sign-files-vault-* (or WERF_SIGN_FILES_VAULT_AUTH_ROLE/PATH/TRANSIT_PATH) and pass them through VaultOpts — the SDK already supports this (key_options.go, README "Programmatic configuration"). As written, the independent-rotation claim in commit 3 is false for Vault-backed keys.

  2. The PR does not cover SIGNER_CERT_PATH from engine-sysext.tmpl. Deckhouse pulls the leaf certificate from Vault PKI (dh-engine-root-pki/cert/<serial>), whereas --sign-files-cert accepts only a PEM path or base64. Deckhouse CI will have to fetch the cert itself and inject it via env — doable, but "the consumer needs no further tooling" overstates it. Mention in the description/docs.

  3. pkg/build/image/stapel.go:164-174 + sign_files.go — signFiles* runs with network: default under build.sbom.enable. The stage is not covered by the SetNetworkOverride loop and does not set NeedsNetwork. In the non-legacy path nothing runs in the container, so no leak; in the legacy path (c.UseLegacyStapelBuilder(cb), sign_files.go:125-135, 145-148) stapel-toolchain mkdir/install commands do run inside a container with network. Simplest fix: call SetNetworkOverride("none") unconditionally in generateSignFilesStage — the stage never needs network, not only under sbom.

  4. cmd/werf/common/signature.go:49-56 / GetSignFiles — commands without the flag (lint, stages copy, sbom get) treat it as false, so stages copy happily copies an image whose stage chain contains signFiles*, and a werf build elsewhere without a key then fails. Expected, but worth a line in the flag table in imports.md:116-121.

Minor

  1. pkg/build/stage/sign_files_test.go uses testing+testify in a package with a Ginkgo suite (stage_suite_test.go). AGENTS.md: "ALWAYS use Ginkgo and Gomega when writing new tests" — the package is transitional, but new tests should be Ginkgo DescribeTable.
  2. pkg/build/signing/files_signing_test.go:27-30 adds a RunSpecs to package signing; confirm it is the only RunSpecs in the package (two in one package panic in Ginkgo).
  3. pkg/build/stage/sign_files.go:117-122 — err shadowing inside LogProcess(...).DoError: the outer err from :111 is assigned in the closure while if err := … on :117 declares a new one. Works, reads poorly; use signErr.
  4. schemas/werf.json:1527-1540 — pattern: "^/" does not reject / or //x; the Go validator does, the schema is looser. Acceptable, noting it.
  5. docs/_data/werf_yaml.yml:950-958 — the ru descriptions of before/after lack the "only install and setup stages are supported" sentence present in en.
  6. go.mod — smallstep/pkcs7 promoted from indirect to a direct dependency (flagged in commit 2, recording for the protocol).
  7. PR description lists the commands that get --sign-files and omits render; the code registers it in cmd/werf/render/render.go. Sync the list.

Risks

№ Risk Type Likelihood Severity Location Circumstances Consequences Recommendation
1 The sbom e2e scenario does not detect network access Technical Likely High state_sbom/werf.yaml:14 wget absent from the base image A regression of "no network in shell stages under sbom.enable" passes CI unnoticed Use busybox wget/ping, fail when the probe tool is missing
2 Deckhouse CI cannot configure two different Vault auth paths / transit mounts for --sign-key and --sign-files-key UX/Product Likely High signature.go:198-202 SBOM signer and engine signer live in different transit mounts / auth paths Adoption in engine-sysext.tmpl blocked or forces a shared Vault path Pass VaultOpts through dedicated WERF_SIGN_FILES_VAULT_*, or document the limitation
3 Legacy stapel builder runs commands in a networked container Security Unlikely Medium sign_files.go:125-148 UseLegacyStapelBuilder = true, sbom.enable Stage container has network although it holds no key Unconditional SetNetworkOverride("none") in generateSignFilesStage
4 main moved ahead (6c93b1 → 236b3b) Operational Possible Low PR base merge Conflicts in stapel.go / signature.go Rebase before merge

Not verified

  • Buildah / native-rootless e2e entries — do not compile or run on macOS; locally they failed on needs-buildah as expected. In the PR's CI every e2e job is fail, but the cause is suite-wide [TIMEDOUT] DeferCleanup … docker images … signal: killed — a runner/infrastructure timeout, not this PR; the sign directive specs also hit TIMEDOUT in cleanup, with no substantive [FAIL] on them.
  • hashivault:// path not exercised — no Vault available; Major 1 is derived from SDK v1.4.2 sources (key_loader.go:44-45, auth_factory.go, config_vault.go).
  • task lint not run locally (remote taskfile); CI lint is green.
  • Full task test:unit not run; scoped runs of pkg/config, pkg/build/stage, pkg/build/signing, pkg/build/image are green except 2 pre-existing LFS failures (git lfs missing on the host, unrelated).
  • The original case (engine-sysext.tmpl → pm install systemd@260.4, veritysetup, mkfs.erofs) was not built — no access to fox/Vault.

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>
@reyreavman
reyreavman force-pushed the feat/sbom/workflow-for-stronghold branch from 6a9ac72 to 80735a9 Compare October 16, 2026 06:57
@reyreavman

Copy link
Copy Markdown
Collaborator Author

Thanks — all items addressed in 6cd727df4d…80735a9511 (plus fix(buildah) from a second independent review that landed in the same push).

Critical 1 — confirmed: wget is absent from container-factory@sha256:7aac…, so the probe never ran. The fixture now requires ping (command -v ping || exit 1 with a distinct message) and probes ping -c1 -W2 1.1.1.1. Checked in that image directly: --network bridge → network is reachable, exit 1; --network none → passes. The offline property is additionally pinned in pkg/build/image/stapel_test.go, which asserts the stage's resolved network mode (Network() == "none") with and without build.sbom.

Major 1 — added --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_*), passed through signver.KeyOpts.SignerVerifierOpts.VaultOpts. Every option left unset falls back to the environment the --sign-key signer reads, so nothing changes for existing setups. Covered by a fake-Vault unit test (signer_vault_test.go) asserting the login mount, role, JWT and transit path actually used. The independent-rotation claim now holds for the engine-sysext layout (auth/fox + dh-engine-signer vs. the SBOM signer's mount).

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 — generateSignFilesStage calls SetNetworkOverride("none") unconditionally; the legacy-builder mkdir/install now runs in an offline container on every build, SBOM or not.

Major 4 — documented in imports.md (EN/RU) next to the flag table.

Minor 1 — stage test rewritten in Ginkgo. Minor 2 — one RunSpecs in pkg/build/signing. Minor 3 — signErr. Minor 4 — noted, left as is (Go validator is authoritative). Minor 5 — RU descriptions aligned. Minor 6 — acknowledged. Minor 7 — render added to the list. Risk 4 — rebased on origin/main.

Also from the second review: the Buildah reader resolved path with os.ReadFile(root+path), so an absolute symlink in the image read a host file once path became user-controlled; it now resolves against the root mount (as the write side already did) and accepts regular files only. --sign-files-intermediates dropped from the stage digest since only the leaf is embedded.

Verification on a Linux host (Docker + Buildah): task format, task lint (0 issues), task test:unit (only the pre-existing automatic LFS build context … warming the cache spec fails, identically on origin/main), task test:e2e … labelFilter=sign-files 4/4 including native-rootless.

@reyreavman

Copy link
Copy Markdown
Collaborator Author

Code Review Report — iteration 2

Base: origin/main (merge-base ba4cc59c06, PR rebased)
Head: 80735a9511 (+3 commits since the previous pass: e1df4b34fb, 6cd727df4d, 80735a9511; 56 files)
Delta since the previous review: 27 files, +582/−101

Every claim in the author's reply was re-derived from the code and by running it, not taken from the comment text.

Verdict

  • Technical: All three substantive fixes are confirmed by the code and by mutation: SetNetworkOverride("none") is unconditional in generateSignFilesStage (sign_files.go:79), VaultOptions reach the SDK through SignerVerifierOpts.VaultOpts (signer.go:37-48,105-107), and the Buildah reader resolves the path with SecureJoin and accepts regular files only (buildah_image_reader.go:43-56). The new tests fail under the matching mutations (details below). No remaining code findings.
  • Product: The engine-sysext.tmpl case is now covered end to end: WERF_SIGN_FILES_VAULT_AUTH_PATH=fox + WERF_SIGN_FILES_VAULT_TRANSIT_PATH=dh-engine-signer coexist with the SBOM signer in one process. EN/RU docs and the PR body agree with the code (render listed, PKI certificate caveat present, stages copy behaviour described, intermediates honestly declared as not participating).
  • Risk: One residual: the state_sbom e2e still cannot pass outside infrastructure with referrers access to registry.deckhouse.io — but that is a property of the whole sbom e2e set (every test/e2e/sbom/_fixtures/* uses the same base), not of this PR. The probe itself works now — reproduced in the image: --network bridge → network is reachable, --network none → pass.

Verified claims

Claim (author) Check Result
The ping probe actually detects network docker run --network bridge/none in container-factory@7aac… with the fixture's exact commands ✅ bridge → exit 1, none → pass; ping is a busybox symlink
Network()=="none" for signFiles* with and without sbom stapel_test.go DescribeTable; mutation: remove SetNetworkOverride ✅ fails runs the sign stages without network [It] build.sbom absent
Vault options reach the SDK (mount/role/jwt/transit) signer_vault_test.go against the fake Vault; mutation: drop Role from JWTAuth ✅ fails logs in and reads the key where the Vault options say
Empty options → env fallback falls back to the environment for every option left empty + SDK config_vault.go ✅
Buildah reader no longer reads the host through an absolute symlink mutation: SecureJoin → filepath.Join ✅ both tests fail (symlink + non-regular)
Intermediates removed from the digest sign_files.go:96 — Cert only ✅; docs EN/RU:136 and PR body:18 agree
--sign-files-vault-* on every command that has --sign-files 11 SetupFilesSigningOptions registrations, all 5 flags in one place (signature.go:70-88); task doc:gen reflected in 11 pages ✅
Minor 1/3/5/7 Ginkgo in sign_files_test.go; signErr; RU before/after descriptions; render in the PR body ✅
task build, scoped task test:unit locally ✅ exit 0; signing 6/6, stage 169/169, container_backend 181/191 (S), image 126/126 except the 2 pre-existing LFS specs (git-lfs missing on this host)
e2e sign-files && docker locally against localhost:5001 2/3 ✅; state_sbom ❌ only on must have an SBOM artifact attached for the base image (no referrers access), unrelated to the PR

Issues

Minor

  1. pkg/build/build_phase.go:1326-1334 — the stage log misreports the network mode. getLogImageNetwork reads ImageBuildOptions.Network / ImageBaseConfig().Network, not stage.Network(), so the output shows signFilesAfterInstall … network: default (and install … network: default under sbom) while the container actually runs with none. This is exactly what misled me in the first pass. Pre-existing for shell stages, but BaseStage.Network() now exists (base.go:140-145) — wiring it into getLogImageNetwork is cheap and removes the contradiction between the log and the "always runs with network: none" statement in the docs. Not blocking; fine as a follow-up PR.
  2. signer.go:37-48 — AuthRole/AuthPath without AuthJWT are silently ignored. The author raised this in "Review focus". My take: it should be an error in getFilesSigningOptions (signature.go:231-240). --sign-files-vault-auth-role without --sign-files-vault-auth-jwt is almost certainly a CI-variable typo, and it will fail not here but at auth/<env-path>/login with the env role — the diagnosis goes the wrong way. One check, if (role != "" || path != "") && jwt == "", with an explicit message.
  3. sign_files.go:96 — the digest excludes Chain, yet --sign-files-intermediates still validates the chain in NewSignerVerifier (SDK signerverifier.go:73-80). Behaviour: broken intermediates = error before any stage, valid intermediates = no effect. Documented correctly; recording that the flag is in practice a pre-flight chain check only.

Risks

№ Risk Type Likelihood Severity Location Recommendation
1 state_sbom e2e depends on SBOM referrers of the base image in registry.deckhouse.io Operational Likely Low state_sbom/werf.yaml:11 Shared by every sbom e2e; nothing to do in this PR
2 --sign-files-vault-auth-role/-path without -jwt fall through to env authentication with no warning UX/Product Possible Low signature.go:231-240 Validate the combination (Minor 2)

Not verified

  • Buildah / native-rootless e2e — macOS; the author reports 4/4 on Linux, CI unit is still pending and the e2e jobs have not started on the new head yet.
  • hashivault:// against a real Vault — fake only (/v1/auth/<mount>/login, /v1/<transit>/keys/<name>); the sign path (/v1/<transit>/sign/<key>/sha2-256, prehashed, pkcs1v15) is not covered by the fake — but it is unchanged in the SDK and identical to what --sign-key uses.
  • task lint not run locally; CI lint is green on the new head.

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>
@reyreavman

Copy link
Copy Markdown
Collaborator Author

Iteration 2 minors, in 8f358517b6:

Minor 1 — getLogImageNetwork now takes the stage and reports stage.Network(); --network still wins, as it does for the build. Checked on a real build: signFilesAfterInstall … network: none, ordinary stages network: default.

Minor 2 — --sign-files-vault-auth-role/-path without -auth-jwt is now an error in getFilesSigningOptions, with a message naming the fix. Covered by cmd/werf/common/files_signing_test.go (also covers the no-key / no-cert gate and the three disabled paths); mutation of the check fails both entries.

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: task format ✓, task lint 0 issues, task test:unit — only the pre-existing LFS spec, task test:e2e … labelFilter=sign-files 4/4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant