Repository navigation
feat(build, stapel): sign files matched by a glob pattern - #471
Conversation
`sign[].path` used to accept one absolute file path, so a set of files
known only after the build — the kernel modules `make modules_install`
produces, several hundred of them — could not be signed without
listing each one in werf.yaml. Accept an absolute glob pattern (`*`,
`?`, `[...]`, `{a,b}`, `**`) and expand it against the image filesystem
when the signing stage runs: every regular file it matches gets its
own `<file>.p7s`, symlinks are neither matched nor followed, and a
pattern matching nothing fails the build the way a missing literal
path does.
Expansion goes through the ImageReader of the container backend, which
gains a Glob method: Docker streams the pattern's literal base
directory out once with `docker cp`, Buildah walks it inside the
mounted container root with the base resolved against that root so a
symlink there cannot lead to a host path. The Buildah backend now also
opens a real reader — one container created and mounted for all the
files of a stage — instead of a container per file.
Duplicate-file and signature-of-the-same-stage conflicts, which the
config parser checks statically for literal paths, are checked for
patterns at expansion time against the actual file set. Under the
legacy stapel builder the install commands go through a script in the
mounted signatures directory, as the command line has a fixed size
limit a few hundred files would exceed.
Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
Verification
Review focus
Follow-up
|
reyreavman
left a comment
There was a problem hiding this comment.
Review of 9e525b2 against main. Built locally, ran the unit suites for pkg/build/stage, pkg/config, pkg/container_backend, and ran the sign-files e2e label on a Linux host with both backends: Ran 8 of 88 Specs — 8 Passed, including the glob spec on Native Rootless, which CI never reached (every Buildah job here dies on lstat /run/user/998 at backend init — infra, not this diff). Three mutations against the load-bearing tests (hard link to a non-regular entry, symlink in WalkDir, dropping the signature-of-signed check) were all caught.
Major
1. Literal paths containing [, { or ? silently change meaning — pkg/config/sign.go:28
IsPattern() is ContainsAny(Path, "*?[{"). Before this PR path: /out/file[1].bin signed exactly that file. Now it is a pattern, and doublestar.Match("/out/file[1].bin", "/out/file[1].bin") is false while /out/file1.bin matches. If that file does not exist the build fails with matches no file (at least visible); if it does, a different file is signed and the intended one ships unsigned. The PR description's "a path without them keeps its exact previous behaviour" is true but omits that a path with them does not. Either document in imports.md (EN/RU) and in the PR body that these are now metacharacters and a literal is written with \-escaping, or treat a pattern that matches itself as a literal. Documenting is simpler and honest about the grammar change.
Minor
2. Glob with base / on the Docker backend — docker_image_reader.go:163. For /**/*.ko the base is / and docker cp <ctr>:/ - yields entries named /bin/arch, so path.Join(path.Dir("/"), hdr.Name) happens to produce the right path — but the collector's comment ("rebased onto its parent") does not describe what happens there, and streaming the whole rootfs of a kernel image through docker cp is operationally bad. Consider rejecting a / base in validate(), or at least add a unit case with the names Docker actually emits for the root.
3. Buildah Glob resolves a symlink in the base through SecureJoin — buildah_image_reader.go:80. Correct (stays inside the rootfs), but files then surface under the path through the link, which contradicts "signed under its real path only" in the docs and PR body. Suggest "below the base, symlinks are not followed" — the unit test already phrases it that way.
4. Comments — buildah_image_reader.go:122-124 (ReadDir) and sign_files.go:147-151 (expandSigns) restate the error texts; both can be cut to a line.
5. CI is red and the PR is draft — before marking ready, a green run is needed so the Native Rootless glob spec is confirmed by CI, not only by my local run. docs_check_links failure not analysed.
Checked and fine
- Stage digest for literal paths is unchanged (
digest()hashessign.Pathas written) — no cache invalidation. - One container/mount per signing stage on both backends; Buildah
Closeattempts unmount and remove and joins the errors; a failed mount removes the container before returning. - Legacy Docker path: install commands go into
install.shviastapel.CreateScript,Cmdreceives a single line; host dir is already mounted:ro. - Duplicate-file and signature-of-signed detection at expansion time name both patterns; the static parser checks remain for literals.
- EN/RU docs,
werf_yaml.yml, JSON schema and the migration skill are in sync.
…s metacharacters
A pattern whose literal base is `/` (`/**/*.ko`) streams the whole image
filesystem out through `docker cp` for one signing stage; require a
literal directory below `/` instead. `*`, `?`, `[` and `{` in `path`
are now metacharacters, so a literal file name containing one is
written with a backslash — say so in the docs and the skill, and state
that symlinks are not followed below the base rather than "never":
the base itself resolves through the container root.
Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
|
Thanks — addressed in 7d3848f:
|
`sign[].path` was a literal path before glob support, so turning `?`,
`[` and `{` into syntax changed the meaning of existing names such as
`/out/file[1].bin` and forced a backslash-escaping rule nobody needs:
signing wants "every .ko below this directory" and nothing more. A
path is a pattern when it contains `*`; every other character stands
for itself, which is done by escaping it before the pattern reaches
doublestar.
Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
|
Follow-up on point 1, in 737366f: rather than documenting an escaping rule, the grammar is reduced — |
🤖 I have created a release *beep* *boop* --- ## [3.12.0-dk.3](v3.12.0-dk.2...v3.12.0-dk.3) (2026-10-26) ### Features * **build, stapel:** sign files matched by a glob pattern ([#471](#471)) ([ffda24e](ffda24e)) * **sbom:** inventory installed Python, JavaScript and Lua packages ([#472](#472)) ([7fde219](7fde219)) ### Bug Fixes * **build, stapel, import:** stop imports failing with sbom enabled ([#470](#470)) ([478b751](478b751)) * **sbom:** avoid marking editable Python packages as absent ([#474](#474)) ([e0b4b66](e0b4b66)) * **sbom:** retain legacy Python bytecode after source removal ([#476](#476)) ([36debcb](36debcb)) * **sbom:** stop marking os-pm packages under /lib64 as not shipped ([#475](#475)) ([cd0a029](cd0a029)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
sign[].pathin werf.yaml may now be an absolute glob pattern. werf expands it against the image filesystem when the signing stage runs and lays a detached PKCS#7<file>.p7snext to every regular file it matches, with the same host-side signing and the same signature shape as a literal path. This is what signing a set of files known only after the build takes — such as the several hundred kernel modulesmake modules_installproduces, which previously had to be signed inshellwith a Vault client inside the build container.What
werf.yaml
sign[].pathcontaining*is a glob pattern:*matches within one path segment,**across segments. Every other character —?,[,{included — stands for itself, so an existing literal path such as/out/file[1].binkeeps its exact previous behaviour and no escaping rule exists. The one thing that cannot be signed is a file with*in its name./(/**/*.ko) is rejected:a glob pattern must start with a literal directory below /!— otherwise the whole image filesystem would be streamed out for one signing stage.`path: /out/**/*.ko` of sign in image "kernel" matches no file./lib -> /usr/lib); below it symlinks are neither matched nor followed, so a file is signed under its real path only. A hard link is a regular file under another name and is signed under each name..p7sof another file signed at the same position is an error as well. Both are detected at expansion time; the static parser checks stay for literal paths and for an identical pattern string listed twice.Build
Signing <path>log line and its own<path>.p7s; the signature format and the certificate requirements are unchanged.Cmd.Documentation and skill
docs/pages_{en,ru}/usage/build/stapel/imports.mddescribe the pattern form, its error cases and the kernel-module example.werf-to-delivery-kitskill no longer lists kernel-module signing as a stop condition: it describes thesign:glob plusscripts/sign-file -sinbeforeSetupas the migration of a Vault-based module signing step.Why
A
signentry named exactly one file, so an image whose signed files are produced by the build — a kernel's modules — could not use the directive and kept signing inshell: a Vault client and a short-lived token inside the build container, and a network dependency thatbuild.sbom.enableforbids. Listing every module in werf.yaml is not an alternative: the set changes with the kernel config, and a module missing from the list would ship unsigned and refuse to load underCONFIG_MODULE_SIG_FORCE.The alternative of signing with an ephemeral key generated by the kernel build (
CONFIG_MODULE_SIG_ALL=y) was rejected: it puts a second trust anchor into the builtin keyring on every build and changes the CA-only trust model the consumer relies on.