From 968c365f2f2479018229c9b67c4c263a322bd987 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 16:42:36 +0300 Subject: [PATCH 01/22] feat(config): parse the sign directive of stapel images 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 --- pkg/config/raw_sign.go | 50 ++++++++++++++++ pkg/config/raw_stapel_image.go | 15 +++++ pkg/config/raw_stapel_image_test.go | 88 +++++++++++++++++++++++++++++ pkg/config/sign.go | 43 ++++++++++++++ pkg/config/stapel_image_base.go | 1 + 5 files changed, 197 insertions(+) create mode 100644 pkg/config/raw_sign.go create mode 100644 pkg/config/sign.go diff --git a/pkg/config/raw_sign.go b/pkg/config/raw_sign.go new file mode 100644 index 0000000000..d67b2ce26b --- /dev/null +++ b/pkg/config/raw_sign.go @@ -0,0 +1,50 @@ +package config + +type rawSign struct { + Path string `yaml:"path,omitempty"` + Before string `yaml:"before,omitempty"` + After string `yaml:"after,omitempty"` + + rawStapelImage *rawStapelImage `yaml:"-"` // parent + + UnsupportedAttributes map[string]interface{} `yaml:",inline"` +} + +func (c *rawSign) configSection() interface{} { + return c +} + +func (c *rawSign) doc() *doc { + return c.rawStapelImage.doc +} + +func (c *rawSign) UnmarshalYAML(unmarshal func(interface{}) error) error { + if parent, ok := parentStack.Peek().(*rawStapelImage); ok { + c.rawStapelImage = parent + } + + parentStack.Push(c) + type plain rawSign + err := unmarshal((*plain)(c)) + parentStack.Pop() + if err != nil { + return err + } + + return checkOverflow(c.UnsupportedAttributes, c, c.rawStapelImage.doc) +} + +func (c *rawSign) toDirective() (*Sign, error) { + sign := &Sign{ + Path: c.Path, + Before: c.Before, + After: c.After, + raw: c, + } + + if err := sign.validate(); err != nil { + return nil, err + } + + return sign, nil +} diff --git a/pkg/config/raw_stapel_image.go b/pkg/config/raw_stapel_image.go index bf8c3af4ca..0afe538320 100644 --- a/pkg/config/raw_stapel_image.go +++ b/pkg/config/raw_stapel_image.go @@ -21,6 +21,7 @@ type rawStapelImage struct { RawShell *rawShell `yaml:"shell,omitempty"` RawMount []*rawMount `yaml:"mount,omitempty"` RawImport []*rawImport `yaml:"import,omitempty"` + RawSign []*rawSign `yaml:"sign,omitempty"` RawDependencies []*rawDependency `yaml:"dependencies,omitempty"` Platform []string `yaml:"platform,omitempty"` Network string `yaml:"network,omitempty"` @@ -195,6 +196,20 @@ func (c *rawStapelImage) toStapelImageBaseDirective(ctx context.Context, gitermi return nil, err } + signPaths := map[string]struct{}{} + for _, rawSign := range c.RawSign { + signDirective, err := rawSign.toDirective() + if err != nil { + return nil, err + } + if _, exist := signPaths[signDirective.Path]; exist { + return nil, newDetailedConfigError(fmt.Sprintf("duplicated `path: %s` for sign!", signDirective.Path), rawSign, c.doc) + } + signPaths[signDirective.Path] = struct{}{} + + imageBase.Sign = append(imageBase.Sign, signDirective) + } + for _, rawDep := range c.RawDependencies { dependencyDirective, err := rawDep.toDirective() if err != nil { diff --git a/pkg/config/raw_stapel_image_test.go b/pkg/config/raw_stapel_image_test.go index 48af53aa09..f566599b14 100644 --- a/pkg/config/raw_stapel_image_test.go +++ b/pkg/config/raw_stapel_image_test.go @@ -43,6 +43,9 @@ var _ = Describe("rawStapelImage", func() { Expect(err).To(Succeed()) stapelImage.StapelImageBase.raw = nil // set to nil for correct deep comparison + for _, sign := range stapelImage.StapelImageBase.Sign { + sign.raw = nil + } Expect(&stapelImage).To(Equal(&expectedImage)) }, Entry( @@ -65,6 +68,32 @@ var _ = Describe("rawStapelImage", func() { }, }, ), + Entry( + "should handle sign", + map[string]interface{}{ + "image": "image1", + "from": "alpine:latest", + "sign": []map[string]interface{}{ + {"path": "/out/a.roothash", "after": "install"}, + {"path": "/out/b.roothash", "before": "setup"}, + }, + }, + &StapelImage{ + StapelImageBase: &StapelImageBase{ + Name: "image1", + From: "alpine:latest", + Git: &GitManager{}, + Secrets: []Secret{}, + Sign: []*Sign{ + {Path: "/out/a.roothash", After: "install"}, + {Path: "/out/b.roothash", Before: "setup"}, + }, + + platform: []string{}, + final: true, + }, + }, + ), ) DescribeTable("unmarshal and convert to directive succeed and produce expected Dependencies", @@ -322,5 +351,64 @@ var _ = Describe("rawStapelImage", func() { }}, }, ), + Entry( + "with sign missing path", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{{"after": "install"}}, + }, + ), + Entry( + "with sign relative path", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{{"path": "out/x.roothash", "after": "install"}}, + }, + ), + Entry( + "with sign root path", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{{"path": "/", "after": "install"}}, + }, + ), + Entry( + "with sign missing before/after", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{{"path": "/out/x.roothash"}}, + }, + ), + Entry( + "with sign both before and after", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{{"path": "/out/x.roothash", "before": "setup", "after": "install"}}, + }, + ), + Entry( + "with sign invalid stage", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{{"path": "/out/x.roothash", "after": "beforeInstall"}}, + }, + ), + Entry( + "with sign duplicated path", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{ + {"path": "/out/x.roothash", "after": "install"}, + {"path": "/out/x.roothash", "before": "setup"}, + }, + }, + ), ) }) diff --git a/pkg/config/sign.go b/pkg/config/sign.go new file mode 100644 index 0000000000..2efbad664a --- /dev/null +++ b/pkg/config/sign.go @@ -0,0 +1,43 @@ +package config + +import ( + "fmt" + "path" +) + +// Sign declares a file of the image whose detached PKCS#7 signature werf writes +// next to it as .p7s, right after the stage named by Before or After. +type Sign struct { + Path string + Before string + After string + + raw *rawSign +} + +func (c *Sign) GetRaw() interface{} { + return c.raw +} + +func (c *Sign) OutputPath() string { + return c.Path + ".p7s" +} + +func (c *Sign) validate() error { + switch { + case c.Path == "": + return newDetailedConfigError("`path: /PATH/TO/FILE` required for sign!", c.raw, c.raw.rawStapelImage.doc) + case !path.IsAbs(c.Path) || path.Clean(c.Path) != c.Path || c.Path == "/": + return newDetailedConfigError(fmt.Sprintf("invalid `path: %s` for sign: expected an absolute clean file path!", c.Path), c.raw, c.raw.rawStapelImage.doc) + case c.Before != "" && c.After != "": + return newDetailedConfigError("specify only one stage using `before: install|setup` or `after: install|setup` for sign!", c.raw, c.raw.rawStapelImage.doc) + case c.Before == "" && c.After == "": + return newDetailedConfigError("stage is not specified with `before: install|setup` or `after: install|setup` for sign!", c.raw, c.raw.rawStapelImage.doc) + case c.Before != "" && checkInvalidRelation(c.Before): + return newDetailedConfigError(fmt.Sprintf("invalid stage `before: %s` for sign: expected install or setup!", c.Before), c.raw, c.raw.rawStapelImage.doc) + case c.After != "" && checkInvalidRelation(c.After): + return newDetailedConfigError(fmt.Sprintf("invalid stage `after: %s` for sign: expected install or setup!", c.After), c.raw, c.raw.rawStapelImage.doc) + default: + return nil + } +} diff --git a/pkg/config/stapel_image_base.go b/pkg/config/stapel_image_base.go index bc6a74014f..be9542485d 100644 --- a/pkg/config/stapel_image_base.go +++ b/pkg/config/stapel_image_base.go @@ -18,6 +18,7 @@ type StapelImageBase struct { Shell *Shell Mount []*Mount Import []*Import + Sign []*Sign Dependencies []*Dependency Secrets []Secret ImageSpec *ImageSpec From ffaf63caf0738f9a940325bd6a11bea648aa4948 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 16:47:38 +0300 Subject: [PATCH 02/22] feat(signing): produce detached PKCS#7 signatures of files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- go.mod | 2 +- pkg/build/signing/files_signing.go | 121 ++++++++++++++++++++++++ pkg/build/signing/files_signing_test.go | 116 +++++++++++++++++++++++ 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 pkg/build/signing/files_signing.go create mode 100644 pkg/build/signing/files_signing_test.go diff --git a/go.mod b/go.mod index 7fb81273c8..b2ec641d6a 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/samber/lo v1.53.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sirupsen/logrus v1.10.2 + github.com/smallstep/pkcs7 v0.2.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/werf/common-go v0.0.0-20260831162604-46c3b45163be @@ -237,7 +238,6 @@ require ( github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect github.com/sigstore/sigstore-go v1.2.2 // indirect github.com/sigstore/timestamp-authority/v2 v2.1.2 // indirect - github.com/smallstep/pkcs7 v0.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect github.com/stretchr/objx v0.5.3 // indirect diff --git a/pkg/build/signing/files_signing.go b/pkg/build/signing/files_signing.go new file mode 100644 index 0000000000..43ff52cf29 --- /dev/null +++ b/pkg/build/signing/files_signing.go @@ -0,0 +1,121 @@ +package signing + +import ( + "bytes" + "context" + "crypto" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "io" + + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" + "github.com/smallstep/pkcs7" +) + +type FilesSigningOptions struct { + Enabled bool + + signer *Signer +} + +func (o FilesSigningOptions) Signer() *Signer { + return o.signer +} + +func NewFilesSigningOptions(signer *Signer) FilesSigningOptions { + return FilesSigningOptions{signer: signer} +} + +// SignDetachedPKCS7 wraps an RSASSA-PKCS1-v1_5/SHA-256 signature of content into a +// detached CMS SignedData with the leaf certificate embedded and no authenticated +// attributes: the profile systemd-keyutil writes and the kernel dm-verity signature +// check and systemd-dissect read. The signature is checked against the leaf +// certificate before it is returned, so a Vault answer that does not belong to the +// certificate stops the build here. +func SignDetachedPKCS7(ctx context.Context, content []byte, signer *Signer) ([]byte, error) { + if signer == nil || signer.sv == nil { + return nil, errors.New("signer is not configured") + } + + leaf, err := parseLeafCertificate(signer.Cert()) + if err != nil { + return nil, err + } + leafPub, ok := leaf.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("signing certificate carries a %T public key, only RSA is supported for detached PKCS#7", leaf.PublicKey) + } + + sd, err := pkcs7.NewSignedData(content) + if err != nil { + return nil, fmt.Errorf("create signed data: %w", err) + } + sd.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) + + cs := &digestSigner{ctx: ctx, sv: signer.sv, pub: leafPub} + if err := sd.SignWithoutAttr(leaf, cs, pkcs7.SignerInfoConfig{}); err != nil { + return nil, fmt.Errorf("sign content: %w", err) + } + if len(cs.signature) == 0 { + return nil, errors.New("signing produced no signature") + } + if err := rsa.VerifyPKCS1v15(leafPub, crypto.SHA256, cs.digest, cs.signature); err != nil { + return nil, fmt.Errorf("the signing certificate does not verify the signature the key produced: %w", err) + } + + sd.Detach() + der, err := sd.Finish() + if err != nil { + return nil, fmt.Errorf("encode signed data: %w", err) + } + return der, nil +} + +func parseLeafCertificate(pemData string) (*x509.Certificate, error) { + certs, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(pemData)) + if err != nil { + return nil, fmt.Errorf("parse signing certificate: %w", err) + } + if len(certs) == 0 { + return nil, errors.New("signing certificate is empty") + } + return certs[0], nil +} + +// digestSigner adapts a sigstore signer to crypto.Signer for pkcs7, which hands over +// the content digest; the signer receives it as is instead of hashing again. +type digestSigner struct { + ctx context.Context + sv signature.Signer + pub crypto.PublicKey + + digest []byte + signature []byte +} + +var _ crypto.Signer = (*digestSigner)(nil) + +func (s *digestSigner) Public() crypto.PublicKey { + return s.pub +} + +func (s *digestSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if opts == nil || opts.HashFunc() != crypto.SHA256 { + return nil, fmt.Errorf("unsupported digest for detached PKCS#7: %v", opts) + } + sig, err := s.sv.SignMessage(bytes.NewReader(nil), + options.WithContext(s.ctx), + options.WithDigest(digest), + options.WithCryptoSignerOpts(crypto.SHA256), + ) + if err != nil { + return nil, err + } + s.digest = digest + s.signature = sig + return sig, nil +} diff --git a/pkg/build/signing/files_signing_test.go b/pkg/build/signing/files_signing_test.go new file mode 100644 index 0000000000..6798a49f02 --- /dev/null +++ b/pkg/build/signing/files_signing_test.go @@ -0,0 +1,116 @@ +package signing + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "math/big" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/deckhouse/delivery-kit-sdk/pkg/signver" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/smallstep/pkcs7" +) + +func TestFilesSigning(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Files signing suite") +} + +func newTestSigner(ctx context.Context) (*Signer, *rsa.PrivateKey) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "files-signing-test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + Expect(err).NotTo(HaveOccurred()) + encryptedKey, err := encrypted.Encrypt(keyDER, nil) + Expect(err).NotTo(HaveOccurred()) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: signver.PrivateKeyPemType, Bytes: encryptedKey}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + signer, err := NewSigner(ctx, SignerOptions{ + KeyRef: base64.StdEncoding.EncodeToString(keyPEM), + CertRef: base64.StdEncoding.EncodeToString(certPEM), + }) + Expect(err).NotTo(HaveOccurred()) + return signer, key +} + +var _ = Describe("SignDetachedPKCS7", func() { + // The content a dm-verity root hash signature covers: the hex digest, no newline. + content := []byte("3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b") + + It("produces a detached SignedData without authenticated attributes that verifies against the leaf", func(ctx SpecContext) { + signer, key := newTestSigner(ctx) + + der, err := SignDetachedPKCS7(ctx, content, signer) + Expect(err).NotTo(HaveOccurred()) + + p7, err := pkcs7.Parse(der) + Expect(err).NotTo(HaveOccurred()) + Expect(p7.Content).To(BeEmpty(), "signature must be detached") + Expect(p7.Certificates).To(HaveLen(1), "the leaf certificate must be embedded") + Expect(p7.Signers).To(HaveLen(1)) + Expect(p7.Signers[0].AuthenticatedAttributes).To(BeEmpty(), "systemd-keyutil profile carries no authenticated attributes") + Expect(p7.Certificates[0].PublicKey).To(Equal(&key.PublicKey)) + + digest := sha256.Sum256(content) + Expect(rsa.VerifyPKCS1v15(&key.PublicKey, crypto.SHA256, digest[:], p7.Signers[0].EncryptedDigest)).To(Succeed(), + "the embedded signature must be PKCS#1 v1.5 over sha256(content), the form the kernel checks") + + p7.Content = content + Expect(p7.Verify()).To(Succeed()) + }) + + It("is accepted by openssl as a detached signature of the content", func(ctx SpecContext) { + if _, err := exec.LookPath("openssl"); err != nil { + Skip("openssl is not installed") + } + signer, _ := newTestSigner(ctx) + + der, err := SignDetachedPKCS7(ctx, content, signer) + Expect(err).NotTo(HaveOccurred()) + + dir := GinkgoT().TempDir() + p7sPath := filepath.Join(dir, "x.roothash.p7s") + contentPath := filepath.Join(dir, "x.roothash") + Expect(os.WriteFile(p7sPath, der, 0o600)).To(Succeed()) + Expect(os.WriteFile(contentPath, content, 0o600)).To(Succeed()) + + out, err := exec.CommandContext(ctx, "openssl", "smime", "-verify", "-inform", "der", "-in", p7sPath, "-content", contentPath, "-noverify", "-out", os.DevNull).CombinedOutput() + Expect(err).NotTo(HaveOccurred(), string(out)) + + tampered := append([]byte{}, content...) + tampered[0] ^= 1 + Expect(os.WriteFile(contentPath, tampered, 0o600)).To(Succeed()) + _, err = exec.CommandContext(ctx, "openssl", "smime", "-verify", "-inform", "der", "-in", p7sPath, "-content", contentPath, "-noverify", "-out", os.DevNull).CombinedOutput() + Expect(err).To(HaveOccurred(), "openssl must reject the signature over different content") + }) + + It("fails when the signer is not configured", func(ctx SpecContext) { + _, err := SignDetachedPKCS7(ctx, content, &Signer{}) + Expect(err).To(MatchError(ContainSubstring("signer is not configured"))) + }) +}) From c8f8ccfc7cbd61aa40b403b15607b13a9017c107 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 16:52:44 +0300 Subject: [PATCH 03/22] feat(build): require a key for the files the sign directive declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/werf/build/main.go | 1 + cmd/werf/bundle/publish/publish.go | 1 + cmd/werf/common/cmd_data.go | 5 ++ cmd/werf/common/conveyor_options.go | 7 ++ cmd/werf/common/signature.go | 67 +++++++++++++++++++ cmd/werf/compose/main.go | 1 + cmd/werf/converge/converge.go | 1 + cmd/werf/export/export.go | 1 + cmd/werf/kube_run/kube_run.go | 1 + cmd/werf/plan/plan.go | 1 + cmd/werf/render/render.go | 1 + cmd/werf/run/run.go | 1 + docs/_includes/reference/cli/werf_build.md | 16 +++++ .../reference/cli/werf_bundle_publish.md | 16 +++++ .../reference/cli/werf_compose_config.md | 16 +++++ .../reference/cli/werf_compose_down.md | 16 +++++ .../reference/cli/werf_compose_run.md | 16 +++++ .../reference/cli/werf_compose_up.md | 16 +++++ docs/_includes/reference/cli/werf_converge.md | 16 +++++ docs/_includes/reference/cli/werf_export.md | 16 +++++ docs/_includes/reference/cli/werf_kube_run.md | 16 +++++ docs/_includes/reference/cli/werf_plan.md | 16 +++++ docs/_includes/reference/cli/werf_render.md | 16 +++++ docs/_includes/reference/cli/werf_run.md | 16 +++++ pkg/build/build_phase.go | 1 + pkg/build/conveyor.go | 2 + pkg/build/image/image.go | 1 + 27 files changed, 284 insertions(+) diff --git a/cmd/werf/build/main.go b/cmd/werf/build/main.go index 614fb08c79..60e2554d18 100644 --- a/cmd/werf/build/main.go +++ b/cmd/werf/build/main.go @@ -89,6 +89,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupLogOptions(&commonCmdData, cmd) diff --git a/cmd/werf/bundle/publish/publish.go b/cmd/werf/bundle/publish/publish.go index ffa3f7f6ee..105c6479bd 100644 --- a/cmd/werf/bundle/publish/publish.go +++ b/cmd/werf/bundle/publish/publish.go @@ -136,6 +136,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) diff --git a/cmd/werf/common/cmd_data.go b/cmd/werf/common/cmd_data.go index 3985b6386c..3b5707ff02 100644 --- a/cmd/werf/common/cmd_data.go +++ b/cmd/werf/common/cmd_data.go @@ -123,6 +123,11 @@ type CmdData struct { SignCert *string SignIntermediates *string + SignFiles *bool + SignFilesKey *string + SignFilesCert *string + SignFilesIntermediates *string + VerifyManifest *bool VerifyRoots *[]string diff --git a/cmd/werf/common/conveyor_options.go b/cmd/werf/common/conveyor_options.go index 98fc8c403a..78f3264717 100644 --- a/cmd/werf/common/conveyor_options.go +++ b/cmd/werf/common/conveyor_options.go @@ -75,6 +75,7 @@ func GetConveyorOptionsWithParallel(ctx context.Context, commonCmdData *CmdData, conveyorOptions.ManifestSigningOptions = buildStagesOptions.ManifestSigningOptions conveyorOptions.SbomSigningOptions = buildStagesOptions.SbomSigningOptions conveyorOptions.VexSigningOptions = buildStagesOptions.VexSigningOptions + conveyorOptions.FilesSigningOptions = buildStagesOptions.FilesSigningOptions conveyorOptions.VerityAnnotationOptions = buildStagesOptions.VerityAnnotationOptions return conveyorOptions, nil @@ -138,6 +139,11 @@ func GetBuildOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *co return buildOptions, err } + filesSigningOptions, err := getFilesSigningOptions(ctx, commonCmdData, werfConfig) + if err != nil { + return buildOptions, err + } + verityAnnotationOptions, err := getVerityAnnotationOptions(commonCmdData) if err != nil { return build.BuildOptions{}, fmt.Errorf("getting verity annotation options: %w", err) @@ -157,6 +163,7 @@ func GetBuildOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *co SbomSigningOptions: sbomSigningOptions, VexSigningOptions: vexSigningOptions, ELFSigningOptions: elfSigningOptions, + FilesSigningOptions: filesSigningOptions, VerityAnnotationOptions: verityAnnotationOptions, ReportOperations: GetBuildReportOperations(commonCmdData), } diff --git a/cmd/werf/common/signature.go b/cmd/werf/common/signature.go index 9af0d1c1ee..59717dbb72 100644 --- a/cmd/werf/common/signature.go +++ b/cmd/werf/common/signature.go @@ -15,6 +15,7 @@ import ( "github.com/werf/common-go/pkg/util" "github.com/werf/werf/v3/pkg/build/signing" + "github.com/werf/werf/v3/pkg/config" "github.com/werf/werf/v3/pkg/signature" "github.com/werf/werf/v3/pkg/util/option" "github.com/werf/werf/v3/pkg/werf/exec" @@ -45,6 +46,32 @@ func GetSignManifest(cmdData *CmdData) bool { return option.PtrValueOrDefault(cmdData.SignManifest, false) } +func SetupFilesSigningOptions(cmdData *CmdData, cmd *cobra.Command) { + cmdData.SignFiles = new(bool) + cmd.Flags().BoolVarP(cmdData.SignFiles, "sign-files", "", util.GetBoolEnvironmentDefaultTrue("WERF_SIGN_FILES"), + `Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES or true). +When enabled and at least one image declares files to sign, +the private signing key must be specified with --sign-files-key option and +the certificate must be specified with --sign-files-cert option. +Disable to build such images without signatures`) + + cmdData.SignFilesKey = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesKey, "sign-files-key", "", os.Getenv("WERF_SIGN_FILES_KEY"), + "The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY)") + + cmdData.SignFilesCert = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesCert, "sign-files-cert", "", os.Getenv("WERF_SIGN_FILES_CERT"), + "The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_FILES_CERT)") + + cmdData.SignFilesIntermediates = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesIntermediates, "sign-files-intermediates", "", os.Getenv("WERF_SIGN_FILES_INTERMEDIATES"), + "The intermediate certificates of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES)") +} + +func GetSignFiles(cmdData *CmdData) bool { + return option.PtrValueOrDefault(cmdData.SignFiles, true) +} + func SetupELFSigningOptions(cmdData *CmdData, cmd *cobra.Command) { cmdData.SignELFFiles = new(bool) cmd.Flags().BoolVarP(cmdData.SignELFFiles, "sign-elf-files", "", util.GetBoolEnvironmentDefaultFalse("WERF_SIGN_ELF_FILES"), @@ -154,6 +181,46 @@ func getVexSigningOptions(commonCmdData *CmdData, signer *signing.Signer) signin return options } +// getFilesSigningOptions builds the signer of the sign directive. The directive is a +// requirement of the image, so when an image declares one the key and certificate are +// mandatory unless signing is switched off explicitly. +func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *config.WerfConfig) (signing.FilesSigningOptions, error) { + var signingImages []string + for _, img := range werfConfig.Images(false) { + stapelImg, ok := img.(config.StapelImageInterface) + if !ok || len(stapelImg.ImageBaseConfig().Sign) == 0 { + continue + } + signingImages = append(signingImages, img.GetName()) + } + + if len(signingImages) == 0 || !GetSignFiles(commonCmdData) { + return signing.NewFilesSigningOptions(&signing.Signer{}), nil + } + + keyRef := lo.FromPtr(commonCmdData.SignFilesKey) + certRef := lo.FromPtr(commonCmdData.SignFilesCert) + switch { + case keyRef == "": + return signing.FilesSigningOptions{}, fmt.Errorf("images %s declare files to sign: specify the signing key with --sign-files-key or pass --sign-files=false to build them unsigned", strings.Join(signingImages, ", ")) + case certRef == "": + return signing.FilesSigningOptions{}, fmt.Errorf("images %s declare files to sign: specify the signing certificate with --sign-files-cert", strings.Join(signingImages, ", ")) + } + + signer, err := signing.NewSigner(ctx, signing.SignerOptions{ + KeyRef: keyRef, + CertRef: certRef, + IntermediatesRef: lo.FromPtr(commonCmdData.SignFilesIntermediates), + }) + if err != nil { + return signing.FilesSigningOptions{}, fmt.Errorf("create files signer: %w", err) + } + + options := signing.NewFilesSigningOptions(signer) + options.Enabled = true + return options, nil +} + func getELFSigningOptions(commonCmdData *CmdData, signer *signing.Signer) (signing.ELFSigningOptions, error) { options := signing.NewELFSigningOptions(signer) diff --git a/cmd/werf/compose/main.go b/cmd/werf/compose/main.go index 52b7240412..f94020b0b4 100644 --- a/cmd/werf/compose/main.go +++ b/cmd/werf/compose/main.go @@ -303,6 +303,7 @@ func newCmd(ctx context.Context, composeCmdName string, options *newCmdOptions) common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) diff --git a/cmd/werf/converge/converge.go b/cmd/werf/converge/converge.go index 3416dda8f2..ca17f72f41 100644 --- a/cmd/werf/converge/converge.go +++ b/cmd/werf/converge/converge.go @@ -92,6 +92,7 @@ werf converge --repo registry.mydomain.com/web --env production`, common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupSecondaryStagesStorageOptions(&commonCmdData, cmd) diff --git a/cmd/werf/export/export.go b/cmd/werf/export/export.go index b8d90046ca..c4c2be7433 100644 --- a/cmd/werf/export/export.go +++ b/cmd/werf/export/export.go @@ -109,6 +109,7 @@ func NewExportCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) diff --git a/cmd/werf/kube_run/kube_run.go b/cmd/werf/kube_run/kube_run.go index 77bc77c4c2..bfe33294a5 100644 --- a/cmd/werf/kube_run/kube_run.go +++ b/cmd/werf/kube_run/kube_run.go @@ -171,6 +171,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) diff --git a/cmd/werf/plan/plan.go b/cmd/werf/plan/plan.go index 0c49d2abd0..319459d255 100644 --- a/cmd/werf/plan/plan.go +++ b/cmd/werf/plan/plan.go @@ -130,6 +130,7 @@ werf plan --repo registry.mydomain.com/web --env production`, common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupDisableAutoHostCleanup(&commonCmdData, cmd) diff --git a/cmd/werf/render/render.go b/cmd/werf/render/render.go index e6ad72bd8e..b54a7e5fb4 100644 --- a/cmd/werf/render/render.go +++ b/cmd/werf/render/render.go @@ -121,6 +121,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) commonCmdData.SetupPlatform(cmd) diff --git a/cmd/werf/run/run.go b/cmd/werf/run/run.go index 0fba9d6b67..432e1ae597 100644 --- a/cmd/werf/run/run.go +++ b/cmd/werf/run/run.go @@ -137,6 +137,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) + common.SetupFilesSigningOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) diff --git a/docs/_includes/reference/cli/werf_build.md b/docs/_includes/reference/cli/werf_build.md index 040d5c622a..ca5642c96a 100644 --- a/docs/_includes/reference/cli/werf_build.md +++ b/docs/_includes/reference/cli/werf_build.md @@ -280,6 +280,22 @@ werf build [IMAGE_NAME...] [options] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_bundle_publish.md b/docs/_includes/reference/cli/werf_bundle_publish.md index 94b5941445..ec78ebd082 100644 --- a/docs/_includes/reference/cli/werf_bundle_publish.md +++ b/docs/_includes/reference/cli/werf_bundle_publish.md @@ -328,6 +328,22 @@ werf bundle publish [IMAGE_NAME...] [options] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_config.md b/docs/_includes/reference/cli/werf_compose_config.md index cdcb48de1b..dc37dbfcf5 100644 --- a/docs/_includes/reference/cli/werf_compose_config.md +++ b/docs/_includes/reference/cli/werf_compose_config.md @@ -272,6 +272,22 @@ werf compose config [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_down.md b/docs/_includes/reference/cli/werf_compose_down.md index 1db89e6622..73a6999977 100644 --- a/docs/_includes/reference/cli/werf_compose_down.md +++ b/docs/_includes/reference/cli/werf_compose_down.md @@ -265,6 +265,22 @@ werf compose down [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_run.md b/docs/_includes/reference/cli/werf_compose_run.md index 6532501981..0eddc6c0d9 100644 --- a/docs/_includes/reference/cli/werf_compose_run.md +++ b/docs/_includes/reference/cli/werf_compose_run.md @@ -262,6 +262,22 @@ werf compose run [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_up.md b/docs/_includes/reference/cli/werf_compose_up.md index 255730b56b..d4f47d6aca 100644 --- a/docs/_includes/reference/cli/werf_compose_up.md +++ b/docs/_includes/reference/cli/werf_compose_up.md @@ -273,6 +273,22 @@ werf compose up [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] [ Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_converge.md b/docs/_includes/reference/cli/werf_converge.md index 964feac08f..c3d3adee4f 100644 --- a/docs/_includes/reference/cli/werf_converge.md +++ b/docs/_includes/reference/cli/werf_converge.md @@ -521,6 +521,22 @@ werf converge --repo registry.mydomain.com/web --env production Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_export.md b/docs/_includes/reference/cli/werf_export.md index cd29c7cc50..98b13d95c2 100644 --- a/docs/_includes/reference/cli/werf_export.md +++ b/docs/_includes/reference/cli/werf_export.md @@ -242,6 +242,22 @@ werf export [IMAGE_NAME...] [options] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_kube_run.md b/docs/_includes/reference/cli/werf_kube_run.md index 2ae44f27fa..3f20d531c3 100644 --- a/docs/_includes/reference/cli/werf_kube_run.md +++ b/docs/_includes/reference/cli/werf_kube_run.md @@ -309,6 +309,22 @@ werf kube-run [options] [IMAGE_NAME] [-- COMMAND ARG...] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_plan.md b/docs/_includes/reference/cli/werf_plan.md index 696acdec30..e02a40c176 100644 --- a/docs/_includes/reference/cli/werf_plan.md +++ b/docs/_includes/reference/cli/werf_plan.md @@ -506,6 +506,22 @@ werf plan --repo registry.mydomain.com/web --env production Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_render.md b/docs/_includes/reference/cli/werf_render.md index afd827729b..771e1f2631 100644 --- a/docs/_includes/reference/cli/werf_render.md +++ b/docs/_includes/reference/cli/werf_render.md @@ -407,6 +407,22 @@ werf render [IMAGE_NAME...] [options] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_run.md b/docs/_includes/reference/cli/werf_run.md index 5f3b3244f5..14d39266a6 100644 --- a/docs/_includes/reference/cli/werf_run.md +++ b/docs/_includes/reference/cli/werf_run.md @@ -215,6 +215,22 @@ werf run [options] [IMAGE_NAME] [-- COMMAND ARG...] Enable ELF files signing (default $WERF_SIGN_ELF_FILES). When enabled, the private signing key must be specified with --sign-key option and the certificate must be specified with --sign-cert option + --sign-files=true + Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES + or true). + When enabled and at least one image declares files to sign, + the private signing key must be specified with --sign-files-key option and + the certificate must be specified with --sign-files-cert option. + Disable to build such images without signatures + --sign-files-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_FILES_CERT) + --sign-files-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) + --sign-files-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index 953e015cfc..170618cd37 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -64,6 +64,7 @@ type BuildOptions struct { SbomSigningOptions signing.SbomSigningOptions VexSigningOptions signing.VexSigningOptions ELFSigningOptions signing.ELFSigningOptions + FilesSigningOptions signing.FilesSigningOptions VerityAnnotationOptions verify_annotation.Options ReportPath string diff --git a/pkg/build/conveyor.go b/pkg/build/conveyor.go index 8bbed260c3..7b39af39d2 100644 --- a/pkg/build/conveyor.go +++ b/pkg/build/conveyor.go @@ -91,6 +91,7 @@ type ConveyorOptions struct { ManifestSigningOptions signing.ManifestSigningOptions SbomSigningOptions signing.SbomSigningOptions VexSigningOptions signing.VexSigningOptions + FilesSigningOptions signing.FilesSigningOptions VerityAnnotationOptions verify_annotation.Options UseBuildReport bool BuildReportPath string @@ -142,6 +143,7 @@ func NewConveyor(werfConfig *config.WerfConfig, giterminismManager giterminism_m ContainerWerfDir: c.containerWerfDir, TmpDir: c.tmpDir, ManifestSigningOptions: opts.ManifestSigningOptions, + FilesSigningOptions: opts.FilesSigningOptions, VerityAnnotationOptions: opts.VerityAnnotationOptions, }, ImagesToProcess: opts.ImagesToProcess, diff --git a/pkg/build/image/image.go b/pkg/build/image/image.go index d2bd2fc61a..2061786e66 100644 --- a/pkg/build/image/image.go +++ b/pkg/build/image/image.go @@ -51,6 +51,7 @@ type CommonImageOptions struct { ForceTargetPlatformLogging bool ManifestSigningOptions signing.ManifestSigningOptions + FilesSigningOptions signing.FilesSigningOptions VerityAnnotationOptions verify_annotation.Options } From e7505682ca4c0985d85cd9fd35be655b724df0ea Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 17:00:49 +0300 Subject: [PATCH 04/22] feat(build): sign the files the sign directive declares inside the image 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 .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 --- pkg/build/image/stapel.go | 3 + pkg/build/image/stapel_test.go | 76 +++++++++++++++ pkg/build/stage/sign_files.go | 164 +++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 pkg/build/stage/sign_files.go diff --git a/pkg/build/image/stapel.go b/pkg/build/image/stapel.go index e8fcb82f93..9eefdc9e4f 100644 --- a/pkg/build/image/stapel.go +++ b/pkg/build/image/stapel.go @@ -128,10 +128,13 @@ func initStages(ctx context.Context, image *Image, metaConfig *config.Meta, stap stages = appendIfExist(stages, stage.GenerateInstallStage(ctx, imageBaseConfig, gitPatchStageOptions, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateDependenciesAfterInstallStage(imageBaseConfig, baseStageOptions)) + stages = appendIfExist(stages, stage.GenerateSignFilesAfterInstallStage(imageBaseConfig, opts.FilesSigningOptions, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateBeforeSetupStage(ctx, imageBaseConfig, gitPatchStageOptions, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateDependenciesBeforeSetupStage(imageBaseConfig, baseStageOptions)) + stages = appendIfExist(stages, stage.GenerateSignFilesBeforeSetupStage(imageBaseConfig, opts.FilesSigningOptions, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateSetupStage(ctx, imageBaseConfig, gitPatchStageOptions, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateDependenciesAfterSetupStage(imageBaseConfig, baseStageOptions)) + stages = appendIfExist(stages, stage.GenerateSignFilesAfterSetupStage(imageBaseConfig, opts.FilesSigningOptions, baseStageOptions)) if gitMappingsExist { stages = append(stages, stage.NewGitCacheStage(gitPatchStageOptions, baseStageOptions)) diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index 3067d1239c..cd20038190 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/werf/werf/v3/pkg/build/signing" "github.com/werf/werf/v3/pkg/build/stage" "github.com/werf/werf/v3/pkg/config" ) @@ -111,3 +112,78 @@ packages: Entry("build.sbom.enable true", "build:\n sbom:\n enable: true\n standard: cyclonedx@1.6"), ) }) + +var _ = Describe("sign files stages", func() { + werfYaml := func(sbomBlock string) string { + return fmt.Sprintf(`project: test +configVersion: 1 +%s +--- +image: app +from: alpine:3.20 +shell: + install: + - echo install + setup: + - echo setup +sign: +- path: /out/a.roothash + after: install +- path: /out/b.roothash + before: setup +- path: /out/c.roothash + after: setup +`, sbomBlock) + } + + newImage := func(ctx context.Context, sbomBlock string, filesSigning signing.FilesSigningOptions) *Image { + projectDir := newProjectRepo(ctx, map[string]string{"werf.yaml": werfYaml(sbomBlock)}) + metaConfig, imageConfig, opts := stapelImageConfig(ctx, projectDir) + opts.FilesSigningOptions = filesSigning + + image, err := mapStapelConfigToImage(ctx, metaConfig, imageConfig, "linux/amd64", false, opts) + Expect(err).NotTo(HaveOccurred()) + return image + } + + enabled := func() signing.FilesSigningOptions { + o := signing.NewFilesSigningOptions(&signing.Signer{}) + o.Enabled = true + return o + } + + It("places one stage per position right after the matching dependencies position", func() { + image := newImage(context.Background(), "", enabled()) + + names := stageNames(image) + Expect(names).To(Equal([]stage.StageName{ + stage.From, + stage.Install, + stage.SignFilesAfterInstall, + stage.SignFilesBeforeSetup, + stage.Setup, + stage.SignFilesAfterSetup, + })) + }) + + It("generates no stage when files signing is disabled", func() { + image := newImage(context.Background(), "", signing.NewFilesSigningOptions(&signing.Signer{})) + + for _, name := range []stage.StageName{stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { + Expect(stageNames(image)).NotTo(ContainElement(name)) + } + }) + + It("keeps shell stages offline under build.sbom.enable while the sign stages need no network either", func() { + image := newImage(context.Background(), "build:\n sbom:\n enable: true\n standard: cyclonedx@1.6", enabled()) + + for _, s := range image.GetStages() { + if s.Name() == stage.From { + continue + } + nn, ok := s.(interface{ NeedsNetwork() bool }) + Expect(ok).To(BeTrue(), string(s.Name())) + Expect(nn.NeedsNetwork()).To(BeFalse(), string(s.Name())) + } + }) +}) diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go new file mode 100644 index 0000000000..66f23815be --- /dev/null +++ b/pkg/build/stage/sign_files.go @@ -0,0 +1,164 @@ +package stage + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + + "github.com/werf/common-go/pkg/util" + "github.com/werf/logboek" + "github.com/werf/werf/v3/pkg/build/signing" + "github.com/werf/werf/v3/pkg/config" + "github.com/werf/werf/v3/pkg/container_backend" + "github.com/werf/werf/v3/pkg/stapel" +) + +const ( + SignFilesAfterInstall StageName = "signFilesAfterInstall" + SignFilesBeforeSetup StageName = "signFilesBeforeSetup" + SignFilesAfterSetup StageName = "signFilesAfterSetup" +) + +// SignFilesStage signs the files the sign directive places at one position of the +// stage chain. The files are read out of the previous built image, signed on the host +// by the files signer and the detached PKCS#7 signatures are laid down next to them as +// a new layer; the stage container itself runs nothing and needs no network. +type SignFilesStage struct { + *BaseStage + + signs []*config.Sign + options signing.FilesSigningOptions +} + +var _ Interface = (*SignFilesStage)(nil) + +func GenerateSignFilesAfterInstallStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { + return generateSignFilesStage(imageBaseConfig, SignFilesAfterInstall, &getImportsOptions{After: Install}, options, baseStageOptions) +} + +func GenerateSignFilesBeforeSetupStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { + return generateSignFilesStage(imageBaseConfig, SignFilesBeforeSetup, &getImportsOptions{Before: Setup}, options, baseStageOptions) +} + +func GenerateSignFilesAfterSetupStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { + return generateSignFilesStage(imageBaseConfig, SignFilesAfterSetup, &getImportsOptions{After: Setup}, options, baseStageOptions) +} + +func generateSignFilesStage(imageBaseConfig *config.StapelImageBase, name StageName, position *getImportsOptions, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { + if !options.Enabled { + return nil + } + + var signs []*config.Sign + for _, s := range imageBaseConfig.Sign { + if (position.After != "" && s.After == string(position.After)) || (position.Before != "" && s.Before == string(position.Before)) { + signs = append(signs, s) + } + } + if len(signs) == 0 { + return nil + } + + return &SignFilesStage{ + BaseStage: NewBaseStage(name, baseStageOptions), + signs: signs, + options: options, + } +} + +func (s *SignFilesStage) GetDependencies(_ context.Context, _ Conveyor, _ container_backend.ContainerBackend, _, _ *StageImage, _ container_backend.BuildContextArchiver) (string, error) { + return s.digest(), nil +} + +func (s *SignFilesStage) GetContentDependencies(_ context.Context, _ Conveyor, _ container_backend.BuildContextArchiver) (string, error) { + return s.digest(), nil +} + +func (s *SignFilesStage) digest() string { + args := []string{string(s.Name())} + for _, sign := range s.signs { + args = append(args, "Path", sign.Path) + } + args = append(args, "Cert", s.options.Signer().Cert(), "Chain", s.options.Signer().Chain()) + return util.Sha256Hash(args...) +} + +func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) error { + if err := s.BaseStage.PrepareImage(ctx, c, cb, prevBuiltImage, stageImage, buildContextArchive); err != nil { + return err + } + + signaturesDir := filepath.Join(s.imageTmpDir, "sign", string(s.Name())) + if err := os.MkdirAll(signaturesDir, os.ModePerm); err != nil { + return fmt.Errorf("create signatures dir %s: %w", signaturesDir, err) + } + + legacy := c.UseLegacyStapelBuilder(cb) + var commands []string + for _, sign := range s.signs { + content, err := cb.ReadFileFromImage(ctx, prevBuiltImage.Image.Name(), sign.Path, container_backend.ReadFileFromImageOpts{TargetPlatform: s.targetPlatform}) + if err != nil { + return fmt.Errorf("read %s to sign from image %q: %w", sign.Path, s.imageName, err) + } + + var signature []byte + if err := logboek.Context(ctx).Default().LogProcess("Signing %s", sign.Path).DoError(func() error { + signature, err = signing.SignDetachedPKCS7(ctx, content, s.options.Signer()) + return err + }); err != nil { + return fmt.Errorf("sign %s of image %q: %w", sign.Path, s.imageName, err) + } + + outputName := path.Base(sign.OutputPath()) + if legacy { + hostPath := filepath.Join(signaturesDir, outputName) + if err := os.WriteFile(hostPath, signature, 0o644); err != nil { + return fmt.Errorf("write signature %s: %w", hostPath, err) + } + containerPath := path.Join(s.containerWerfDir, "sign", string(s.Name()), outputName) + commands = append(commands, + fmt.Sprintf("%s -p %s", stapel.MkdirBinPath(), shellQuote(path.Dir(sign.OutputPath()))), + fmt.Sprintf("%s -m 0644 %s %s", stapel.InstallBinPath(), shellQuote(containerPath), shellQuote(sign.OutputPath())), + ) + continue + } + + archive, err := singleFileTar(outputName, signature) + if err != nil { + return fmt.Errorf("pack signature of %s: %w", sign.Path, err) + } + stageImage.Builder.StapelStageBuilder().AddDataArchive(io.NopCloser(archive), container_backend.FileArchive, sign.OutputPath(), container_backend.AddDataArchiveOptions{}) + } + + if legacy { + stageImage.Builder.LegacyStapelStageBuilder().Container().RunOptions().AddVolume(fmt.Sprintf("%s:%s:ro", signaturesDir, path.Join(s.containerWerfDir, "sign", string(s.Name())))) + stageImage.Builder.LegacyStapelStageBuilder().Container().AddServiceRunCommands(commands...) + } + + return nil +} + +func singleFileTar(name string, content []byte) (*bytes.Buffer, error) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(content)), Typeflag: tar.TypeReg}); err != nil { + return nil, err + } + if _, err := tw.Write(content); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + return &buf, nil +} + +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" +} From adac36b624a8adc39df36bd6bc11287b7830a8dc Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 17:36:36 +0300 Subject: [PATCH 05/22] test(e2e): cover the sign directive and document it 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 --- docs/_data/werf_yaml.yml | 25 +++ docs/pages_en/usage/build/sbom.md | 2 +- docs/pages_en/usage/build/stapel/imports.md | 31 ++++ docs/pages_ru/usage/build/sbom.md | 2 +- docs/pages_ru/usage/build/stapel/imports.md | 31 ++++ .../sign_files/keys/rsa_2048.pem.crt | 17 ++ .../sign_files/keys/rsa_2048.pem.key | 41 +++++ .../_fixtures/sign_files/state/werf.yaml | 14 ++ .../_fixtures/sign_files/state_sbom/werf.yaml | 21 +++ test/e2e/build/sign_files_test.go | 167 ++++++++++++++++++ 10 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.crt create mode 100644 test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.key create mode 100644 test/e2e/build/_fixtures/sign_files/state/werf.yaml create mode 100644 test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml create mode 100644 test/e2e/build/sign_files_test.go diff --git a/docs/_data/werf_yaml.yml b/docs/_data/werf_yaml.yml index 03f3c742a6..a07a368162 100644 --- a/docs/_data/werf_yaml.yml +++ b/docs/_data/werf_yaml.yml @@ -928,6 +928,31 @@ sections: description: en: "Globs for excluding" ru: "Глобы исключения" + - name: sign + description: + en: "Files to sign inside the image" + ru: "Подписание файлов внутри образа" + detailsArticle: + en: "/usage/build/stapel/imports.html#signing-files-inside-the-image" + ru: "/usage/build/stapel/imports.html#подписание-файлов-внутри-образа" + collapsible: true + isCollapsedByDefault: false + directiveList: + - name: path + value: "string" + description: + en: "The absolute path of the file to sign; the detached PKCS#7 signature is written next to it as `.p7s`" + ru: "Абсолютный путь к подписываемому файлу; отсоединённая подпись PKCS#7 записывается рядом как `.p7s`" + - 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" + - 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" - name: dependencies description: en: "Dependencies images for current image" diff --git a/docs/pages_en/usage/build/sbom.md b/docs/pages_en/usage/build/sbom.md index f1d1a4df49..bb79201edf 100644 --- a/docs/pages_en/usage/build/sbom.md +++ b/docs/pages_en/usage/build/sbom.md @@ -17,7 +17,7 @@ SBOM generation requires a container registry (specify --repo) - Only the **Docker backend** is supported. - Only the **Stapel syntax** for describing images (`werf.yaml`) is supported. -- **Network is disabled in shell stages**: downloading dependencies with a command in any shell stage (`beforeInstall`, `install`, `beforeSetup`, `setup`) will not work — dependencies are installed declaratively via the [`packages` directive]({{ "/usage/build/stapel/instructions.html#installing-binary-packages" | true_relative_url }}) only. This is what guarantees that all dependencies are recorded in the SBOM. +- **Network is disabled in shell stages**: downloading dependencies with a command in any shell stage (`beforeInstall`, `install`, `beforeSetup`, `setup`) will not work — dependencies are installed declaratively via the [`packages` directive]({{ "/usage/build/stapel/instructions.html#installing-binary-packages" | true_relative_url }}) only. This is what guarantees that all dependencies are recorded in the SBOM. A result that has to be signed by an external KMS is produced with the [`sign` directive]({{ "/usage/build/stapel/imports.html#signing-files-inside-the-image" | true_relative_url }}), which needs no network inside the stage. - The SBOM is built from **controlled inputs**: every `packages` entry is handled by its own cataloger (for file-based types syft reads the manifest/lock file — `go.sum`, `package-lock.json`, and so on; for `os-pm` — a dedicated cataloger). The list of supported ecosystems is fixed. - **Vendored dependencies are not recorded**: dependencies committed to the repository directly (`vendor/`, `third_party/`, and so on) bypass `packages` and do not end up in the SBOM. Do not use vendoring — all dependencies must come in via `packages`. - An SBOM exists only for images built with `build.sbom.enable: true` — previously built images have no SBOM and must be rebuilt. diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index 33f7fec9b1..e4419122a1 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -94,3 +94,34 @@ If `add` is a **file**, the outcome depends on the _destination image_: werf creates only the parent directory of `to`, so whether `to` itself exists is decided by the _destination image_ — pin the full file path when the difference matters. A trailing slash in `to` (anything other than `to: /`) is a configuration error and fails the build: write `to: /usr/sbin`, not `to: /usr/sbin/`. + +## 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. 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. + +```yaml +image: sysext +from: registry.example.com/base/ubuntu:22.04 +shell: + install: + - mkfs.erofs /out/ext.raw /src + - veritysetup format --root-hash-file=/out/ext.roothash /out/ext.raw /out/ext.verity +sign: +- path: /out/ext.roothash + after: install +``` + +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. + +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: + +| Flag | Environment variable | Description | +|---|---|---| +| `--sign-files` | `WERF_SIGN_FILES` | process `sign` directives (default `true`); `false` skips them and builds the image without the signatures | +| `--sign-files-key` | `WERF_SIGN_FILES_KEY` | the private key: a path to a PEM file, a base64-encoded PEM, or `hashivault://[KEY]` | +| `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | the signing certificate: a path to a PEM file or a base64-encoded PEM | +| `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | the intermediate certificates: a path to a PEM file or a base64-encoded PEM | + +A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. With `hashivault://` the private key never leaves the KMS: the stage container runs no commands and needs no network, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). + +The stage is cached like any other. Its digest covers the signed paths and the signing certificate chain, so rotating the key rebuilds the signature and the stages after it, while a rebuild with the same key reuses the cached layer. diff --git a/docs/pages_ru/usage/build/sbom.md b/docs/pages_ru/usage/build/sbom.md index 3ddb754187..0eafaa44ce 100644 --- a/docs/pages_ru/usage/build/sbom.md +++ b/docs/pages_ru/usage/build/sbom.md @@ -17,7 +17,7 @@ SBOM generation requires a container registry (specify --repo) - Поддерживается только **Docker backend**. - Поддерживается только **Stapel-синтаксис** описания образов (`werf.yaml`). -- **В shell-стадиях отключена сеть**: скачать зависимости командой в любой shell-стадии (`beforeInstall`, `install`, `beforeSetup`, `setup`) не получится — установка зависимостей выполняется только декларативно через [директиву `packages`]({{ "/usage/build/stapel/instructions.html#установка-бинарных-пакетов" | true_relative_url }}). Это и гарантирует, что все зависимости учтены в SBOM. +- **В shell-стадиях отключена сеть**: скачать зависимости командой в любой shell-стадии (`beforeInstall`, `install`, `beforeSetup`, `setup`) не получится — установка зависимостей выполняется только декларативно через [директиву `packages`]({{ "/usage/build/stapel/instructions.html#установка-бинарных-пакетов" | true_relative_url }}). Это и гарантирует, что все зависимости учтены в SBOM. Результат, который должен быть подписан внешним KMS, получают через [директиву `sign`]({{ "/usage/build/stapel/imports.html#подписание-файлов-внутри-образа" | true_relative_url }}) — ей не нужна сеть внутри стадии. - SBOM формируется из **контролируемых входов**: каждая запись `packages` учитывается своим каталогизатором (для file-based типов syft читает манифест/lock-файл — `go.sum`, `package-lock.json` и т.п.; для `os-pm` — отдельный каталогизатор). Список поддерживаемых экосистем фиксированный. - **Vendored-зависимости не учитываются**: зависимости, положенные в репозиторий напрямую (`vendor/`, `third_party/` и т.п.), проходят мимо `packages` и не попадают в SBOM. Не используйте vendoring — все зависимости должны заезжать через `packages`. - SBOM существует только у образов, собранных с `build.sbom.enable: true` — у ранее собранных образов SBOM нет, их нужно пересобрать. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index b8bb115611..cadfd314dd 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -94,3 +94,34 @@ import: werf создаёт только родительский каталог для `to`, поэтому существует ли сам `to`, определяет _образ назначения_ — когда разница важна, указывайте полный путь до файла. Завершающий слеш в `to` (всё, кроме `to: /`) — ошибка конфигурации, сборка падает: пишите `to: /usr/sbin`, а не `to: /usr/sbin/`. + +## Подписание файлов внутри образа + +Директива `sign` подписывает файл, который производит образ, и кладёт подпись рядом с ним, не давая сборочной стадии доступ к ключу подписи. Она занимает в цепочке стадий то же место, что и `import`: файл должен существовать после стадии, указанной в `before` или `after`, а подпись доступна последующим стадиям. + +```yaml +image: sysext +from: registry.example.com/base/ubuntu:22.04 +shell: + install: + - mkfs.erofs /out/ext.raw /src + - veritysetup format --root-hash-file=/out/ext.roothash /out/ext.raw /out/ext.verity +sign: +- path: /out/ext.roothash + after: install +``` + +werf читает файл из собранного к этому моменту образа, подписывает его на сборочном хосте и добавляет `.p7s` новым слоем. Подпись — отсоединённый PKCS#7 (CMS) `SignedData` над содержимым файла: SHA-256, RSASSA-PKCS1-v1_5, без аутентифицированных атрибутов, со встроенным сертификатом подписанта — ровно та форма, которую производит `systemd-keyutil pkcs7`, поэтому systemd и ядро Linux принимают её как подпись dm-verity root hash. Ключ, соответственно, должен быть RSA. + +Ключ и сертификат никогда не являются частью `werf.yaml`; они передаются только через командную строку, отдельно от ключа подписи манифеста и SBOM: + +| Флаг | Переменная окружения | Описание | +|---|---|---| +| `--sign-files` | `WERF_SIGN_FILES` | обрабатывать директивы `sign` (по умолчанию `true`); `false` пропускает их и собирает образ без подписей | +| `--sign-files-key` | `WERF_SIGN_FILES_KEY` | приватный ключ: путь к PEM-файлу, PEM в base64 или `hashivault://[KEY]` | +| `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | сертификат подписанта: путь к PEM-файлу или PEM в base64 | +| `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | промежуточные сертификаты: путь к PEM-файлу или PEM в base64 | + +Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. С `hashivault://` приватный ключ не покидает KMS: контейнер стадии не выполняет команд и не нуждается в сети, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). + +Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути и цепочку сертификатов подписанта, поэтому ротация ключа пересобирает подпись и последующие стадии, а повторная сборка с тем же ключом переиспользует закэшированный слой. diff --git a/test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.crt b/test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.crt new file mode 100644 index 0000000000..a4bb815593 --- /dev/null +++ b/test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyTCCAbGgAwIBAgIBBzANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDExN3ZXJm +LWUyZS1zaWduLWZpbGVzMB4XDTI2MTAxNDEzMDU1NloXDTM2MTAxNDE0MDU1Nlow +HjEcMBoGA1UEAxMTd2VyZi1lMmUtc2lnbi1maWxlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAODtpy6et9IHDZVN4Fl/o0qou1EGQUKLphFPha4a93PD +/qFerzeqcnIJst4QzXHah0SgUu3y+GtZuBexTAp9jOEGxx4LwoqllPezp/o5oUNS +49RU7zFfo/4GtIGi5+asMDFLfFsMIrGXjaJLykhVsanFgZx527B3bYC1l0qgoNKP +TWfbxTTU2zG+H1dv0SG0bc19Z1qsJSnmemSXrkeQ47c7U+CU8acNA6opg4UOzPfe +r8O1GTT9wYK9MN/Aa7fAJ9s0Mnbb+1HrBO6XqokMS/v55daN+l93dn2d9V+kCbXx +z27oMqPRb1GH4yftg3BO0YCVj3YAqUeNiJ4qqKkJCmECAwEAAaMSMBAwDgYDVR0P +AQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQA3jmuzx5v0ELZzWLaU9+ltAw/l +YkMC4C3dhfG/JMsqV9c0jkSy6rNuC4IEONiccqkTP14sCrenzP/wGwMS3zkre0a/ ++Xu/s80A95A3cXpXBxG3FOGGXfkL6Mqx0KQsIAolYC/o0vLDA0b3508u2C2rpiRB +smOyM97pHJ9xDGKhxlx+IsFbZr9rD8zHTfdX+607t8NqCWGHzTKPuUfueDsgazUD +kqpu+wWi4aF42jxSk+r57wvVI1eEzWJr/EkqJ/7Vg/5FXd+3CYQ60ui9+ty/dyRR +SCeWEhs0e5ftQ0uI2IYuAtYsZQmtAT0f8360US1X/x3S1Iu6/kSF1Ic0B4Ni +-----END CERTIFICATE----- diff --git a/test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.key b/test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.key new file mode 100644 index 0000000000..1fecb3d283 --- /dev/null +++ b/test/e2e/build/_fixtures/sign_files/keys/rsa_2048.pem.key @@ -0,0 +1,41 @@ +-----BEGIN PRIVATE KEY----- +eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6 +OCwicCI6MX0sInNhbHQiOiJDcXh5Ky93Y3g2RDZXUEoxZy84c09tVHFRakJiekV0 +cG1YdFhhaXJOODEwPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 +Iiwibm9uY2UiOiIxTVY5aTB3UzJKZk9Vc3JzYnBITnk2OUkxUEUwcElYNCJ9LCJj +aXBoZXJ0ZXh0Ijoiak0vVjFOVXVNdXVaVC9NUW5wK2IvN2FzSHlNQXZvUUdnWXA1 +b2hESTFTMGVQQUhvVnptOXFTOWxheWphRDFQWXVCVzZLZjJINUs5eFc5SXAva01Z +RTZKYisvVkZ2SFYwS3REZG12TmUxSVVLM1pORndYYTZoRFpVckVaQmhoQWYwM0dZ +djYvY0NWdDBKcURvditMWkNnanBxUlkvaGxZQmFYaFBEd3NKbUI4ZEJRb3RuSjlS +dngxWWFUWjByZnlpVE1RVGJBVjUrTWljbjduNDNSNGxRamVyWXNQR0l0OHM5SHdt +SmgwcnhHRkpNTzlGNjdheTNGaC8wYkczS3BjRUNRSEhnUDhKQllIL2FVR01GZm1i +U2doaFFoTEt2ZlV4SWJ0bE0rM2JzbDh3am10Z3B4MHYxdHFNM3pYZ042eFErYzlV +ZmhURTk3OHIvYW9SNUN0WU9mUEdsaVJlUTBudzhYTVhQTWNhemxHdGM4ZHlmUit3 +Ym54NkZoQVkyaXZ6UXVTRmtzVU5vb28xTDl1UitWcDkxRWRnblZWbHA4WlBmMTEw +a2hiNzg5S3ZHc1NWZlJJZnIvVEFqSklESFgrdjhadkxPdThvOE8raVBlaVVqK3l2 +T1B0QU81cTFVaVc2WUI3YXF3ZHVpWC9KWVRZRkhFQ3VCdzFjbkNMRXZ4MUZvZUh0 +OUQ2bU0zc3Z5c3hYUDZKb1JxeVd3SS9xbkpVWXU2SyttbWF2andCZXJUK3NaOUc5 +SkwxbW9SaGg0T2R2d0pRRjNnOHdpQ1NFTm9nN3Q1RWxiTy9ZcFYzczhrUDEzZlBt +ZW1yT3lKM3lOZXBMSXgvSGxMcHBzQS9BOFNqUERMc1QrdW9RcWVYaXNYNXpnbFRI +ellHL2xTU0U5NVNtZmxWLzRYRVNtNGJVZDNwbGowUjdtb01zVnFwM0Q0MTNUUzlY +NjdTN2FnM05VSnphdzVDemhLaDBLdXRSYmozTHVYZlJHRGRYdE4yWkNHWnpiZ0o2 +ZzJJWFprQ054VTRaUnMzank4QTNmQ1gxQ1dYSnpKQVlzRmVxUXlmZGIxb1lxTkVQ +SUpjOG9ZOFF1TmlJVDRIaEd2U2xBbFI5VWpmc3RCTE4vaUlTcnRjSmlVM3F2Smkv +dmdtSDVZRXpEZk5WaWZDMjhSR0xRdktGSldoeUlkRDVKZHhmQzhHMm9ySXFBY0xE +QjR2QnRnQ3pySHZiUUh3VHJSTXEzeVFYY3BsWDQ3V09yRnRCcDhaaVJMOVhjSGU1 +K1BrU0o2VysrRVJ0dnQ4NGVSbUxKWXp4NnNlQVQzQ0NPMlJtOGhQVG8zNW9aMm9X +cVRNQTV6Q1IzOGJ6ODlzZDI2SnFob0pJbFloRlh1ZFFUaE9UbkRjc0lPbm84Vjht +RU4zeUd4eUZNQit2VjZPMlNGNVdyeFdDUXh4WjdGejVrak9HYk9GTHgzaDl6L2Zm +N2UrblpMZUIyZ2lUcUw2UU5oT0taSFN4WjR6eXdDRkFvUVBjYUZsWjY0aVFXQWlU +VlNVaXB1WmVrRWU1UTVwQk5sVEJMWStCWGYyT1pjMGJlMDBXZUxpNDA4Q1ZzTjNy +dFV6dko1dTRjK05GWjF4TnVwSWZrTTE2UFJiZFl4T1lERm9QaGRlN3VGcnJMa2hl +L2FuSm96Z2NXOGwwY3gzeUZnOGwzWjJ6RnF4bFM4cUZndXFGVkR0SWZMVUFTWFAv +cjJSUmhMaWQ2c2Z1MG5iRUxDVkhobXVRaGRTOG5kekJHaWVYRGhucVdpL2hEajl3 +SlQ1YWdpT255bWo1OHRWSE04UXh3ZDA0b0VtSTZJRVBabnFjclI1VjFMQnpGdXJm +YUNYYlF6bVovZktUaGt0OXl2VHF2Q09vL0IvNHgrK083am5DZmp0OFErMnUzemFG +M1JNbTcvbkowNzFRTVZoNUp1QUJpbzNBekNDN1dGWVBhZkh0bnErN1pBVEFNelJM +WUcxbzhlUnE1SnhVSnp2a29sQ3BiVW5mMldpdVh2bmRBclZPQjREajV3ak0yM0xS +OTEybVdCREF1dFZ1Y0k4TEhDcEhKYlRMcWdLaytZUURvakU4K1VzWGVOaks0cGRM +RnNvc2Uwa09OT2ZpTFJscWpKYUJyWWVkeXZjb3h5WU1hM2NFdEt1b0t6dUsvUGlv +aFJLczV5VEtqZnk5TTQzeWVrVXNJOFJvZEE9PSJ9 +-----END PRIVATE KEY----- diff --git a/test/e2e/build/_fixtures/sign_files/state/werf.yaml b/test/e2e/build/_fixtures/sign_files/state/werf.yaml new file mode 100644 index 0000000000..da8318c2c0 --- /dev/null +++ b/test/e2e/build/_fixtures/sign_files/state/werf.yaml @@ -0,0 +1,14 @@ +project: werf-test-e2e-sign-files +configVersion: 1 +--- +image: sysext +from: alpine:3.20 +shell: + install: + - mkdir -p /out + - printf '%s' 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b > /out/ext.roothash + setup: + - test -s /out/ext.roothash.p7s +sign: +- path: /out/ext.roothash + after: install diff --git a/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml b/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml new file mode 100644 index 0000000000..b8020b253d --- /dev/null +++ b/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml @@ -0,0 +1,21 @@ +project: werf-test-e2e-sign-files-sbom +configVersion: 1 +build: + platform: + - linux/amd64 + sbom: + enable: true + standard: cyclonedx@1.6 +--- +image: sysext +from: registry.deckhouse.io/container-factory@sha256:7aac8d97a91ac233b19111aaad9d7bba12b975e620cfa9287b97e0003e4d0a71 +shell: + install: + - mkdir -p /out + - printf '%s' 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b > /out/ext.roothash + - if wget -q -T 3 -O /dev/null http://1.1.1.1 2>/dev/null; then echo 'network is reachable' >&2; exit 1; fi + setup: + - test -s /out/ext.roothash.p7s +sign: +- path: /out/ext.roothash + after: install diff --git a/test/e2e/build/sign_files_test.go b/test/e2e/build/sign_files_test.go new file mode 100644 index 0000000000..1905a38d98 --- /dev/null +++ b/test/e2e/build/sign_files_test.go @@ -0,0 +1,167 @@ +package e2e_build_test + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + _ "embed" + "encoding/base64" + "os" + "os/exec" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/smallstep/pkcs7" + + elfTar "github.com/werf/werf/v3/pkg/signature/elf/tar" + "github.com/werf/werf/v3/test/pkg/report" + "github.com/werf/werf/v3/test/pkg/utils" + "github.com/werf/werf/v3/test/pkg/werf" +) + +const ( + signFilesContent = "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b" + signFilesPath = "/out/ext.roothash" +) + +var ( + //go:embed _fixtures/sign_files/keys/rsa_2048.pem.key + signFilesKeyData []byte + //go:embed _fixtures/sign_files/keys/rsa_2048.pem.crt + signFilesCertData []byte +) + +type signFilesTestOptions struct { + setupEnvOptions +} + +var _ = Describe("sign directive", Label("e2e", "sign-files", "simple"), func() { + signArgs := []string{ + "--sign-files-key", base64.StdEncoding.EncodeToString(signFilesKeyData), + "--sign-files-cert", base64.StdEncoding.EncodeToString(signFilesCertData), + } + + // readSignedFiles returns the signed file and its .p7s out of the built image. + readSignedFiles := func(imageName string) (content, p7s []byte) { + img := loadImageFromRegistry(imageName) + utils.ForEachImageFile(SuiteData.TmpDir, img, func(header *elfTar.Header, tmpLocalPath string) { + switch "/" + strings.TrimPrefix(header.Name, "/") { + case signFilesPath: + var err error + content, err = os.ReadFile(tmpLocalPath) + Expect(err).To(Succeed()) + case signFilesPath + ".p7s": + var err error + p7s, err = os.ReadFile(tmpLocalPath) + Expect(err).To(Succeed()) + } + }) + Expect(content).To(Equal([]byte(signFilesContent))) + Expect(p7s).NotTo(BeEmpty(), "the image must carry %s.p7s", signFilesPath) + return content, p7s + } + + // expectSystemdProfile asserts the .p7s is what systemd-keyutil would have written: + // a detached SignedData, no authenticated attributes, the leaf embedded, an + // RSASSA-PKCS1-v1_5/SHA-256 signature over the content itself. + expectSystemdProfile := func(content, p7s []byte) { + p7, err := pkcs7.Parse(p7s) + Expect(err).To(Succeed()) + Expect(p7.Content).To(BeEmpty()) + Expect(p7.Signers).To(HaveLen(1)) + Expect(p7.Signers[0].AuthenticatedAttributes).To(BeEmpty()) + + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(signFilesCertData) + Expect(err).To(Succeed()) + Expect(p7.Certificates).To(HaveLen(1)) + Expect(p7.Certificates[0].Raw).To(Equal(certs[0].Raw)) + + digest := sha256.Sum256(content) + Expect(rsa.VerifyPKCS1v15(certs[0].PublicKey.(*rsa.PublicKey), crypto.SHA256, digest[:], p7.Signers[0].EncryptedDigest)).To(Succeed()) + + if _, err := exec.LookPath("openssl"); err != nil { + return + } + dir := GinkgoT().TempDir() + contentPath := filepath.Join(dir, "ext.roothash") + p7sPath := contentPath + ".p7s" + Expect(os.WriteFile(contentPath, content, 0o600)).To(Succeed()) + Expect(os.WriteFile(p7sPath, p7s, 0o600)).To(Succeed()) + out, err := exec.Command("openssl", "smime", "-verify", "-inform", "der", "-in", p7sPath, "-content", contentPath, "-noverify", "-out", os.DevNull).CombinedOutput() + Expect(err).To(Succeed(), string(out)) + } + + DescribeTable("signs the declared file after the named stage", + func(ctx SpecContext, testOpts signFilesTestOptions) { + setupEnv(testOpts.setupEnvOptions) + + SuiteData.InitTestRepo(ctx, "repo0", "sign_files/state") + werfProject := report.NewProjectWithReport(werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0"))) + + By("building with the files signer") + buildOut, buildReport := werfProject.BuildWithReport(ctx, SuiteData.GetBuildReportPath("report0.json"), &werf.WithReportOptions{CommonOptions: werf.CommonOptions{ExtraArgs: signArgs}}) + Expect(buildOut).To(ContainSubstring("Building stage sysext/install")) + Expect(buildOut).To(ContainSubstring("Signing " + signFilesPath)) + Expect(buildOut).To(ContainSubstring("Building stage sysext/signFilesAfterInstall")) + Expect(buildOut).To(ContainSubstring("Building stage sysext/setup")) + + By("verifying the signature in the image") + expectSystemdProfile(readSignedFiles(buildReport.Images["sysext"].DockerImageName)) + + By("rebuilding with the same key reuses the stage") + rebuildOut := werfProject.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: signArgs}}) + Expect(rebuildOut).NotTo(ContainSubstring("Building stage sysext/signFilesAfterInstall")) + }, + backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ + ContainerBackendMode: "docker", + WithLocalRepo: true, + }}), + ) + + DescribeTable("keeps the build offline under build.sbom.enable and still signs", + func(ctx SpecContext, testOpts signFilesTestOptions) { + setupEnv(testOpts.setupEnvOptions) + + SuiteData.InitTestRepo(ctx, "repo0", "sign_files/state_sbom") + werfProject := report.NewProjectWithReport(werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0"))) + + buildOut, buildReport := werfProject.BuildWithReport(ctx, SuiteData.GetBuildReportPath("report0.json"), &werf.WithReportOptions{CommonOptions: werf.CommonOptions{ExtraArgs: signArgs}}) + Expect(buildOut).To(ContainSubstring("Network is disabled for shell stages")) + Expect(buildOut).To(ContainSubstring("Building stage sysext/signFilesAfterInstall")) + + expectSystemdProfile(readSignedFiles(buildReport.Images["sysext"].DockerImageName)) + }, + backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ + ContainerBackendMode: "docker", + WithLocalRepo: true, + }}), + ) + + DescribeTable("refuses to build without a key unless signing is switched off", + func(ctx SpecContext, testOpts signFilesTestOptions) { + setupEnv(testOpts.setupEnvOptions) + + SuiteData.InitTestRepo(ctx, "repo0", "sign_files/state") + werfProject := werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0")) + + By("no key: the build stops before any stage") + out, err := werfProject.BuildWithErr(ctx, &werf.BuildOptions{}) + Expect(err).To(HaveOccurred()) + Expect(out).To(ContainSubstring("images sysext declare files to sign")) + Expect(out).NotTo(ContainSubstring("Building stage")) + + By("--sign-files=false: the image is built without the signature") + out, err = werfProject.BuildWithErr(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: []string{"--sign-files=false"}}}) + Expect(err).To(HaveOccurred(), "the setup stage asserts the .p7s is present, so it must fail") + Expect(out).NotTo(ContainSubstring("signFilesAfterInstall")) + Expect(out).To(ContainSubstring("Building stage sysext/setup")) + }, + backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ + ContainerBackendMode: "docker", + WithLocalRepo: true, + }}), + ) +}) From c6ed7c680994a41b0f438a2667bd14b12ca253e7 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 17:56:52 +0300 Subject: [PATCH 06/22] fix(build): sign before install too and leave commands without the flag 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 --- cmd/werf/common/signature.go | 2 +- docs/pages_en/usage/build/stapel/imports.md | 2 +- docs/pages_ru/usage/build/stapel/imports.md | 2 +- pkg/build/image/stapel.go | 1 + pkg/build/image/stapel_test.go | 5 +- pkg/build/stage/sign_files.go | 24 +++--- pkg/build/stage/sign_files_test.go | 96 +++++++++++++++++++++ test/e2e/build/sign_files_test.go | 7 +- 8 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 pkg/build/stage/sign_files_test.go diff --git a/cmd/werf/common/signature.go b/cmd/werf/common/signature.go index 59717dbb72..ab9beed715 100644 --- a/cmd/werf/common/signature.go +++ b/cmd/werf/common/signature.go @@ -69,7 +69,7 @@ Disable to build such images without signatures`) } func GetSignFiles(cmdData *CmdData) bool { - return option.PtrValueOrDefault(cmdData.SignFiles, true) + return option.PtrValueOrDefault(cmdData.SignFiles, false) } func SetupELFSigningOptions(cmdData *CmdData, cmd *cobra.Command) { diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index e4419122a1..0cdc6dd2ee 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -122,6 +122,6 @@ The key and the certificate are never part of `werf.yaml`; they come from the co | `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | the signing certificate: a path to a PEM file or a base64-encoded PEM | | `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | the intermediate certificates: a path to a PEM file or a base64-encoded PEM | -A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. With `hashivault://` the private key never leaves the KMS: the stage container runs no commands and needs no network, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). +A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. With `hashivault://` the private key never leaves the KMS: the stage runs none of the image's commands and needs no network, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). The stage is cached like any other. Its digest covers the signed paths and the signing certificate chain, so rotating the key rebuilds the signature and the stages after it, while a rebuild with the same key reuses the cached layer. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index cadfd314dd..1d5a462068 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -122,6 +122,6 @@ werf читает файл из собранного к этому момент | `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | сертификат подписанта: путь к PEM-файлу или PEM в base64 | | `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | промежуточные сертификаты: путь к PEM-файлу или PEM в base64 | -Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. С `hashivault://` приватный ключ не покидает KMS: контейнер стадии не выполняет команд и не нуждается в сети, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). +Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. С `hashivault://` приватный ключ не покидает KMS: стадия не выполняет команд образа и не нуждается в сети, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути и цепочку сертификатов подписанта, поэтому ротация ключа пересобирает подпись и последующие стадии, а повторная сборка с тем же ключом переиспользует закэшированный слой. diff --git a/pkg/build/image/stapel.go b/pkg/build/image/stapel.go index 9eefdc9e4f..158074bf7d 100644 --- a/pkg/build/image/stapel.go +++ b/pkg/build/image/stapel.go @@ -119,6 +119,7 @@ func initStages(ctx context.Context, image *Image, metaConfig *config.Meta, stap stages = appendIfExist(stages, stage.GenerateFromStage(imageBaseConfig, image.baseImageRepoId, imageCacheVersion, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateBeforeInstallStage(ctx, imageBaseConfig, baseStageOptions)) stages = appendIfExist(stages, stage.GenerateDependenciesBeforeInstallStage(imageBaseConfig, baseStageOptions)) + stages = appendIfExist(stages, stage.GenerateSignFilesBeforeInstallStage(imageBaseConfig, opts.FilesSigningOptions, baseStageOptions)) if gitMappingsExist { stages = append(stages, stage.NewGitArchiveStage(gitArchiveStageOptions, baseStageOptions)) diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index cd20038190..17655882de 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -127,6 +127,8 @@ shell: setup: - echo setup sign: +- path: /etc/os-release + before: install - path: /out/a.roothash after: install - path: /out/b.roothash @@ -158,6 +160,7 @@ sign: names := stageNames(image) Expect(names).To(Equal([]stage.StageName{ stage.From, + stage.SignFilesBeforeInstall, stage.Install, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, @@ -169,7 +172,7 @@ sign: It("generates no stage when files signing is disabled", func() { image := newImage(context.Background(), "", signing.NewFilesSigningOptions(&signing.Signer{})) - for _, name := range []stage.StageName{stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { + for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { Expect(stageNames(image)).NotTo(ContainElement(name)) } }) diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index 66f23815be..72c23e4e99 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -9,7 +9,8 @@ import ( "os" "path" "path/filepath" - "strings" + + "al.essio.dev/pkg/shellescape" "github.com/werf/common-go/pkg/util" "github.com/werf/logboek" @@ -20,15 +21,16 @@ import ( ) const ( - SignFilesAfterInstall StageName = "signFilesAfterInstall" - SignFilesBeforeSetup StageName = "signFilesBeforeSetup" - SignFilesAfterSetup StageName = "signFilesAfterSetup" + SignFilesBeforeInstall StageName = "signFilesBeforeInstall" + SignFilesAfterInstall StageName = "signFilesAfterInstall" + SignFilesBeforeSetup StageName = "signFilesBeforeSetup" + SignFilesAfterSetup StageName = "signFilesAfterSetup" ) // SignFilesStage signs the files the sign directive places at one position of the // stage chain. The files are read out of the previous built image, signed on the host // by the files signer and the detached PKCS#7 signatures are laid down next to them as -// a new layer; the stage container itself runs nothing and needs no network. +// a new layer; nothing of the user's runs in the stage container and it needs no network. type SignFilesStage struct { *BaseStage @@ -38,6 +40,10 @@ type SignFilesStage struct { var _ Interface = (*SignFilesStage)(nil) +func GenerateSignFilesBeforeInstallStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { + return generateSignFilesStage(imageBaseConfig, SignFilesBeforeInstall, &getImportsOptions{Before: Install}, options, baseStageOptions) +} + func GenerateSignFilesAfterInstallStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { return generateSignFilesStage(imageBaseConfig, SignFilesAfterInstall, &getImportsOptions{After: Install}, options, baseStageOptions) } @@ -123,8 +129,8 @@ func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb contai } containerPath := path.Join(s.containerWerfDir, "sign", string(s.Name()), outputName) commands = append(commands, - fmt.Sprintf("%s -p %s", stapel.MkdirBinPath(), shellQuote(path.Dir(sign.OutputPath()))), - fmt.Sprintf("%s -m 0644 %s %s", stapel.InstallBinPath(), shellQuote(containerPath), shellQuote(sign.OutputPath())), + fmt.Sprintf("%s -p %s", stapel.MkdirBinPath(), shellescape.Quote(path.Dir(sign.OutputPath()))), + fmt.Sprintf("%s -m 0644 %s %s", stapel.InstallBinPath(), shellescape.Quote(containerPath), shellescape.Quote(sign.OutputPath())), ) continue } @@ -158,7 +164,3 @@ func singleFileTar(name string, content []byte) (*bytes.Buffer, error) { } return &buf, nil } - -func shellQuote(s string) string { - return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" -} diff --git a/pkg/build/stage/sign_files_test.go b/pkg/build/stage/sign_files_test.go new file mode 100644 index 0000000000..5fc93b5320 --- /dev/null +++ b/pkg/build/stage/sign_files_test.go @@ -0,0 +1,96 @@ +package stage + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "math/big" + "testing" + "time" + + "github.com/deckhouse/delivery-kit-sdk/pkg/signver" + "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/stretchr/testify/require" + + "github.com/werf/werf/v3/pkg/build/signing" + "github.com/werf/werf/v3/pkg/config" +) + +func newSignFilesTestSigner(t *testing.T, cn string) *signing.Signer { + t.Helper() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + encryptedKey, err := encrypted.Encrypt(keyDER, nil) + require.NoError(t, err) + + signer, err := signing.NewSigner(context.Background(), signing.SignerOptions{ + KeyRef: base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: signver.PrivateKeyPemType, Bytes: encryptedKey})), + CertRef: base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})), + }) + require.NoError(t, err) + return signer +} + +func newSignFilesTestStage(signer *signing.Signer, paths ...string) *SignFilesStage { + imageConfig := &config.StapelImageBase{} + for _, p := range paths { + imageConfig.Sign = append(imageConfig.Sign, &config.Sign{Path: p, After: "install"}) + } + options := signing.NewFilesSigningOptions(signer) + options.Enabled = true + return GenerateSignFilesAfterInstallStage(imageConfig, options, &BaseStageOptions{}) +} + +func TestSignFilesStage_DigestChangesWithPathsAndCertificate(t *testing.T) { + ctx := context.Background() + signerA := newSignFilesTestSigner(t, "a") + signerB := newSignFilesTestSigner(t, "b") + + base, err := newSignFilesTestStage(signerA, "/out/x").GetDependencies(ctx, nil, nil, nil, nil, nil) + require.NoError(t, err) + + same, err := newSignFilesTestStage(signerA, "/out/x").GetDependencies(ctx, nil, nil, nil, nil, nil) + require.NoError(t, err) + require.Equal(t, base, same, "the digest must be stable for the same paths and signer") + + otherPath, err := newSignFilesTestStage(signerA, "/out/y").GetDependencies(ctx, nil, nil, nil, nil, nil) + require.NoError(t, err) + require.NotEqual(t, base, otherPath, "a different signed path must change the digest") + + otherCert, err := newSignFilesTestStage(signerB, "/out/x").GetDependencies(ctx, nil, nil, nil, nil, nil) + require.NoError(t, err) + require.NotEqual(t, base, otherCert, "a different signing certificate must change the digest") + + content, err := newSignFilesTestStage(signerA, "/out/x").GetContentDependencies(ctx, nil, nil) + require.NoError(t, err) + require.Equal(t, base, content, "the content digest must be the same input set") +} + +func TestSignFilesStage_NotGeneratedWithoutMatchingDirective(t *testing.T) { + signer := newSignFilesTestSigner(t, "a") + + imageConfig := &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", Before: "install"}}} + options := signing.NewFilesSigningOptions(signer) + options.Enabled = true + require.Nil(t, GenerateSignFilesAfterInstallStage(imageConfig, options, &BaseStageOptions{})) + require.NotNil(t, GenerateSignFilesBeforeInstallStage(imageConfig, options, &BaseStageOptions{})) + + options.Enabled = false + require.Nil(t, GenerateSignFilesBeforeInstallStage(imageConfig, options, &BaseStageOptions{})) +} diff --git a/test/e2e/build/sign_files_test.go b/test/e2e/build/sign_files_test.go index 1905a38d98..8c35fbbe95 100644 --- a/test/e2e/build/sign_files_test.go +++ b/test/e2e/build/sign_files_test.go @@ -113,12 +113,17 @@ var _ = Describe("sign directive", Label("e2e", "sign-files", "simple"), func() By("rebuilding with the same key reuses the stage") rebuildOut := werfProject.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: signArgs}}) - Expect(rebuildOut).NotTo(ContainSubstring("Building stage sysext/signFilesAfterInstall")) + Expect(rebuildOut).To(ContainSubstring("Use previously built image for sysext")) + Expect(rebuildOut).NotTo(ContainSubstring("Building stage")) }, backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ ContainerBackendMode: "docker", WithLocalRepo: true, }}), + backendEntry("with repo using Native Rootless", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ + ContainerBackendMode: "native-rootless", + WithLocalRepo: true, + }}), ) DescribeTable("keeps the build offline under build.sbom.enable and still signs", From f61790c3038162ecff3baa8b4f5d743e2b9a0c8e Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Wed, 14 Oct 2026 18:03:44 +0300 Subject: [PATCH 07/22] feat(config): describe the sign directive in the werf.yaml JSON schema Signed-off-by: Radmir Khurum --- pkg/config/werf_schema_test.go | 34 +++++++++++++++++++++++++++ schemas/werf.json | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/pkg/config/werf_schema_test.go b/pkg/config/werf_schema_test.go index 7cde66e47f..170ecccafc 100644 --- a/pkg/config/werf_schema_test.go +++ b/pkg/config/werf_schema_test.go @@ -661,6 +661,40 @@ import: - from: builder before: beforeInstall add: /app +`), + Entry("sign without path", ` +image: app +from: alpine +sign: +- after: install +`), + Entry("sign without stage", ` +image: app +from: alpine +sign: +- path: /out/x +`), + Entry("sign with both before and after", ` +image: app +from: alpine +sign: +- path: /out/x + before: install + after: install +`), + Entry("sign with unknown stage", ` +image: app +from: alpine +sign: +- path: /out/x + after: beforeInstall +`), + Entry("sign with relative path", ` +image: app +from: alpine +sign: +- path: out/x + after: install `), Entry("import with empty from", ` image: app diff --git a/schemas/werf.json b/schemas/werf.json index ec0bd99106..69988fb722 100644 --- a/schemas/werf.json +++ b/schemas/werf.json @@ -1506,6 +1506,41 @@ } ] }, + "sign": { + "description": "Signs a file the image produces at a user stage boundary and lays the detached PKCS#7 signature down next to it as `.p7s`. Exactly one of `before` or `after` must be set.\nhttps://werf.io/docs/latest/usage/build/stapel/imports.html#signing-files-inside-the-image", + "type": "object", + "additionalProperties": false, + "required": [ + "path" + ], + "properties": { + "path": { + "description": "The absolute path of the file to sign.", + "type": "string", + "pattern": "^/" + }, + "before": { + "description": "The stage before which to sign the file.", + "$ref": "#/definitions/stageName" + }, + "after": { + "description": "The stage after which to sign the file.", + "$ref": "#/definitions/stageName" + } + }, + "oneOf": [ + { + "required": [ + "before" + ] + }, + { + "required": [ + "after" + ] + } + ] + }, "stapelImage": { "description": "Stapel image section: builds an image with werf's own assembly instructions. Define as many as you need.\nhttps://werf.io/docs/latest/usage/build/stapel/base.html", "type": "object", @@ -1573,6 +1608,13 @@ "$ref": "#/definitions/import" } }, + "sign": { + "description": "Files to sign inside the image.\nhttps://werf.io/docs/latest/usage/build/stapel/imports.html#signing-files-inside-the-image", + "type": "array", + "items": { + "$ref": "#/definitions/sign" + } + }, "dependencies": { "description": "Dependency images for the current image.", "type": "array", From e1df4b34fb170a84817d9cc81a727984cae05078 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 15 Oct 2026 11:08:25 +0300 Subject: [PATCH 08/22] fix(buildah): resolve the file to sign inside the image root, not the 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 --- docs/pages_en/usage/build/stapel/imports.md | 2 +- docs/pages_ru/usage/build/stapel/imports.md | 2 +- pkg/build/signing/files_signing.go | 2 +- pkg/build/stage/sign_files.go | 2 +- pkg/build/stage/sign_files_test.go | 129 +++++++++--------- pkg/container_backend/buildah_image_reader.go | 21 ++- .../buildah_image_reader_test.go | 47 +++++++ 7 files changed, 132 insertions(+), 73 deletions(-) create mode 100644 pkg/container_backend/buildah_image_reader_test.go diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index 0cdc6dd2ee..3b360054d2 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -124,4 +124,4 @@ The key and the certificate are never part of `werf.yaml`; they come from the co A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. With `hashivault://` the private key never leaves the KMS: the stage runs none of the image's commands and needs no network, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). -The stage is cached like any other. Its digest covers the signed paths and the signing certificate chain, so rotating the key rebuilds the signature and the stages after it, while a rebuild with the same key reuses the cached layer. +The stage is cached like any other. Its digest covers the signed paths and the signing certificate, so rotating the key rebuilds the signature and the stages after it, while a rebuild with the same key reuses the cached layer. Only the signing certificate is embedded in the `.p7s`; intermediates do not take part in the signature or the digest. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index 1d5a462068..3072cb013c 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -124,4 +124,4 @@ werf читает файл из собранного к этому момент Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. С `hashivault://` приватный ключ не покидает KMS: стадия не выполняет команд образа и не нуждается в сети, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). -Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути и цепочку сертификатов подписанта, поэтому ротация ключа пересобирает подпись и последующие стадии, а повторная сборка с тем же ключом переиспользует закэшированный слой. +Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути и сертификат подписанта, поэтому ротация ключа пересобирает подпись и последующие стадии, а повторная сборка с тем же ключом переиспользует закэшированный слой. В `.p7s` встраивается только сертификат подписанта; промежуточные сертификаты не участвуют ни в подписи, ни в дайджесте. diff --git a/pkg/build/signing/files_signing.go b/pkg/build/signing/files_signing.go index 43ff52cf29..d067951bf5 100644 --- a/pkg/build/signing/files_signing.go +++ b/pkg/build/signing/files_signing.go @@ -113,7 +113,7 @@ func (s *digestSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) options.WithCryptoSignerOpts(crypto.SHA256), ) if err != nil { - return nil, err + return nil, fmt.Errorf("sign digest with the files signing key: %w", err) } s.digest = digest s.signature = sig diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index 72c23e4e99..fb20b7f608 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -91,7 +91,7 @@ func (s *SignFilesStage) digest() string { for _, sign := range s.signs { args = append(args, "Path", sign.Path) } - args = append(args, "Cert", s.options.Signer().Cert(), "Chain", s.options.Signer().Chain()) + args = append(args, "Cert", s.options.Signer().Cert()) return util.Sha256Hash(args...) } diff --git a/pkg/build/stage/sign_files_test.go b/pkg/build/stage/sign_files_test.go index 5fc93b5320..76a3fcaabf 100644 --- a/pkg/build/stage/sign_files_test.go +++ b/pkg/build/stage/sign_files_test.go @@ -9,88 +9,83 @@ import ( "encoding/base64" "encoding/pem" "math/big" - "testing" "time" "github.com/deckhouse/delivery-kit-sdk/pkg/signver" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/secure-systems-lab/go-securesystemslib/encrypted" - "github.com/stretchr/testify/require" "github.com/werf/werf/v3/pkg/build/signing" "github.com/werf/werf/v3/pkg/config" ) -func newSignFilesTestSigner(t *testing.T, cn string) *signing.Signer { - t.Helper() - - key, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: cn}, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, +var _ = Describe("SignFilesStage", func() { + newSigner := func(cn string) *signing.Signer { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + Expect(err).NotTo(HaveOccurred()) + encryptedKey, err := encrypted.Encrypt(keyDER, nil) + Expect(err).NotTo(HaveOccurred()) + + signer, err := signing.NewSigner(context.Background(), signing.SignerOptions{ + KeyRef: base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: signver.PrivateKeyPemType, Bytes: encryptedKey})), + CertRef: base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})), + }) + Expect(err).NotTo(HaveOccurred()) + return signer } - certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) - require.NoError(t, err) - keyDER, err := x509.MarshalPKCS8PrivateKey(key) - require.NoError(t, err) - encryptedKey, err := encrypted.Encrypt(keyDER, nil) - require.NoError(t, err) - - signer, err := signing.NewSigner(context.Background(), signing.SignerOptions{ - KeyRef: base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: signver.PrivateKeyPemType, Bytes: encryptedKey})), - CertRef: base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})), - }) - require.NoError(t, err) - return signer -} -func newSignFilesTestStage(signer *signing.Signer, paths ...string) *SignFilesStage { - imageConfig := &config.StapelImageBase{} - for _, p := range paths { - imageConfig.Sign = append(imageConfig.Sign, &config.Sign{Path: p, After: "install"}) + enabledOptions := func(signer *signing.Signer) signing.FilesSigningOptions { + options := signing.NewFilesSigningOptions(signer) + options.Enabled = true + return options } - options := signing.NewFilesSigningOptions(signer) - options.Enabled = true - return GenerateSignFilesAfterInstallStage(imageConfig, options, &BaseStageOptions{}) -} - -func TestSignFilesStage_DigestChangesWithPathsAndCertificate(t *testing.T) { - ctx := context.Background() - signerA := newSignFilesTestSigner(t, "a") - signerB := newSignFilesTestSigner(t, "b") - - base, err := newSignFilesTestStage(signerA, "/out/x").GetDependencies(ctx, nil, nil, nil, nil, nil) - require.NoError(t, err) - same, err := newSignFilesTestStage(signerA, "/out/x").GetDependencies(ctx, nil, nil, nil, nil, nil) - require.NoError(t, err) - require.Equal(t, base, same, "the digest must be stable for the same paths and signer") - - otherPath, err := newSignFilesTestStage(signerA, "/out/y").GetDependencies(ctx, nil, nil, nil, nil, nil) - require.NoError(t, err) - require.NotEqual(t, base, otherPath, "a different signed path must change the digest") + digestOf := func(signer *signing.Signer, paths ...string) string { + imageConfig := &config.StapelImageBase{} + for _, p := range paths { + imageConfig.Sign = append(imageConfig.Sign, &config.Sign{Path: p, After: "install"}) + } + stage := GenerateSignFilesAfterInstallStage(imageConfig, enabledOptions(signer), &BaseStageOptions{}) + Expect(stage).NotTo(BeNil()) + + digest, err := stage.GetDependencies(context.Background(), nil, nil, nil, nil, nil) + Expect(err).NotTo(HaveOccurred()) + contentDigest, err := stage.GetContentDependencies(context.Background(), nil, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(contentDigest).To(Equal(digest), "the content digest must be the same input set") + return digest + } - otherCert, err := newSignFilesTestStage(signerB, "/out/x").GetDependencies(ctx, nil, nil, nil, nil, nil) - require.NoError(t, err) - require.NotEqual(t, base, otherCert, "a different signing certificate must change the digest") + It("keeps the digest stable for the same paths and signer and changes it with either", func() { + signerA := newSigner("a") + signerB := newSigner("b") - content, err := newSignFilesTestStage(signerA, "/out/x").GetContentDependencies(ctx, nil, nil) - require.NoError(t, err) - require.Equal(t, base, content, "the content digest must be the same input set") -} + base := digestOf(signerA, "/out/x") + Expect(digestOf(signerA, "/out/x")).To(Equal(base)) + Expect(digestOf(signerA, "/out/y")).NotTo(Equal(base), "a different signed path must change the digest") + Expect(digestOf(signerB, "/out/x")).NotTo(Equal(base), "a different signing certificate must change the digest") + }) -func TestSignFilesStage_NotGeneratedWithoutMatchingDirective(t *testing.T) { - signer := newSignFilesTestSigner(t, "a") + It("is generated only for the position the directive names and only when enabled", func() { + imageConfig := &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", Before: "install"}}} + options := enabledOptions(newSigner("a")) - imageConfig := &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", Before: "install"}}} - options := signing.NewFilesSigningOptions(signer) - options.Enabled = true - require.Nil(t, GenerateSignFilesAfterInstallStage(imageConfig, options, &BaseStageOptions{})) - require.NotNil(t, GenerateSignFilesBeforeInstallStage(imageConfig, options, &BaseStageOptions{})) + Expect(GenerateSignFilesAfterInstallStage(imageConfig, options, &BaseStageOptions{})).To(BeNil()) + Expect(GenerateSignFilesBeforeInstallStage(imageConfig, options, &BaseStageOptions{})).NotTo(BeNil()) - options.Enabled = false - require.Nil(t, GenerateSignFilesBeforeInstallStage(imageConfig, options, &BaseStageOptions{})) -} + options.Enabled = false + Expect(GenerateSignFilesBeforeInstallStage(imageConfig, options, &BaseStageOptions{})).To(BeNil()) + }) +}) diff --git a/pkg/container_backend/buildah_image_reader.go b/pkg/container_backend/buildah_image_reader.go index 168d5cd8c3..d5216acf1a 100644 --- a/pkg/container_backend/buildah_image_reader.go +++ b/pkg/container_backend/buildah_image_reader.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" "github.com/werf/logboek" ) @@ -30,7 +29,7 @@ func (backend *BuildahBackend) ReadFileFromImage(ctx context.Context, imageRef, } }() - data, err := os.ReadFile(filepath.Join(container.RootMount, path)) + data, err := readRegularFileFromRoot(container.RootMount, path) if err != nil { return nil, fmt.Errorf("read %s from image %q: %w", path, imageRef, err) } @@ -38,6 +37,24 @@ func (backend *BuildahBackend) ReadFileFromImage(ctx context.Context, imageRef, return data, nil } +// readRegularFileFromRoot reads path inside the mounted container root, following +// symlinks against that root rather than the host, and accepts regular files only, +// matching what the Docker reader yields for the same path. +func readRegularFileFromRoot(rootMount, path string) ([]byte, error) { + resolvedPath, err := resolveContainerRootPath(rootMount, path) + if err != nil { + return nil, err + } + info, err := os.Lstat(resolvedPath) + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("not a regular file") + } + return os.ReadFile(resolvedPath) +} + // OpenImageReader is intentionally unsupported on the Buildah backend: the only caller is // the SBOM directory-scan flow, which rejects Buildah upfront (see the SBOM guard in the // conveyor), so a ContainerBackend backed by Buildah never reaches an image reader. diff --git a/pkg/container_backend/buildah_image_reader_test.go b/pkg/container_backend/buildah_image_reader_test.go new file mode 100644 index 0000000000..74bfb47a79 --- /dev/null +++ b/pkg/container_backend/buildah_image_reader_test.go @@ -0,0 +1,47 @@ +package container_backend + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("BuildahBackend ReadFileFromImage path resolution", func() { + It("reads a regular file by its container path", func() { + root := GinkgoT().TempDir() + Expect(os.MkdirAll(filepath.Join(root, "out"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(root, "out", "x"), []byte("content"), 0o644)).To(Succeed()) + + Expect(readRegularFileFromRoot(root, "/out/x")).To(Equal([]byte("content"))) + }) + + It("follows an absolute symlink against the container root, not the host", func() { + base := GinkgoT().TempDir() + root := filepath.Join(base, "root") + Expect(os.MkdirAll(filepath.Join(root, "out"), 0o755)).To(Succeed()) + Expect(os.MkdirAll(filepath.Join(root, "etc"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(root, "etc", "secret"), []byte("container"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(base, "secret"), []byte("host"), 0o644)).To(Succeed()) + Expect(os.Symlink("/etc/secret", filepath.Join(root, "out", "x"))).To(Succeed()) + Expect(os.Symlink("../../secret", filepath.Join(root, "out", "y"))).To(Succeed()) + + Expect(readRegularFileFromRoot(root, "/out/x")).To(Equal([]byte("container"))) + + _, err := readRegularFileFromRoot(root, "/out/y") + Expect(err).To(MatchError(os.ErrNotExist)) + }) + + It("rejects anything but a regular file", func() { + root := GinkgoT().TempDir() + Expect(os.MkdirAll(filepath.Join(root, "out"), 0o755)).To(Succeed()) + Expect(os.Symlink("/nowhere", filepath.Join(root, "out", "dangling"))).To(Succeed()) + + _, err := readRegularFileFromRoot(root, "/out") + Expect(err).To(MatchError("not a regular file")) + + _, err = readRegularFileFromRoot(root, "/out/dangling") + Expect(err).To(MatchError(os.ErrNotExist)) + }) +}) From 6cd727df4d93fe91b6cd8b7976c107d722857a07 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Fri, 16 Oct 2026 09:44:30 +0300 Subject: [PATCH 09/22] fix(build): run the sign stages with network none on every build 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 --- docs/_data/werf_yaml.yml | 4 +-- docs/pages_en/usage/build/stapel/imports.md | 4 ++- docs/pages_ru/usage/build/stapel/imports.md | 4 ++- pkg/build/image/stapel_test.go | 25 +++++++++++++------ pkg/build/stage/base.go | 16 +++++++----- pkg/build/stage/sign_files.go | 9 ++++--- .../_fixtures/sign_files/state_sbom/werf.yaml | 3 ++- 7 files changed, 43 insertions(+), 22 deletions(-) diff --git a/docs/_data/werf_yaml.yml b/docs/_data/werf_yaml.yml index a07a368162..d9243cad7e 100644 --- a/docs/_data/werf_yaml.yml +++ b/docs/_data/werf_yaml.yml @@ -947,12 +947,12 @@ sections: 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" + ru: "Стадия, перед которой подписать файл. Поддерживаются только стадии install и setup" - 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" + ru: "Стадия, после которой подписать файл. Поддерживаются только стадии install и setup" - name: dependencies description: en: "Dependencies images for current image" diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index 3b360054d2..322ec45d74 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -122,6 +122,8 @@ The key and the certificate are never part of `werf.yaml`; they come from the co | `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | the signing certificate: a path to a PEM file or a base64-encoded PEM | | `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | the intermediate certificates: a path to a PEM file or a base64-encoded PEM | -A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. With `hashivault://` the private key never leaves the KMS: the stage runs none of the image's commands and needs no network, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). +A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. Commands that do not build images (`lint`, `stages copy`, `sbom get`) have no `--sign-files` flag and treat it as off: they accept such a `werf.yaml` without a key. + +With `hashivault://` the private key never leaves the KMS. The certificate is not fetched from the KMS: when it is issued by a PKI (Vault PKI, for example), the pipeline retrieves the PEM itself and passes it as a path or base64. The stage container always runs with `network: none`, whatever the image's `network` setting, and none of the image's commands run in it, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). The stage is cached like any other. Its digest covers the signed paths and the signing certificate, so rotating the key rebuilds the signature and the stages after it, while a rebuild with the same key reuses the cached layer. Only the signing certificate is embedded in the `.p7s`; intermediates do not take part in the signature or the digest. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index 3072cb013c..043d330bf2 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -122,6 +122,8 @@ werf читает файл из собранного к этому момент | `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | сертификат подписанта: путь к PEM-файлу или PEM в base64 | | `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | промежуточные сертификаты: путь к PEM-файлу или PEM в base64 | -Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. С `hashivault://` приватный ключ не покидает KMS: стадия не выполняет команд образа и не нуждается в сети, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). +Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. Команды, которые не собирают образы (`lint`, `stages copy`, `sbom get`), не имеют флага `--sign-files` и считают его выключенным: такой `werf.yaml` они принимают без ключа. + +С `hashivault://` приватный ключ не покидает KMS. Сертификат из KMS не запрашивается: если его выпускает PKI (например, Vault PKI), пайплайн сам получает PEM и передаёт его путём или в base64. Контейнер стадии всегда запускается с `network: none` независимо от настройки `network` образа, и команды образа в нём не выполняются, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути и сертификат подписанта, поэтому ротация ключа пересобирает подпись и последующие стадии, а повторная сборка с тем же ключом переиспользует закэшированный слой. В `.p7s` встраивается только сертификат подписанта; промежуточные сертификаты не участвуют ни в подписи, ни в дайджесте. diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index 17655882de..7072143093 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -177,16 +177,25 @@ sign: } }) - It("keeps shell stages offline under build.sbom.enable while the sign stages need no network either", func() { - image := newImage(context.Background(), "build:\n sbom:\n enable: true\n standard: cyclonedx@1.6", enabled()) + DescribeTable("runs the sign stages without network", + func(sbomBlock string) { + image := newImage(context.Background(), sbomBlock, enabled()) - for _, s := range image.GetStages() { - if s.Name() == stage.From { - continue + for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { + s := image.GetStage(name) + Expect(s).NotTo(BeNil(), string(name)) + Expect(s.(*stage.SignFilesStage).Network()).To(Equal("none"), string(name)) } - nn, ok := s.(interface{ NeedsNetwork() bool }) - Expect(ok).To(BeTrue(), string(s.Name())) - Expect(nn.NeedsNetwork()).To(BeFalse(), string(s.Name())) + }, + Entry("build.sbom absent", ""), + Entry("build.sbom.enable true", "build:\n sbom:\n enable: true\n standard: cyclonedx@1.6"), + ) + + It("keeps shell stages offline under build.sbom.enable", func() { + image := newImage(context.Background(), "build:\n sbom:\n enable: true\n standard: cyclonedx@1.6", enabled()) + + for _, name := range []stage.StageName{stage.Install, stage.Setup} { + Expect(image.GetStage(name).(interface{ Network() string }).Network()).To(Equal("none"), string(name)) } }) }) diff --git a/pkg/build/stage/base.go b/pkg/build/stage/base.go index 8553088388..5c6865888e 100644 --- a/pkg/build/stage/base.go +++ b/pkg/build/stage/base.go @@ -135,6 +135,15 @@ func (s *BaseStage) NeedsNetwork() bool { return s.needsNetwork } +// Network returns the networking mode the stage container will run with: the +// override when one is set, otherwise the mode the image config asked for. +func (s *BaseStage) Network() string { + if s.networkOverride != "" { + return s.networkOverride + } + return s.network +} + func (s *BaseStage) IsMutable() bool { return false } @@ -292,12 +301,7 @@ func (s *BaseStage) PrepareImage(ctx context.Context, c Conveyor, cb container_b s.addProjectRepoCommitLabel(ctx, c, cb, stageImage) - network := s.network - if s.networkOverride != "" { - network = s.networkOverride - } - - if network != "" { + if network := s.Network(); network != "" { if c.UseLegacyStapelBuilder(cb) { stageImage.Builder.LegacyStapelStageBuilder().Container().RunOptions().AddNetwork(network) } else { diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index fb20b7f608..55bb561e1a 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -71,11 +71,13 @@ func generateSignFilesStage(imageBaseConfig *config.StapelImageBase, name StageN return nil } - return &SignFilesStage{ + stage := &SignFilesStage{ BaseStage: NewBaseStage(name, baseStageOptions), signs: signs, options: options, } + stage.SetNetworkOverride("none") + return stage } func (s *SignFilesStage) GetDependencies(_ context.Context, _ Conveyor, _ container_backend.ContainerBackend, _, _ *StageImage, _ container_backend.BuildContextArchiver) (string, error) { @@ -115,8 +117,9 @@ func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb contai var signature []byte if err := logboek.Context(ctx).Default().LogProcess("Signing %s", sign.Path).DoError(func() error { - signature, err = signing.SignDetachedPKCS7(ctx, content, s.options.Signer()) - return err + var signErr error + signature, signErr = signing.SignDetachedPKCS7(ctx, content, s.options.Signer()) + return signErr }); err != nil { return fmt.Errorf("sign %s of image %q: %w", sign.Path, s.imageName, err) } diff --git a/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml b/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml index b8020b253d..08309be6ee 100644 --- a/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml +++ b/test/e2e/build/_fixtures/sign_files/state_sbom/werf.yaml @@ -13,7 +13,8 @@ shell: install: - mkdir -p /out - printf '%s' 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b > /out/ext.roothash - - if wget -q -T 3 -O /dev/null http://1.1.1.1 2>/dev/null; then echo 'network is reachable' >&2; exit 1; fi + - command -v ping >/dev/null || { echo 'network probe tool is missing' >&2; exit 1; } + - if ping -c1 -W2 1.1.1.1 >/dev/null 2>&1; then echo 'network is reachable' >&2; exit 1; fi setup: - test -s /out/ext.roothash.p7s sign: From 80735a9511bb6e2e08dd1bf866bf8cffbf0e440d Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Fri, 16 Oct 2026 09:49:48 +0300 Subject: [PATCH 10/22] feat(build): point the files signing key at its own Vault mount and login 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 --- cmd/werf/common/cmd_data.go | 13 +- cmd/werf/common/signature.go | 27 ++++ docs/_includes/reference/cli/werf_build.md | 16 +++ .../reference/cli/werf_bundle_publish.md | 16 +++ .../reference/cli/werf_compose_config.md | 16 +++ .../reference/cli/werf_compose_down.md | 16 +++ .../reference/cli/werf_compose_run.md | 16 +++ .../reference/cli/werf_compose_up.md | 16 +++ docs/_includes/reference/cli/werf_converge.md | 16 +++ docs/_includes/reference/cli/werf_export.md | 16 +++ docs/_includes/reference/cli/werf_kube_run.md | 16 +++ docs/_includes/reference/cli/werf_plan.md | 16 +++ docs/_includes/reference/cli/werf_render.md | 16 +++ docs/_includes/reference/cli/werf_run.md | 16 +++ docs/pages_en/usage/build/stapel/imports.md | 7 + docs/pages_ru/usage/build/stapel/imports.md | 7 + pkg/build/signing/signer.go | 31 +++- pkg/build/signing/signer_vault_test.go | 136 ++++++++++++++++++ 18 files changed, 407 insertions(+), 6 deletions(-) create mode 100644 pkg/build/signing/signer_vault_test.go diff --git a/cmd/werf/common/cmd_data.go b/cmd/werf/common/cmd_data.go index 3b5707ff02..08212c9076 100644 --- a/cmd/werf/common/cmd_data.go +++ b/cmd/werf/common/cmd_data.go @@ -123,10 +123,15 @@ type CmdData struct { SignCert *string SignIntermediates *string - SignFiles *bool - SignFilesKey *string - SignFilesCert *string - SignFilesIntermediates *string + SignFiles *bool + SignFilesKey *string + SignFilesCert *string + SignFilesIntermediates *string + SignFilesVaultAddr *string + SignFilesVaultTransitPath *string + SignFilesVaultAuthJWT *string + SignFilesVaultAuthRole *string + SignFilesVaultAuthPath *string VerifyManifest *bool VerifyRoots *[]string diff --git a/cmd/werf/common/signature.go b/cmd/werf/common/signature.go index ab9beed715..2aa1e77377 100644 --- a/cmd/werf/common/signature.go +++ b/cmd/werf/common/signature.go @@ -66,6 +66,26 @@ Disable to build such images without signatures`) cmdData.SignFilesIntermediates = new(string) cmd.Flags().StringVarP(cmdData.SignFilesIntermediates, "sign-files-intermediates", "", os.Getenv("WERF_SIGN_FILES_INTERMEDIATES"), "The intermediate certificates of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES)") + + cmdData.SignFilesVaultAddr = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesVaultAddr, "sign-files-vault-addr", "", os.Getenv("WERF_SIGN_FILES_VAULT_ADDR"), + "The Vault address a hashivault:// files signing key is resolved against (default $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR)") + + cmdData.SignFilesVaultTransitPath = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesVaultTransitPath, "sign-files-vault-transit-path", "", os.Getenv("WERF_SIGN_FILES_VAULT_TRANSIT_PATH"), + "The Vault transit engine mount of the files signing key (default $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH)") + + cmdData.SignFilesVaultAuthJWT = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesVaultAuthJWT, "sign-files-vault-auth-jwt", "", os.Getenv("WERF_SIGN_FILES_VAULT_AUTH_JWT"), + "The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default $WERF_SIGN_FILES_VAULT_AUTH_JWT)") + + cmdData.SignFilesVaultAuthRole = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesVaultAuthRole, "sign-files-vault-auth-role", "", os.Getenv("WERF_SIGN_FILES_VAULT_AUTH_ROLE"), + "The Vault JWT role for --sign-files-vault-auth-jwt (default $WERF_SIGN_FILES_VAULT_AUTH_ROLE)") + + cmdData.SignFilesVaultAuthPath = new(string) + cmd.Flags().StringVarP(cmdData.SignFilesVaultAuthPath, "sign-files-vault-auth-path", "", os.Getenv("WERF_SIGN_FILES_VAULT_AUTH_PATH"), + "The Vault JWT auth mount for --sign-files-vault-auth-jwt (default $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt)") } func GetSignFiles(cmdData *CmdData) bool { @@ -211,6 +231,13 @@ func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfCon KeyRef: keyRef, CertRef: certRef, IntermediatesRef: lo.FromPtr(commonCmdData.SignFilesIntermediates), + Vault: signing.VaultOptions{ + Address: lo.FromPtr(commonCmdData.SignFilesVaultAddr), + TransitPath: lo.FromPtr(commonCmdData.SignFilesVaultTransitPath), + AuthJWT: lo.FromPtr(commonCmdData.SignFilesVaultAuthJWT), + AuthRole: lo.FromPtr(commonCmdData.SignFilesVaultAuthRole), + AuthPath: lo.FromPtr(commonCmdData.SignFilesVaultAuthPath), + }, }) if err != nil { return signing.FilesSigningOptions{}, fmt.Errorf("create files signer: %w", err) diff --git a/docs/_includes/reference/cli/werf_build.md b/docs/_includes/reference/cli/werf_build.md index ca5642c96a..9c32190e24 100644 --- a/docs/_includes/reference/cli/werf_build.md +++ b/docs/_includes/reference/cli/werf_build.md @@ -296,6 +296,22 @@ werf build [IMAGE_NAME...] [options] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_bundle_publish.md b/docs/_includes/reference/cli/werf_bundle_publish.md index ec78ebd082..3b8461987a 100644 --- a/docs/_includes/reference/cli/werf_bundle_publish.md +++ b/docs/_includes/reference/cli/werf_bundle_publish.md @@ -344,6 +344,22 @@ werf bundle publish [IMAGE_NAME...] [options] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_config.md b/docs/_includes/reference/cli/werf_compose_config.md index dc37dbfcf5..75b1f39ab6 100644 --- a/docs/_includes/reference/cli/werf_compose_config.md +++ b/docs/_includes/reference/cli/werf_compose_config.md @@ -288,6 +288,22 @@ werf compose config [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_down.md b/docs/_includes/reference/cli/werf_compose_down.md index 73a6999977..946da9edb6 100644 --- a/docs/_includes/reference/cli/werf_compose_down.md +++ b/docs/_includes/reference/cli/werf_compose_down.md @@ -281,6 +281,22 @@ werf compose down [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_run.md b/docs/_includes/reference/cli/werf_compose_run.md index 0eddc6c0d9..ecd19fda0e 100644 --- a/docs/_includes/reference/cli/werf_compose_run.md +++ b/docs/_includes/reference/cli/werf_compose_run.md @@ -278,6 +278,22 @@ werf compose run [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_up.md b/docs/_includes/reference/cli/werf_compose_up.md index d4f47d6aca..0bf1cb3056 100644 --- a/docs/_includes/reference/cli/werf_compose_up.md +++ b/docs/_includes/reference/cli/werf_compose_up.md @@ -289,6 +289,22 @@ werf compose up [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] [ --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_converge.md b/docs/_includes/reference/cli/werf_converge.md index c3d3adee4f..f91478f3df 100644 --- a/docs/_includes/reference/cli/werf_converge.md +++ b/docs/_includes/reference/cli/werf_converge.md @@ -537,6 +537,22 @@ werf converge --repo registry.mydomain.com/web --env production --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_export.md b/docs/_includes/reference/cli/werf_export.md index 98b13d95c2..7d75b449a3 100644 --- a/docs/_includes/reference/cli/werf_export.md +++ b/docs/_includes/reference/cli/werf_export.md @@ -258,6 +258,22 @@ werf export [IMAGE_NAME...] [options] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_kube_run.md b/docs/_includes/reference/cli/werf_kube_run.md index 3f20d531c3..a14b11d5e4 100644 --- a/docs/_includes/reference/cli/werf_kube_run.md +++ b/docs/_includes/reference/cli/werf_kube_run.md @@ -325,6 +325,22 @@ werf kube-run [options] [IMAGE_NAME] [-- COMMAND ARG...] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_plan.md b/docs/_includes/reference/cli/werf_plan.md index e02a40c176..f4a3c1793f 100644 --- a/docs/_includes/reference/cli/werf_plan.md +++ b/docs/_includes/reference/cli/werf_plan.md @@ -522,6 +522,22 @@ werf plan --repo registry.mydomain.com/web --env production --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_render.md b/docs/_includes/reference/cli/werf_render.md index 771e1f2631..ef267827b8 100644 --- a/docs/_includes/reference/cli/werf_render.md +++ b/docs/_includes/reference/cli/werf_render.md @@ -423,6 +423,22 @@ werf render [IMAGE_NAME...] [options] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_run.md b/docs/_includes/reference/cli/werf_run.md index 14d39266a6..7a6cf955fa 100644 --- a/docs/_includes/reference/cli/werf_run.md +++ b/docs/_includes/reference/cli/werf_run.md @@ -231,6 +231,22 @@ werf run [options] [IMAGE_NAME] [-- COMMAND ARG...] --sign-files-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) + --sign-files-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) + --sign-files-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_FILES_VAULT_AUTH_JWT) + --sign-files-vault-auth-path="" + The Vault JWT auth mount for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) + --sign-files-vault-auth-role="" + The Vault JWT role for --sign-files-vault-auth-jwt (default + $WERF_SIGN_FILES_VAULT_AUTH_ROLE) + --sign-files-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index 322ec45d74..d44ee99595 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -121,6 +121,13 @@ The key and the certificate are never part of `werf.yaml`; they come from the co | `--sign-files-key` | `WERF_SIGN_FILES_KEY` | the private key: a path to a PEM file, a base64-encoded PEM, or `hashivault://[KEY]` | | `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | the signing certificate: a path to a PEM file or a base64-encoded PEM | | `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | the intermediate certificates: a path to a PEM file or a base64-encoded PEM | +| `--sign-files-vault-addr` | `WERF_SIGN_FILES_VAULT_ADDR` | the Vault address a `hashivault://` key is resolved against; unset → `VAULT_ADDR` | +| `--sign-files-vault-transit-path` | `WERF_SIGN_FILES_VAULT_TRANSIT_PATH` | the transit engine mount of the key; unset → `TRANSIT_SECRET_ENGINE_PATH` | +| `--sign-files-vault-auth-jwt` | `WERF_SIGN_FILES_VAULT_AUTH_JWT` | the JWT to log in to Vault with; unset → the authentication `--sign-key` uses, from the environment | +| `--sign-files-vault-auth-role` | `WERF_SIGN_FILES_VAULT_AUTH_ROLE` | the Vault JWT role for that JWT | +| `--sign-files-vault-auth-path` | `WERF_SIGN_FILES_VAULT_AUTH_PATH` | the JWT auth mount for that JWT; unset → `jwt` | + +The `--sign-files-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`; each option left unset falls back to what `--sign-key` would use. A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. Commands that do not build images (`lint`, `stages copy`, `sbom get`) have no `--sign-files` flag and treat it as off: they accept such a `werf.yaml` without a key. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index 043d330bf2..228019ce19 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -121,6 +121,13 @@ werf читает файл из собранного к этому момент | `--sign-files-key` | `WERF_SIGN_FILES_KEY` | приватный ключ: путь к PEM-файлу, PEM в base64 или `hashivault://[KEY]` | | `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | сертификат подписанта: путь к PEM-файлу или PEM в base64 | | `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | промежуточные сертификаты: путь к PEM-файлу или PEM в base64 | +| `--sign-files-vault-addr` | `WERF_SIGN_FILES_VAULT_ADDR` | адрес Vault, в котором ищется ключ `hashivault://`; не задан → `VAULT_ADDR` | +| `--sign-files-vault-transit-path` | `WERF_SIGN_FILES_VAULT_TRANSIT_PATH` | mount transit-движка с ключом; не задан → `TRANSIT_SECRET_ENGINE_PATH` | +| `--sign-files-vault-auth-jwt` | `WERF_SIGN_FILES_VAULT_AUTH_JWT` | JWT для входа в Vault; не задан → та же аутентификация из окружения, что у `--sign-key` | +| `--sign-files-vault-auth-role` | `WERF_SIGN_FILES_VAULT_AUTH_ROLE` | роль Vault JWT для этого JWT | +| `--sign-files-vault-auth-path` | `WERF_SIGN_FILES_VAULT_AUTH_PATH` | mount JWT-аутентификации для этого JWT; не задан → `jwt` | + +Опции `--sign-files-vault-*` нужны, чтобы подписант файлов и подписант `--sign-key` могли жить в разных mount Vault и входить под разными ролями в рамках одного `werf build`; каждая незаданная опция берёт то, что использовал бы `--sign-key`. Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. Команды, которые не собирают образы (`lint`, `stages copy`, `sbom get`), не имеют флага `--sign-files` и считают его выключенным: такой `werf.yaml` они принимают без ключа. diff --git a/pkg/build/signing/signer.go b/pkg/build/signing/signer.go index 4401cc71b7..450640a4d6 100644 --- a/pkg/build/signing/signer.go +++ b/pkg/build/signing/signer.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/deckhouse/delivery-kit-sdk/pkg/signver" + "github.com/deckhouse/delivery-kit-sdk/pkg/signver/hashivault" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -15,12 +16,37 @@ type SignerOptions struct { KeyRef string CertRef string IntermediatesRef string + Vault VaultOptions } func (o SignerOptions) IsZero() bool { return o.KeyRef == "" || o.CertRef == "" } +// VaultOptions configures the Vault a hashivault:// key is resolved against. Every +// field is optional: an empty one falls back to the SDK's environment lookup, so a +// signer with zero VaultOptions behaves exactly as before. Setting AuthJWT switches +// authentication to that static JWT; AuthRole and AuthPath qualify it and are +// ignored without it. +type VaultOptions struct { + Address string + TransitPath string + AuthJWT string + AuthRole string + AuthPath string +} + +func (o VaultOptions) sdkOpts() hashivault.VaultOpts { + opts := hashivault.VaultOpts{ + Address: o.Address, + TransitSecretEnginePath: o.TransitPath, + } + if o.AuthJWT != "" { + opts.Auth = &hashivault.VaultAuth{JWT: &hashivault.JWTAuth{JWT: o.AuthJWT, Role: o.AuthRole, Path: o.AuthPath}} + } + return opts +} + type Signer struct { sv *signver.SignerVerifier @@ -76,8 +102,9 @@ func NewSigner(ctx context.Context, opts SignerOptions) (*Signer, error) { return &Signer{}, nil } sv, err := signver.NewSignerVerifier(ctx, opts.CertRef, opts.IntermediatesRef, signver.KeyOpts{ - KeyRef: opts.KeyRef, - PassFunc: cryptoutils.SkipPassword, + KeyRef: opts.KeyRef, + PassFunc: cryptoutils.SkipPassword, + SignerVerifierOpts: signver.SignerVerifierOpts{VaultOpts: opts.Vault.sdkOpts()}, }) if err != nil { return nil, fmt.Errorf("failed to create signer verifier: %w", err) diff --git a/pkg/build/signing/signer_vault_test.go b/pkg/build/signing/signer_vault_test.go new file mode 100644 index 0000000000..251ade6ca2 --- /dev/null +++ b/pkg/build/signing/signer_vault_test.go @@ -0,0 +1,136 @@ +package signing + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "net/http" + "net/http/httptest" + "strings" + "sync" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type fakeVaultLogin struct { + path string + role string + jwt string +} + +type fakeVault struct { + *httptest.Server + + publicKeyPEM string + mu sync.Mutex + logins []fakeVaultLogin + keyReads []string +} + +func newFakeVault(publicKeyPEM string) *fakeVault { + v := &fakeVault{publicKeyPEM: publicKeyPEM} + v.Server = httptest.NewServer(http.HandlerFunc(v.handle)) + return v +} + +func (v *fakeVault) handle(w http.ResponseWriter, r *http.Request) { + v.mu.Lock() + defer v.mu.Unlock() + + switch { + case strings.HasPrefix(r.URL.Path, "/v1/auth/"): + var body map[string]string + Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) + v.logins = append(v.logins, fakeVaultLogin{path: r.URL.Path, role: body["role"], jwt: body["jwt"]}) + fmt.Fprintf(w, `{"auth":{"client_token":"s.fake","lease_duration":3600}}`) + default: + v.keyReads = append(v.keyReads, r.URL.Path+"|"+r.Header.Get("X-Vault-Token")) + key := map[string]any{"public_key": v.publicKeyPEM, "name": "rsa-2048"} + Expect(json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{ + "latest_version": 1, + "keys": map[string]any{"1": key}, + }})).To(Succeed()) + } +} + +var _ = Describe("NewSigner with hashivault", func() { + var vault *fakeVault + var certRef string + + BeforeEach(func() { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + Expect(err).NotTo(HaveOccurred()) + vault = newFakeVault(string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}))) + DeferCleanup(vault.Close) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "vault-test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + certRef = base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})) + + GinkgoT().Setenv("VAULT_ADDR", "http://127.0.0.1:1") + GinkgoT().Setenv("TRANSIT_SECRET_ENGINE_PATH", "env-transit") + GinkgoT().Setenv("WERF_VAULT_AUTH_JWT", "env-jwt") + GinkgoT().Setenv("WERF_VAULT_AUTH_ROLE", "env-role") + GinkgoT().Setenv("WERF_VAULT_AUTH_PATH", "env-auth") + }) + + It("logs in and reads the key where the Vault options say, not where the environment does", func(ctx SpecContext) { + _, err := NewSigner(ctx, SignerOptions{ + KeyRef: "hashivault://engine-key", + CertRef: certRef, + Vault: VaultOptions{ + Address: vault.URL, + TransitPath: "dh-engine-signer", + AuthJWT: "files-jwt", + AuthRole: "files-role", + AuthPath: "fox", + }, + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(vault.logins).To(Equal([]fakeVaultLogin{{path: "/v1/auth/fox/login", role: "files-role", jwt: "files-jwt"}})) + Expect(vault.keyReads).To(Equal([]string{"/v1/dh-engine-signer/keys/engine-key|s.fake"})) + }) + + It("falls back to the environment for every option left empty", func(ctx SpecContext) { + GinkgoT().Setenv("VAULT_ADDR", vault.URL) + + _, err := NewSigner(ctx, SignerOptions{ + KeyRef: "hashivault://engine-key", + CertRef: certRef, + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(vault.logins).To(Equal([]fakeVaultLogin{{path: "/v1/auth/env-auth/login", role: "env-role", jwt: "env-jwt"}})) + Expect(vault.keyReads).To(Equal([]string{"/v1/env-transit/keys/engine-key|s.fake"})) + }) + + It("keeps the environment authentication when only the address and transit mount are given", func(ctx SpecContext) { + _, err := NewSigner(ctx, SignerOptions{ + KeyRef: "hashivault://engine-key", + CertRef: certRef, + Vault: VaultOptions{Address: vault.URL, TransitPath: "dh-engine-signer"}, + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(vault.logins).To(Equal([]fakeVaultLogin{{path: "/v1/auth/env-auth/login", role: "env-role", jwt: "env-jwt"}})) + Expect(vault.keyReads).To(Equal([]string{"/v1/dh-engine-signer/keys/engine-key|s.fake"})) + }) +}) From 8f358517b6bb6c1607c1bc8b3c9a7247454ea63e Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Fri, 16 Oct 2026 10:20:12 +0300 Subject: [PATCH 11/22] fix(build): log the network mode a stage really runs with 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 --- cmd/werf/common/files_signing_test.go | 43 +++++++++++++++++++++++++++ cmd/werf/common/signature.go | 19 +++++++----- pkg/build/build_phase.go | 11 ++++--- 3 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 cmd/werf/common/files_signing_test.go diff --git a/cmd/werf/common/files_signing_test.go b/cmd/werf/common/files_signing_test.go new file mode 100644 index 0000000000..6310cba83e --- /dev/null +++ b/cmd/werf/common/files_signing_test.go @@ -0,0 +1,43 @@ +package common + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/samber/lo" + + "github.com/werf/werf/v3/pkg/config" +) + +var _ = Describe("files signing options", func() { + signingConfig := func() *config.WerfConfig { + return config.NewWerfConfig(&config.Meta{}, []config.ImageInterface{ + &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: "app", Sign: []*config.Sign{{Path: "/out/x", After: "install"}}}}, + }) + } + + DescribeTable("refuses an incomplete command line before any stage runs", + func(cmdData *CmdData, expectedError string) { + _, err := getFilesSigningOptions(context.Background(), cmdData, signingConfig()) + Expect(err).To(MatchError(ContainSubstring(expectedError))) + }, + Entry("no key", &CmdData{SignFiles: lo.ToPtr(true)}, "images app declare files to sign: specify the signing key"), + Entry("no certificate", &CmdData{SignFiles: lo.ToPtr(true), SignFilesKey: lo.ToPtr("k")}, "specify the signing certificate"), + Entry("vault role without jwt", &CmdData{SignFiles: lo.ToPtr(true), SignFilesKey: lo.ToPtr("k"), SignFilesCert: lo.ToPtr("c"), SignFilesVaultAuthRole: lo.ToPtr("r")}, "qualify --sign-files-vault-auth-jwt"), + Entry("vault auth path without jwt", &CmdData{SignFiles: lo.ToPtr(true), SignFilesKey: lo.ToPtr("k"), SignFilesCert: lo.ToPtr("c"), SignFilesVaultAuthPath: lo.ToPtr("fox")}, "qualify --sign-files-vault-auth-jwt"), + ) + + DescribeTable("is disabled without touching the key", + func(cmdData *CmdData, werfConfig *config.WerfConfig) { + options, err := getFilesSigningOptions(context.Background(), cmdData, werfConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(options.Enabled).To(BeFalse()) + }, + Entry("signing switched off", &CmdData{SignFiles: lo.ToPtr(false)}, signingConfig()), + Entry("flag not registered", &CmdData{}, signingConfig()), + Entry("no image declares files to sign", &CmdData{SignFiles: lo.ToPtr(true)}, config.NewWerfConfig(&config.Meta{}, []config.ImageInterface{ + &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: "app"}}, + })), + ) +}) diff --git a/cmd/werf/common/signature.go b/cmd/werf/common/signature.go index 2aa1e77377..c416bd8c8b 100644 --- a/cmd/werf/common/signature.go +++ b/cmd/werf/common/signature.go @@ -227,17 +227,22 @@ func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfCon return signing.FilesSigningOptions{}, fmt.Errorf("images %s declare files to sign: specify the signing certificate with --sign-files-cert", strings.Join(signingImages, ", ")) } + vault := signing.VaultOptions{ + Address: lo.FromPtr(commonCmdData.SignFilesVaultAddr), + TransitPath: lo.FromPtr(commonCmdData.SignFilesVaultTransitPath), + AuthJWT: lo.FromPtr(commonCmdData.SignFilesVaultAuthJWT), + AuthRole: lo.FromPtr(commonCmdData.SignFilesVaultAuthRole), + AuthPath: lo.FromPtr(commonCmdData.SignFilesVaultAuthPath), + } + if (vault.AuthRole != "" || vault.AuthPath != "") && vault.AuthJWT == "" { + return signing.FilesSigningOptions{}, fmt.Errorf("--sign-files-vault-auth-role and --sign-files-vault-auth-path qualify --sign-files-vault-auth-jwt: specify the JWT as well, or drop them to log in to Vault as --sign-key does") + } + signer, err := signing.NewSigner(ctx, signing.SignerOptions{ KeyRef: keyRef, CertRef: certRef, IntermediatesRef: lo.FromPtr(commonCmdData.SignFilesIntermediates), - Vault: signing.VaultOptions{ - Address: lo.FromPtr(commonCmdData.SignFilesVaultAddr), - TransitPath: lo.FromPtr(commonCmdData.SignFilesVaultTransitPath), - AuthJWT: lo.FromPtr(commonCmdData.SignFilesVaultAuthJWT), - AuthRole: lo.FromPtr(commonCmdData.SignFilesVaultAuthRole), - AuthPath: lo.FromPtr(commonCmdData.SignFilesVaultAuthPath), - }, + Vault: vault, }) if err != nil { return signing.FilesSigningOptions{}, fmt.Errorf("create files signer: %w", err) diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index 170618cd37..a2a3642657 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -1323,8 +1323,11 @@ func (phase *BuildPhase) getPrevNonEmptyStageCreationTs() int64 { return 0 } -func (phase *BuildPhase) getLogImageNetwork(img *image.Image) string { +func (phase *BuildPhase) getLogImageNetwork(img *image.Image, stg stage.Interface) string { network := phase.ImageBuildOptions.Network + if n, ok := stg.(interface{ Network() string }); network == "" && ok { + network = n.Network() + } if network == "" { if img.IsDockerfileImage && img.DockerfileImageConfig != nil { network = img.DockerfileImageConfig.Network @@ -1404,7 +1407,7 @@ func (phase *BuildPhase) onImageStage(ctx context.Context, img *image.Image, stg if foundSuitableStage { phase.countStageCacheHit(ctx) logboek.Context(ctx).Default().LogFHighlight("Use previously built image for %s\n", stg.LogDetailedName()) - container_backend.LogImageInfo(ctx, stg.GetStageImage().Image, phase.getPrevNonEmptyStageImageSize(), img.ShouldLogPlatform(), phase.getLogImageNetwork(img)) + container_backend.LogImageInfo(ctx, stg.GetStageImage().Image, phase.getPrevNonEmptyStageImageSize(), img.ShouldLogPlatform(), phase.getLogImageNetwork(img, stg)) logboek.Context(ctx).LogOptionalLn() @@ -1569,7 +1572,7 @@ func (phase *BuildPhase) findAndFetchStageFromSecondaryStagesStorage(ctx context } logboek.Context(ctx).Default().LogFHighlight("Use previously built image for %s\n", stg.LogDetailedName()) - container_backend.LogImageInfo(ctx, stg.GetStageImage().Image, phase.getPrevNonEmptyStageImageSize(), img.ShouldLogPlatform(), phase.getLogImageNetwork(img)) + container_backend.LogImageInfo(ctx, stg.GetStageImage().Image, phase.getPrevNonEmptyStageImageSize(), img.ShouldLogPlatform(), phase.getLogImageNetwork(img, stg)) return nil } @@ -1925,7 +1928,7 @@ func (phase *BuildPhase) buildStage(ctx context.Context, img *image.Image, stg s if err != nil { return } - container_backend.LogImageInfo(ctx, stg.GetStageImage().Image, phase.getPrevNonEmptyStageImageSize(), img.ShouldLogPlatform(), phase.getLogImageNetwork(img)) + container_backend.LogImageInfo(ctx, stg.GetStageImage().Image, phase.getPrevNonEmptyStageImageSize(), img.ShouldLogPlatform(), phase.getLogImageNetwork(img, stg)) } var reusedPublishedStage bool From f315c8a901d2d6e7b5f0ce2a8eb80c65ad1b0666 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 22 Oct 2026 09:38:54 +0300 Subject: [PATCH 12/22] fix(build): let the kernel accept the signatures werf writes next to files The kernel PKCS#7 parser resolves the signature algorithm from the signerInfo OID alone and knows only rsaEncryption there, while pkcs7 inferred sha256WithRSAEncryption from the digest algorithm. A dm-verity roothash signed by werf was therefore rejected with -ENOPKG, even though openssl verified it. Pin the encryption OID to rsaEncryption and assert it on the parsed signature. Also cover the branch that rejects a signature the signing certificate does not verify: removing it kept the suite green, so a key that does not belong to the certificate could ship a signature nothing can check. Signed-off-by: Radmir Khurum --- pkg/build/signing/files_signing.go | 4 ++ pkg/build/signing/files_signing_test.go | 63 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/pkg/build/signing/files_signing.go b/pkg/build/signing/files_signing.go index d067951bf5..28c895c1fa 100644 --- a/pkg/build/signing/files_signing.go +++ b/pkg/build/signing/files_signing.go @@ -55,6 +55,10 @@ func SignDetachedPKCS7(ctx context.Context, content []byte, signer *Signer) ([]b return nil, fmt.Errorf("create signed data: %w", err) } sd.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) + // The kernel PKCS#7 parser resolves the signature algorithm from this OID alone and + // knows only rsaEncryption there; the sha256WithRSAEncryption OID pkcs7 would infer + // from the digest makes it reject the signature with -ENOPKG. + sd.SetEncryptionAlgorithm(pkcs7.OIDEncryptionAlgorithmRSA) cs := &digestSigner{ctx: ctx, sv: signer.sv, pub: leafPub} if err := sd.SignWithoutAttr(leaf, cs, pkcs7.SignerInfoConfig{}); err != nil { diff --git a/pkg/build/signing/files_signing_test.go b/pkg/build/signing/files_signing_test.go index 6798a49f02..a254d622dd 100644 --- a/pkg/build/signing/files_signing_test.go +++ b/pkg/build/signing/files_signing_test.go @@ -1,6 +1,7 @@ package signing import ( + "bytes" "context" "crypto" "crypto/rand" @@ -10,6 +11,7 @@ import ( "crypto/x509/pkix" "encoding/base64" "encoding/pem" + "io" "math/big" "os" "os/exec" @@ -21,6 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/sigstore/sigstore/pkg/signature" "github.com/smallstep/pkcs7" ) @@ -29,6 +32,42 @@ func TestFilesSigning(t *testing.T) { RunSpecs(t, "Files signing suite") } +// badSignatureSigner answers every signing request with bytes that are not a valid +// signature of the digest, the shape a wrong Vault key or a mismatched certificate +// produces. +type badSignatureSigner struct { + signature.SignerVerifier + + pub crypto.PublicKey +} + +var _ signature.SignerVerifier = (*badSignatureSigner)(nil) + +func (s *badSignatureSigner) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) { + return s.pub, nil +} + +func (s *badSignatureSigner) SignMessage(_ io.Reader, _ ...signature.SignOption) ([]byte, error) { + return bytes.Repeat([]byte{0x42}, 256), nil +} + +func newTestKeyPair() (*rsa.PrivateKey, []byte) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "files-signing-test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + + return key, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) +} + func newTestSigner(ctx context.Context) (*Signer, *rsa.PrivateKey) { key, err := rsa.GenerateKey(rand.Reader, 2048) Expect(err).NotTo(HaveOccurred()) @@ -84,6 +123,30 @@ var _ = Describe("SignDetachedPKCS7", func() { Expect(p7.Verify()).To(Succeed()) }) + It("names the signature algorithm rsaEncryption, the only one the kernel parser resolves", func(ctx SpecContext) { + signer, _ := newTestSigner(ctx) + + der, err := SignDetachedPKCS7(ctx, content, signer) + Expect(err).NotTo(HaveOccurred()) + + p7, err := pkcs7.Parse(der) + Expect(err).NotTo(HaveOccurred()) + Expect(p7.Signers[0].DigestAlgorithm.Algorithm).To(Equal(pkcs7.OIDDigestAlgorithmSHA256)) + Expect(p7.Signers[0].DigestEncryptionAlgorithm.Algorithm).To(Equal(pkcs7.OIDEncryptionAlgorithmRSA), + "the kernel pkcs7 parser answers -ENOPKG for sha256WithRSAEncryption (1.2.840.113549.1.1.11)") + }) + + It("refuses a signature the signing certificate does not verify", func(ctx SpecContext) { + key, certPEM := newTestKeyPair() + signer := &Signer{sv: &signver.SignerVerifier{ + Cert: certPEM, + SignerVerifier: &badSignatureSigner{pub: &key.PublicKey}, + }} + + _, err := SignDetachedPKCS7(ctx, content, signer) + Expect(err).To(MatchError(ContainSubstring("does not verify the signature the key produced"))) + }) + It("is accepted by openssl as a detached signature of the content", func(ctx SpecContext) { if _, err := exec.LookPath("openssl"); err != nil { Skip("openssl is not installed") From c592779ee7a4709b42d1b781ffc15b3f2597ec02 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 22 Oct 2026 09:41:40 +0300 Subject: [PATCH 13/22] fix(build): sign every declared file with its own signature Two sign entries whose paths end in the same file name shared one temporary file in the legacy stapel builder: the second signature overwrote the first and both destinations in the image received the same bytes, so one of the files shipped with a signature of something else. Keep each signature in its own directory, named after the entry's position. A sign path pointing at a directory was answered by the Docker reader with the first regular file inside it, which then got signed under the directory's name; the Buildah reader already refused it. Refuse the directory at the first tar entry, which is the object `docker cp` was asked for. Signed-off-by: Radmir Khurum --- pkg/build/stage/sign_files.go | 14 +++- pkg/container_backend/docker_image_reader.go | 54 +++++++++---- .../docker_image_reader_test.go | 81 +++++++++++++++++++ .../_fixtures/sign_files/state/werf.yaml | 7 +- 4 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 pkg/container_backend/docker_image_reader_test.go diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index 55bb561e1a..96d598e310 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "strconv" "al.essio.dev/pkg/shellescape" @@ -109,7 +110,7 @@ func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb contai legacy := c.UseLegacyStapelBuilder(cb) var commands []string - for _, sign := range s.signs { + for i, sign := range s.signs { content, err := cb.ReadFileFromImage(ctx, prevBuiltImage.Image.Name(), sign.Path, container_backend.ReadFileFromImageOpts{TargetPlatform: s.targetPlatform}) if err != nil { return fmt.Errorf("read %s to sign from image %q: %w", sign.Path, s.imageName, err) @@ -126,11 +127,18 @@ func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb contai outputName := path.Base(sign.OutputPath()) if legacy { - hostPath := filepath.Join(signaturesDir, outputName) + // Two signed files can share a base name: keep every signature in its own + // directory, or the later one overwrites the earlier and both destinations + // receive the same signature. + entryDir := strconv.Itoa(i) + if err := os.MkdirAll(filepath.Join(signaturesDir, entryDir), os.ModePerm); err != nil { + return fmt.Errorf("create signature dir for %s: %w", sign.Path, err) + } + hostPath := filepath.Join(signaturesDir, entryDir, outputName) if err := os.WriteFile(hostPath, signature, 0o644); err != nil { return fmt.Errorf("write signature %s: %w", hostPath, err) } - containerPath := path.Join(s.containerWerfDir, "sign", string(s.Name()), outputName) + containerPath := path.Join(s.containerWerfDir, "sign", string(s.Name()), entryDir, outputName) commands = append(commands, fmt.Sprintf("%s -p %s", stapel.MkdirBinPath(), shellescape.Quote(path.Dir(sign.OutputPath()))), fmt.Sprintf("%s -m 0644 %s %s", stapel.InstallBinPath(), shellescape.Quote(containerPath), shellescape.Quote(sign.OutputPath())), diff --git a/pkg/container_backend/docker_image_reader.go b/pkg/container_backend/docker_image_reader.go index 207df4ff7a..f0fa3b71bd 100644 --- a/pkg/container_backend/docker_image_reader.go +++ b/pkg/container_backend/docker_image_reader.go @@ -66,28 +66,50 @@ func (r *dockerImageReader) Close(ctx context.Context) error { } func (r *dockerImageReader) ReadFile(ctx context.Context, path string) ([]byte, error) { - var data []byte - found := false + collector := &fileTarCollector{path: path, imageRef: r.imageRef} + if err := r.copy(ctx, path, collector.collect); err != nil { + return nil, err + } + return collector.result() +} - err := r.copy(ctx, path, func(tr *tar.Reader, hdr *tar.Header) error { - if found || hdr.Typeflag != tar.TypeReg { - return nil - } - var err error - if data, err = io.ReadAll(tr); err != nil { - return fmt.Errorf("read %s content from image %q: %w", path, r.imageRef, err) +// fileTarCollector takes the content of the object a `docker cp ` stream was +// asked for. That object is the stream's first entry and whatever it contains follows +// it, so a directory has to be refused there: otherwise an arbitrary file inside it +// would be returned as the content of path. +type fileTarCollector struct { + path string + imageRef string + + rootSeen bool + data []byte + found bool +} + +func (c *fileTarCollector) collect(tr *tar.Reader, hdr *tar.Header) error { + if !c.rootSeen { + c.rootSeen = true + if hdr.Typeflag == tar.TypeDir { + return fmt.Errorf("%s in image %q is a directory, a regular file is required", c.path, c.imageRef) } - found = true + } + if c.found || hdr.Typeflag != tar.TypeReg { return nil - }) - if err != nil { - return nil, err } - if !found { - return nil, fmt.Errorf("no regular file at %s in image %q: %w", path, r.imageRef, fs.ErrNotExist) + data, err := io.ReadAll(tr) + if err != nil { + return fmt.Errorf("read %s content from image %q: %w", c.path, c.imageRef, err) } + c.data = data + c.found = true + return nil +} - return data, nil +func (c *fileTarCollector) result() ([]byte, error) { + if !c.found { + return nil, fmt.Errorf("no regular file at %s in image %q: %w", c.path, c.imageRef, fs.ErrNotExist) + } + return c.data, nil } func (r *dockerImageReader) ReadDir(ctx context.Context, path, destDir string, opts ReadDirOpts) error { diff --git a/pkg/container_backend/docker_image_reader_test.go b/pkg/container_backend/docker_image_reader_test.go new file mode 100644 index 0000000000..9f0569357b --- /dev/null +++ b/pkg/container_backend/docker_image_reader_test.go @@ -0,0 +1,81 @@ +package container_backend + +import ( + "archive/tar" + "bytes" + "io" + "io/fs" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("fileTarCollector", func() { + type entry struct { + name string + typeflag byte + content string + linkname string + } + + // feed mimics what `docker cp ` streams: the requested object first, named + // by its base name, followed by whatever it contains. + feed := func(collector *fileTarCollector, entries []entry) error { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + for _, e := range entries { + hdr := &tar.Header{Name: e.name, Typeflag: e.typeflag, Mode: 0o644, Linkname: e.linkname} + if e.typeflag == tar.TypeReg { + hdr.Size = int64(len(e.content)) + } + Expect(tw.WriteHeader(hdr)).To(Succeed()) + if e.typeflag == tar.TypeReg { + _, err := tw.Write([]byte(e.content)) + Expect(err).To(Succeed()) + } + } + Expect(tw.Close()).To(Succeed()) + + tr := tar.NewReader(&buf) + for { + hdr, err := tr.Next() + if err == io.EOF { + return nil + } + Expect(err).To(Succeed()) + if err := collector.collect(tr, hdr); err != nil { + return err + } + } + } + + It("reads the requested regular file", func() { + collector := &fileTarCollector{path: "/out/ext.roothash", imageRef: "image"} + + Expect(feed(collector, []entry{ + {name: "ext.roothash", typeflag: tar.TypeReg, content: "roothash"}, + })).To(Succeed()) + Expect(collector.result()).To(Equal([]byte("roothash"))) + }) + + It("refuses a directory instead of returning a file it contains", func() { + collector := &fileTarCollector{path: "/out", imageRef: "image"} + + err := feed(collector, []entry{ + {name: "out/", typeflag: tar.TypeDir}, + {name: "out/ext.roothash", typeflag: tar.TypeReg, content: "roothash"}, + }) + Expect(err).To(MatchError(ContainSubstring(`/out in image "image" is a directory`))) + }) + + It("reports a path that holds no regular file as not existing", func() { + collector := &fileTarCollector{path: "/out/link", imageRef: "image"} + + Expect(feed(collector, []entry{ + {name: "link", typeflag: tar.TypeSymlink, linkname: "/elsewhere"}, + })).To(Succeed()) + + _, err := collector.result() + Expect(err).To(MatchError(fs.ErrNotExist)) + }) +}) diff --git a/test/e2e/build/_fixtures/sign_files/state/werf.yaml b/test/e2e/build/_fixtures/sign_files/state/werf.yaml index da8318c2c0..502682f2c0 100644 --- a/test/e2e/build/_fixtures/sign_files/state/werf.yaml +++ b/test/e2e/build/_fixtures/sign_files/state/werf.yaml @@ -5,10 +5,15 @@ image: sysext from: alpine:3.20 shell: install: - - mkdir -p /out + - mkdir -p /out /other - printf '%s' 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b > /out/ext.roothash + - printf '%s' 9f2c04a1b8e3d6570ac41b2e8d93f5061c7ab38e4d2f90b5c6137ae8d40f2b9c > /other/ext.roothash setup: - test -s /out/ext.roothash.p7s + - test -s /other/ext.roothash.p7s + - "! cmp -s /out/ext.roothash.p7s /other/ext.roothash.p7s" sign: - path: /out/ext.roothash after: install +- path: /other/ext.roothash + after: install From cbe04d192e226ad7b6823cd9c7d4757d05bd2b28 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 22 Oct 2026 11:24:35 +0300 Subject: [PATCH 14/22] feat(build)!: rename the file signing options to --sign-build-file-* The sign directive signs files a build produces, which --sign-files read as a generalization of --sign-elf-files. Rename the whole group to --sign-build-file-* / WERF_SIGN_BUILD_FILE_*; the werf.yaml directive is unchanged. Drop --sign-files: a declared sign directive is a requirement of the image, not a build-time choice, so there is no longer a way to build it unsigned. Split the public identity of a signature from the ability to produce one. The certificate takes part in the stage digest, so it is required wherever the stage chain is built; the private key is required only where a signature is actually produced, and resolving, rendering or deploying an already built image now needs neither the key nor access to the KMS. The signer was constructed eagerly for every image of werf.yaml, so building an unrelated image demanded the signing material of another one; the requirement now covers the images being processed and their dependencies only. Move the options from BuildOptions to ConveyorOptions. They were applied in GetConveyorOptionsWithParallel only, so run, compose and kube-run built the image tree without the sign stages and silently produced an unsigned image, while stage image, stages copy, lint, sbom get and helm get-autogenerated-values resolved a digest that no build had ever published. Those commands register the options now as well. Signed-off-by: Radmir Khurum --- cmd/werf/build/main.go | 4 +- cmd/werf/bundle/publish/publish.go | 4 +- cmd/werf/common/cmd_data.go | 18 +-- cmd/werf/common/conveyor_options.go | 24 ++-- cmd/werf/common/files_signing_test.go | 111 ++++++++++++--- cmd/werf/common/signature.go | 130 +++++++++--------- cmd/werf/compose/main.go | 4 +- cmd/werf/converge/converge.go | 4 +- cmd/werf/export/export.go | 4 +- cmd/werf/helm/get_autogenerated_values.go | 4 +- cmd/werf/kube_run/kube_run.go | 4 +- cmd/werf/lint/lint.go | 4 +- cmd/werf/plan/plan.go | 4 +- cmd/werf/render/render.go | 4 +- cmd/werf/run/run.go | 4 +- cmd/werf/sbom/get/get.go | 4 +- cmd/werf/stage/image/main.go | 4 +- cmd/werf/stages/copy/copy.go | 4 +- docs/_includes/reference/cli/werf_build.md | 55 ++++---- .../reference/cli/werf_bundle_publish.md | 55 ++++---- .../reference/cli/werf_compose_config.md | 55 ++++---- .../reference/cli/werf_compose_down.md | 55 ++++---- .../reference/cli/werf_compose_run.md | 55 ++++---- .../reference/cli/werf_compose_up.md | 55 ++++---- docs/_includes/reference/cli/werf_converge.md | 55 ++++---- docs/_includes/reference/cli/werf_export.md | 55 ++++---- .../cli/werf_helm_get_autogenerated_values.md | 25 ++++ docs/_includes/reference/cli/werf_kube_run.md | 55 ++++---- docs/_includes/reference/cli/werf_lint.md | 25 ++++ docs/_includes/reference/cli/werf_plan.md | 55 ++++---- docs/_includes/reference/cli/werf_render.md | 55 ++++---- docs/_includes/reference/cli/werf_run.md | 55 ++++---- docs/_includes/reference/cli/werf_sbom_get.md | 25 ++++ .../reference/cli/werf_stages_copy.md | 25 ++++ docs/pages_en/usage/build/stapel/imports.md | 31 +++-- docs/pages_ru/usage/build/stapel/imports.md | 31 +++-- pkg/build/build_phase.go | 1 - pkg/build/image/stapel_test.go | 4 +- pkg/build/signing/files_signing.go | 10 +- pkg/build/signing/files_signing_test.go | 41 +++--- pkg/build/signing/signer.go | 34 +++++ pkg/build/stage/sign_files.go | 6 +- pkg/build/stage/sign_files_test.go | 19 ++- test/e2e/build/sign_files_test.go | 42 ++++-- 44 files changed, 755 insertions(+), 563 deletions(-) diff --git a/cmd/werf/build/main.go b/cmd/werf/build/main.go index 60e2554d18..bbab3b682f 100644 --- a/cmd/werf/build/main.go +++ b/cmd/werf/build/main.go @@ -89,7 +89,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupLogOptions(&commonCmdData, cmd) @@ -217,7 +217,7 @@ func run(ctx context.Context, containerBackend container_backend.ContainerBacken return err } - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/bundle/publish/publish.go b/cmd/werf/bundle/publish/publish.go index 105c6479bd..359158bdfe 100644 --- a/cmd/werf/bundle/publish/publish.go +++ b/cmd/werf/bundle/publish/publish.go @@ -136,7 +136,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) @@ -280,7 +280,7 @@ func runPublish(ctx context.Context, imageNameListFromArgs []string) error { imagesRepo = storageManager.GetServiceValuesRepo() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/common/cmd_data.go b/cmd/werf/common/cmd_data.go index 08212c9076..351c8e5e9c 100644 --- a/cmd/werf/common/cmd_data.go +++ b/cmd/werf/common/cmd_data.go @@ -123,15 +123,15 @@ type CmdData struct { SignCert *string SignIntermediates *string - SignFiles *bool - SignFilesKey *string - SignFilesCert *string - SignFilesIntermediates *string - SignFilesVaultAddr *string - SignFilesVaultTransitPath *string - SignFilesVaultAuthJWT *string - SignFilesVaultAuthRole *string - SignFilesVaultAuthPath *string + SignFiles *bool + SignBuildFileKey *string + SignBuildFileCert *string + SignBuildFileIntermediates *string + SignBuildFileVaultAddr *string + SignBuildFileVaultTransitPath *string + SignBuildFileVaultAuthJWT *string + SignBuildFileVaultAuthRole *string + SignBuildFileVaultAuthPath *string VerifyManifest *bool VerifyRoots *[]string diff --git a/cmd/werf/common/conveyor_options.go b/cmd/werf/common/conveyor_options.go index 78f3264717..7be8aac5eb 100644 --- a/cmd/werf/common/conveyor_options.go +++ b/cmd/werf/common/conveyor_options.go @@ -23,9 +23,18 @@ import ( "github.com/werf/werf/v3/pkg/storage" ) -func GetConveyorOptions(ctx context.Context, commonCmdData *CmdData, imagesToProcess config.ImagesToProcess) (build.ConveyorOptions, error) { +func GetConveyorOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *config.WerfConfig, imagesToProcess config.ImagesToProcess) (build.ConveyorOptions, error) { + // The sign directive adds stages to the image, so every command that builds the + // stage chain needs them — resolving an already built image included, or its digest + // would not be the one the build published. + filesSigningOptions, err := getFilesSigningOptions(ctx, commonCmdData, werfConfig, imagesToProcess) + if err != nil { + return build.ConveyorOptions{}, err + } + conveyorOptions := build.ConveyorOptions{ - ImagesToProcess: imagesToProcess, + ImagesToProcess: imagesToProcess, + FilesSigningOptions: filesSigningOptions, } if len(commonCmdData.GetPlatform()) > 0 { @@ -64,8 +73,8 @@ func GetDeferredBuildLog(ctx context.Context, commonCmdData *CmdData) bool { return requireBuiltImage || !isVerbose } -func GetConveyorOptionsWithParallel(ctx context.Context, commonCmdData *CmdData, imagesToProcess config.ImagesToProcess, buildStagesOptions build.BuildOptions) (build.ConveyorOptions, error) { - conveyorOptions, err := GetConveyorOptions(ctx, commonCmdData, imagesToProcess) +func GetConveyorOptionsWithParallel(ctx context.Context, commonCmdData *CmdData, werfConfig *config.WerfConfig, imagesToProcess config.ImagesToProcess, buildStagesOptions build.BuildOptions) (build.ConveyorOptions, error) { + conveyorOptions, err := GetConveyorOptions(ctx, commonCmdData, werfConfig, imagesToProcess) if err != nil { return conveyorOptions, err } @@ -75,7 +84,6 @@ func GetConveyorOptionsWithParallel(ctx context.Context, commonCmdData *CmdData, conveyorOptions.ManifestSigningOptions = buildStagesOptions.ManifestSigningOptions conveyorOptions.SbomSigningOptions = buildStagesOptions.SbomSigningOptions conveyorOptions.VexSigningOptions = buildStagesOptions.VexSigningOptions - conveyorOptions.FilesSigningOptions = buildStagesOptions.FilesSigningOptions conveyorOptions.VerityAnnotationOptions = buildStagesOptions.VerityAnnotationOptions return conveyorOptions, nil @@ -139,11 +147,6 @@ func GetBuildOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *co return buildOptions, err } - filesSigningOptions, err := getFilesSigningOptions(ctx, commonCmdData, werfConfig) - if err != nil { - return buildOptions, err - } - verityAnnotationOptions, err := getVerityAnnotationOptions(commonCmdData) if err != nil { return build.BuildOptions{}, fmt.Errorf("getting verity annotation options: %w", err) @@ -163,7 +166,6 @@ func GetBuildOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *co SbomSigningOptions: sbomSigningOptions, VexSigningOptions: vexSigningOptions, ELFSigningOptions: elfSigningOptions, - FilesSigningOptions: filesSigningOptions, VerityAnnotationOptions: verityAnnotationOptions, ReportOperations: GetBuildReportOperations(commonCmdData), } diff --git a/cmd/werf/common/files_signing_test.go b/cmd/werf/common/files_signing_test.go index 6310cba83e..447ca167a0 100644 --- a/cmd/werf/common/files_signing_test.go +++ b/cmd/werf/common/files_signing_test.go @@ -2,6 +2,14 @@ package common import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "math/big" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -11,33 +19,94 @@ import ( ) var _ = Describe("files signing options", func() { - signingConfig := func() *config.WerfConfig { - return config.NewWerfConfig(&config.Meta{}, []config.ImageInterface{ - &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: "app", Sign: []*config.Sign{{Path: "/out/x", After: "install"}}}}, - }) + signingImage := func(name string) config.ImageInterface { + return &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: name, Sign: []*config.Sign{{Path: "/out/x", After: "install"}}}} + } + + plainImage := func(name string) config.ImageInterface { + return &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: name}} + } + + newConfig := func(images ...config.ImageInterface) *config.WerfConfig { + return config.NewWerfConfig(&config.Meta{}, images) + } + + allImages := func(werfConfig *config.WerfConfig) config.ImagesToProcess { + imagesToProcess, err := config.NewImagesToProcess(werfConfig, nil, false, false) + Expect(err).NotTo(HaveOccurred()) + return imagesToProcess + } + + someImages := func(werfConfig *config.WerfConfig, names ...string) config.ImagesToProcess { + imagesToProcess, err := config.NewImagesToProcess(werfConfig, names, false, false) + Expect(err).NotTo(HaveOccurred()) + return imagesToProcess + } + + certPEM := func() string { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "files-signing-options-test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + Expect(err).NotTo(HaveOccurred()) + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})) } DescribeTable("refuses an incomplete command line before any stage runs", func(cmdData *CmdData, expectedError string) { - _, err := getFilesSigningOptions(context.Background(), cmdData, signingConfig()) + werfConfig := newConfig(signingImage("app")) + _, err := getFilesSigningOptions(context.Background(), cmdData, werfConfig, allImages(werfConfig)) Expect(err).To(MatchError(ContainSubstring(expectedError))) }, - Entry("no key", &CmdData{SignFiles: lo.ToPtr(true)}, "images app declare files to sign: specify the signing key"), - Entry("no certificate", &CmdData{SignFiles: lo.ToPtr(true), SignFilesKey: lo.ToPtr("k")}, "specify the signing certificate"), - Entry("vault role without jwt", &CmdData{SignFiles: lo.ToPtr(true), SignFilesKey: lo.ToPtr("k"), SignFilesCert: lo.ToPtr("c"), SignFilesVaultAuthRole: lo.ToPtr("r")}, "qualify --sign-files-vault-auth-jwt"), - Entry("vault auth path without jwt", &CmdData{SignFiles: lo.ToPtr(true), SignFilesKey: lo.ToPtr("k"), SignFilesCert: lo.ToPtr("c"), SignFilesVaultAuthPath: lo.ToPtr("fox")}, "qualify --sign-files-vault-auth-jwt"), + Entry("no certificate", &CmdData{}, "images app declare files to sign: specify the signing certificate"), + Entry("certificate that cannot be read", &CmdData{SignBuildFileCert: lo.ToPtr("c")}, "read the files signing certificate"), ) - DescribeTable("is disabled without touching the key", - func(cmdData *CmdData, werfConfig *config.WerfConfig) { - options, err := getFilesSigningOptions(context.Background(), cmdData, werfConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(options.Enabled).To(BeFalse()) - }, - Entry("signing switched off", &CmdData{SignFiles: lo.ToPtr(false)}, signingConfig()), - Entry("flag not registered", &CmdData{}, signingConfig()), - Entry("no image declares files to sign", &CmdData{SignFiles: lo.ToPtr(true)}, config.NewWerfConfig(&config.Meta{}, []config.ImageInterface{ - &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: "app"}}, - })), - ) + It("refuses a vault role or auth path that qualifies no jwt", func() { + werfConfig := newConfig(signingImage("app")) + cert := base64.StdEncoding.EncodeToString([]byte(certPEM())) + + for _, cmdData := range []*CmdData{ + {SignBuildFileKey: lo.ToPtr("k"), SignBuildFileCert: lo.ToPtr(cert), SignBuildFileVaultAuthRole: lo.ToPtr("r")}, + {SignBuildFileKey: lo.ToPtr("k"), SignBuildFileCert: lo.ToPtr(cert), SignBuildFileVaultAuthPath: lo.ToPtr("fox")}, + } { + _, err := getFilesSigningOptions(context.Background(), cmdData, werfConfig, allImages(werfConfig)) + Expect(err).To(MatchError(ContainSubstring("qualify --sign-build-file-vault-auth-jwt"))) + } + }) + + It("is disabled when no image being processed declares files to sign", func() { + werfConfig := newConfig(plainImage("app")) + + options, err := getFilesSigningOptions(context.Background(), &CmdData{}, werfConfig, allImages(werfConfig)) + Expect(err).NotTo(HaveOccurred()) + Expect(options.Enabled).To(BeFalse()) + }) + + It("ignores the sign directive of an image that is not being processed", func() { + werfConfig := newConfig(plainImage("plain"), signingImage("signed")) + + options, err := getFilesSigningOptions(context.Background(), &CmdData{}, werfConfig, someImages(werfConfig, "plain")) + Expect(err).NotTo(HaveOccurred()) + Expect(options.Enabled).To(BeFalse(), "building an unrelated image must not demand the signing material of another one") + }) + + It("identifies the signature by the certificate without a private key", func() { + werfConfig := newConfig(signingImage("app")) + cert := certPEM() + + options, err := getFilesSigningOptions(context.Background(), &CmdData{ + SignBuildFileCert: lo.ToPtr(base64.StdEncoding.EncodeToString([]byte(cert))), + }, werfConfig, allImages(werfConfig)) + Expect(err).NotTo(HaveOccurred()) + Expect(options.Enabled).To(BeTrue()) + Expect(options.Cert).To(Equal(cert)) + Expect(options.Signer()).To(BeNil(), "no private key was given, so nothing can sign") + }) }) diff --git a/cmd/werf/common/signature.go b/cmd/werf/common/signature.go index c416bd8c8b..2383993248 100644 --- a/cmd/werf/common/signature.go +++ b/cmd/werf/common/signature.go @@ -46,50 +46,38 @@ func GetSignManifest(cmdData *CmdData) bool { return option.PtrValueOrDefault(cmdData.SignManifest, false) } -func SetupFilesSigningOptions(cmdData *CmdData, cmd *cobra.Command) { - cmdData.SignFiles = new(bool) - cmd.Flags().BoolVarP(cmdData.SignFiles, "sign-files", "", util.GetBoolEnvironmentDefaultTrue("WERF_SIGN_FILES"), - `Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES or true). -When enabled and at least one image declares files to sign, -the private signing key must be specified with --sign-files-key option and -the certificate must be specified with --sign-files-cert option. -Disable to build such images without signatures`) - - cmdData.SignFilesKey = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesKey, "sign-files-key", "", os.Getenv("WERF_SIGN_FILES_KEY"), - "The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_FILES_KEY)") - - cmdData.SignFilesCert = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesCert, "sign-files-cert", "", os.Getenv("WERF_SIGN_FILES_CERT"), - "The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_FILES_CERT)") - - cmdData.SignFilesIntermediates = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesIntermediates, "sign-files-intermediates", "", os.Getenv("WERF_SIGN_FILES_INTERMEDIATES"), - "The intermediate certificates of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES)") - - cmdData.SignFilesVaultAddr = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesVaultAddr, "sign-files-vault-addr", "", os.Getenv("WERF_SIGN_FILES_VAULT_ADDR"), - "The Vault address a hashivault:// files signing key is resolved against (default $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR)") - - cmdData.SignFilesVaultTransitPath = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesVaultTransitPath, "sign-files-vault-transit-path", "", os.Getenv("WERF_SIGN_FILES_VAULT_TRANSIT_PATH"), - "The Vault transit engine mount of the files signing key (default $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH)") - - cmdData.SignFilesVaultAuthJWT = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesVaultAuthJWT, "sign-files-vault-auth-jwt", "", os.Getenv("WERF_SIGN_FILES_VAULT_AUTH_JWT"), - "The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default $WERF_SIGN_FILES_VAULT_AUTH_JWT)") - - cmdData.SignFilesVaultAuthRole = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesVaultAuthRole, "sign-files-vault-auth-role", "", os.Getenv("WERF_SIGN_FILES_VAULT_AUTH_ROLE"), - "The Vault JWT role for --sign-files-vault-auth-jwt (default $WERF_SIGN_FILES_VAULT_AUTH_ROLE)") - - cmdData.SignFilesVaultAuthPath = new(string) - cmd.Flags().StringVarP(cmdData.SignFilesVaultAuthPath, "sign-files-vault-auth-path", "", os.Getenv("WERF_SIGN_FILES_VAULT_AUTH_PATH"), - "The Vault JWT auth mount for --sign-files-vault-auth-jwt (default $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt)") -} - -func GetSignFiles(cmdData *CmdData) bool { - return option.PtrValueOrDefault(cmdData.SignFiles, false) +func SetupSignBuildFileOptions(cmdData *CmdData, cmd *cobra.Command) { + cmdData.SignBuildFileKey = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileKey, "sign-build-file-key", "", os.Getenv("WERF_SIGN_BUILD_FILE_KEY"), + "The private key to sign files with as path to PEM file, base64-encoded PEM or hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY)") + + cmdData.SignBuildFileCert = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileCert, "sign-build-file-cert", "", os.Getenv("WERF_SIGN_BUILD_FILE_CERT"), + "The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_CERT)") + + cmdData.SignBuildFileIntermediates = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileIntermediates, "sign-build-file-intermediates", "", os.Getenv("WERF_SIGN_BUILD_FILE_INTERMEDIATES"), + "The intermediate certificates of the files signing key as path to PEM file or base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES)") + + cmdData.SignBuildFileVaultAddr = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileVaultAddr, "sign-build-file-vault-addr", "", os.Getenv("WERF_SIGN_BUILD_FILE_VAULT_ADDR"), + "The Vault address a hashivault:// files signing key is resolved against (default $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR)") + + cmdData.SignBuildFileVaultTransitPath = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileVaultTransitPath, "sign-build-file-vault-transit-path", "", os.Getenv("WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH"), + "The Vault transit engine mount of the files signing key (default $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH)") + + cmdData.SignBuildFileVaultAuthJWT = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileVaultAuthJWT, "sign-build-file-vault-auth-jwt", "", os.Getenv("WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT"), + "The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT)") + + cmdData.SignBuildFileVaultAuthRole = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileVaultAuthRole, "sign-build-file-vault-auth-role", "", os.Getenv("WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE"), + "The Vault JWT role for --sign-build-file-vault-auth-jwt (default $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE)") + + cmdData.SignBuildFileVaultAuthPath = new(string) + cmd.Flags().StringVarP(cmdData.SignBuildFileVaultAuthPath, "sign-build-file-vault-auth-path", "", os.Getenv("WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH"), + "The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt)") } func SetupELFSigningOptions(cmdData *CmdData, cmd *cobra.Command) { @@ -201,12 +189,14 @@ func getVexSigningOptions(commonCmdData *CmdData, signer *signing.Signer) signin return options } -// getFilesSigningOptions builds the signer of the sign directive. The directive is a -// requirement of the image, so when an image declares one the key and certificate are -// mandatory unless signing is switched off explicitly. -func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *config.WerfConfig) (signing.FilesSigningOptions, error) { +// getFilesSigningOptions builds the options of the sign directive for the images that +// are about to be processed. The certificate is the public identity of the signature +// and takes part in the stage digest, so it is required as soon as one of those images +// declares the directive. The private key only produces the signature: it stays +// optional here, and a command that resolves an already built image never asks for it. +func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfConfig *config.WerfConfig, imagesToProcess config.ImagesToProcess) (signing.FilesSigningOptions, error) { var signingImages []string - for _, img := range werfConfig.Images(false) { + for _, img := range werfConfig.GetImagesForProcessing(imagesToProcess) { stapelImg, ok := img.(config.StapelImageInterface) if !ok || len(stapelImg.ImageBaseConfig().Sign) == 0 { continue @@ -214,41 +204,49 @@ func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfCon signingImages = append(signingImages, img.GetName()) } - if len(signingImages) == 0 || !GetSignFiles(commonCmdData) { - return signing.NewFilesSigningOptions(&signing.Signer{}), nil + if len(signingImages) == 0 { + return signing.FilesSigningOptions{}, nil + } + + certRef := lo.FromPtr(commonCmdData.SignBuildFileCert) + if certRef == "" { + return signing.FilesSigningOptions{}, fmt.Errorf("images %s declare files to sign: specify the signing certificate with --sign-build-file-cert", strings.Join(signingImages, ", ")) } - keyRef := lo.FromPtr(commonCmdData.SignFilesKey) - certRef := lo.FromPtr(commonCmdData.SignFilesCert) - switch { - case keyRef == "": - return signing.FilesSigningOptions{}, fmt.Errorf("images %s declare files to sign: specify the signing key with --sign-files-key or pass --sign-files=false to build them unsigned", strings.Join(signingImages, ", ")) - case certRef == "": - return signing.FilesSigningOptions{}, fmt.Errorf("images %s declare files to sign: specify the signing certificate with --sign-files-cert", strings.Join(signingImages, ", ")) + cert, err := signing.ReadCertificatePEM(certRef) + if err != nil { + return signing.FilesSigningOptions{}, fmt.Errorf("read the files signing certificate: %w", err) + } + + keyRef := lo.FromPtr(commonCmdData.SignBuildFileKey) + if keyRef == "" { + options := signing.NewFilesSigningOptions(cert, nil) + options.Enabled = true + return options, nil } vault := signing.VaultOptions{ - Address: lo.FromPtr(commonCmdData.SignFilesVaultAddr), - TransitPath: lo.FromPtr(commonCmdData.SignFilesVaultTransitPath), - AuthJWT: lo.FromPtr(commonCmdData.SignFilesVaultAuthJWT), - AuthRole: lo.FromPtr(commonCmdData.SignFilesVaultAuthRole), - AuthPath: lo.FromPtr(commonCmdData.SignFilesVaultAuthPath), + Address: lo.FromPtr(commonCmdData.SignBuildFileVaultAddr), + TransitPath: lo.FromPtr(commonCmdData.SignBuildFileVaultTransitPath), + AuthJWT: lo.FromPtr(commonCmdData.SignBuildFileVaultAuthJWT), + AuthRole: lo.FromPtr(commonCmdData.SignBuildFileVaultAuthRole), + AuthPath: lo.FromPtr(commonCmdData.SignBuildFileVaultAuthPath), } if (vault.AuthRole != "" || vault.AuthPath != "") && vault.AuthJWT == "" { - return signing.FilesSigningOptions{}, fmt.Errorf("--sign-files-vault-auth-role and --sign-files-vault-auth-path qualify --sign-files-vault-auth-jwt: specify the JWT as well, or drop them to log in to Vault as --sign-key does") + return signing.FilesSigningOptions{}, fmt.Errorf("--sign-build-file-vault-auth-role and --sign-build-file-vault-auth-path qualify --sign-build-file-vault-auth-jwt: specify the JWT as well, or drop them to log in to Vault as --sign-key does") } signer, err := signing.NewSigner(ctx, signing.SignerOptions{ KeyRef: keyRef, CertRef: certRef, - IntermediatesRef: lo.FromPtr(commonCmdData.SignFilesIntermediates), + IntermediatesRef: lo.FromPtr(commonCmdData.SignBuildFileIntermediates), Vault: vault, }) if err != nil { return signing.FilesSigningOptions{}, fmt.Errorf("create files signer: %w", err) } - options := signing.NewFilesSigningOptions(signer) + options := signing.NewFilesSigningOptions(cert, signer) options.Enabled = true return options, nil } diff --git a/cmd/werf/compose/main.go b/cmd/werf/compose/main.go index f94020b0b4..32eea38bbd 100644 --- a/cmd/werf/compose/main.go +++ b/cmd/werf/compose/main.go @@ -303,7 +303,7 @@ func newCmd(ctx context.Context, composeCmdName string, options *newCmdOptions) common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) @@ -475,7 +475,7 @@ func run(ctx context.Context, containerBackend container_backend.ContainerBacken logboek.Context(ctx).Default().LogOptionalLn() - conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, imagesToProcess) + conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, werfConfig, imagesToProcess) if err != nil { return err } diff --git a/cmd/werf/converge/converge.go b/cmd/werf/converge/converge.go index ca17f72f41..246275a737 100644 --- a/cmd/werf/converge/converge.go +++ b/cmd/werf/converge/converge.go @@ -92,7 +92,7 @@ werf converge --repo registry.mydomain.com/web --env production`, common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupSecondaryStagesStorageOptions(&commonCmdData, cmd) @@ -324,7 +324,7 @@ func run( imagesRepo = storageManager.GetServiceValuesRepo() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return ctx, err } diff --git a/cmd/werf/export/export.go b/cmd/werf/export/export.go index c4c2be7433..9792534a5c 100644 --- a/cmd/werf/export/export.go +++ b/cmd/werf/export/export.go @@ -109,7 +109,7 @@ func NewExportCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) @@ -230,7 +230,7 @@ func run(ctx context.Context, imageNameListFromArgs, tagTemplateList []string, e logboek.Context(ctx).Info().LogOptionalLn() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/helm/get_autogenerated_values.go b/cmd/werf/helm/get_autogenerated_values.go index f6f9d00f39..047a801d3d 100644 --- a/cmd/werf/helm/get_autogenerated_values.go +++ b/cmd/werf/helm/get_autogenerated_values.go @@ -75,6 +75,8 @@ func NewGetAutogeneratedValuesCmd(ctx context.Context) *cobra.Command { common.SetupIntrospectBeforeError(&commonCmdData, cmd) common.SetupIntrospectStage(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) + common.SetupSecondaryStagesStorageOptions(&commonCmdData, cmd) common.SetupCacheStagesStorageOptions(&commonCmdData, cmd) common.SetupRepoOptions(&commonCmdData, cmd, common.RepoDataOptions{OptionalRepo: true}) @@ -222,7 +224,7 @@ func runGetServiceValues(ctx context.Context, imageNameListFromArgs []string) er imagesRepository = storageManager.GetServiceValuesRepo() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/kube_run/kube_run.go b/cmd/werf/kube_run/kube_run.go index bfe33294a5..c56bb62ed8 100644 --- a/cmd/werf/kube_run/kube_run.go +++ b/cmd/werf/kube_run/kube_run.go @@ -171,7 +171,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) @@ -381,7 +381,7 @@ func run(ctx context.Context, pod, secret, namespace string, werfConfig *config. logboek.Context(ctx).Info().LogOptionalLn() - conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, imagesToProcess) + conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, werfConfig, imagesToProcess) if err != nil { return err } diff --git a/cmd/werf/lint/lint.go b/cmd/werf/lint/lint.go index 57efb0abb2..2c08af59cf 100644 --- a/cmd/werf/lint/lint.go +++ b/cmd/werf/lint/lint.go @@ -85,6 +85,8 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupIntrospectBeforeError(&commonCmdData, cmd) common.SetupIntrospectStage(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) + common.SetupSecondaryStagesStorageOptions(&commonCmdData, cmd) common.SetupCacheStagesStorageOptions(&commonCmdData, cmd) common.SetupRepoOptions(&commonCmdData, cmd, common.RepoDataOptions{OptionalRepo: true}) @@ -255,7 +257,7 @@ func runLint(ctx context.Context, imageNameListFromArgs []string) error { imagesRepository = storageManager.GetServiceValuesRepo() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/plan/plan.go b/cmd/werf/plan/plan.go index 319459d255..bba72ba59c 100644 --- a/cmd/werf/plan/plan.go +++ b/cmd/werf/plan/plan.go @@ -130,7 +130,7 @@ werf plan --repo registry.mydomain.com/web --env production`, common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupDisableAutoHostCleanup(&commonCmdData, cmd) @@ -346,7 +346,7 @@ func run( imagesRepository = storageManager.GetServiceValuesRepo() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return ctx, err } diff --git a/cmd/werf/render/render.go b/cmd/werf/render/render.go index b54a7e5fb4..9f037281a2 100644 --- a/cmd/werf/render/render.go +++ b/cmd/werf/render/render.go @@ -121,7 +121,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) commonCmdData.SetupPlatform(cmd) @@ -266,7 +266,7 @@ func runRender(ctx context.Context, imageNameListFromArgs []string) error { imagesRepository = storageManager.GetServiceValuesRepo() - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/run/run.go b/cmd/werf/run/run.go index 432e1ae597..9982d0386a 100644 --- a/cmd/werf/run/run.go +++ b/cmd/werf/run/run.go @@ -137,7 +137,7 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupAnnotateLayersWithDmVerityRootHash(&commonCmdData, cmd) common.SetupSigningOptions(&commonCmdData, cmd) - common.SetupFilesSigningOptions(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupELFSigningOptions(&commonCmdData, cmd) common.SetupRequireBuiltImages(&commonCmdData, cmd) @@ -365,7 +365,7 @@ func run(ctx context.Context, containerBackend container_backend.ContainerBacken logboek.Context(ctx).Info().LogOptionalLn() - conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, imagesToProcess) + conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, werfConfig, imagesToProcess) if err != nil { return err } diff --git a/cmd/werf/sbom/get/get.go b/cmd/werf/sbom/get/get.go index b13ca69525..8321171171 100644 --- a/cmd/werf/sbom/get/get.go +++ b/cmd/werf/sbom/get/get.go @@ -97,6 +97,8 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupIntrospectBeforeError(&commonCmdData, cmd) common.SetupIntrospectStage(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) + common.SetupSecondaryStagesStorageOptions(&commonCmdData, cmd) common.SetupCacheStagesStorageOptions(&commonCmdData, cmd) common.SetupRepoOptions(&commonCmdData, cmd, common.RepoDataOptions{OptionalRepo: true}) @@ -360,7 +362,7 @@ func run(ctx context.Context, containerBackend container_backend.ContainerBacken buildOptions.SkipImageMetadataPublication = *commonCmdData.Dev - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imagesToProcess, buildOptions) if err != nil { return err } diff --git a/cmd/werf/stage/image/main.go b/cmd/werf/stage/image/main.go index ab981feb1e..3224d9e2ce 100644 --- a/cmd/werf/stage/image/main.go +++ b/cmd/werf/stage/image/main.go @@ -69,6 +69,8 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupCacheStagesStorageOptions(&commonCmdData, cmd) common.SetupRepoOptions(&commonCmdData, cmd, common.RepoDataOptions{OptionalRepo: true}) common.SetupFinalRepo(&commonCmdData, cmd) + + common.SetupSignBuildFileOptions(&commonCmdData, cmd) common.SetupMetaRepo(&commonCmdData, cmd) common.SetupDockerConfig(&commonCmdData, cmd, "Command needs granted permissions to read and pull images from the specified stages storage") @@ -163,7 +165,7 @@ func run(ctx context.Context, imageName string) error { return fmt.Errorf("unable to init storage manager: %w", err) } - conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, imagesToProcess) + conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, werfConfig, imagesToProcess) if err != nil { return err } diff --git a/cmd/werf/stages/copy/copy.go b/cmd/werf/stages/copy/copy.go index c557ca369e..c02878a723 100644 --- a/cmd/werf/stages/copy/copy.go +++ b/cmd/werf/stages/copy/copy.go @@ -114,6 +114,8 @@ func NewCmd(ctx context.Context) *cobra.Command { common.SetupIntrospectBeforeError(&commonCmdData, cmd) common.SetupIntrospectStage(&commonCmdData, cmd) + common.SetupSignBuildFileOptions(&commonCmdData, cmd) + common.SetupSaveBuildReport(&commonCmdData, cmd) common.SetupBuildReportPath(&commonCmdData, cmd) common.SetupBuildReportOperations(&commonCmdData, cmd) @@ -261,7 +263,7 @@ func initConveyorComponents(ctx context.Context, werfConfig *config.WerfConfig, return nil, build.BuildOptions{}, fmt.Errorf("unable to get build options: %w", err) } - conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imageToProcess, buildOptions) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, werfConfig, imageToProcess, buildOptions) if err != nil { return nil, build.BuildOptions{}, fmt.Errorf("unable to get conveyor options: %w", err) } diff --git a/docs/_includes/reference/cli/werf_build.md b/docs/_includes/reference/cli/werf_build.md index 9c32190e24..0b6652aded 100644 --- a/docs/_includes/reference/cli/werf_build.md +++ b/docs/_includes/reference/cli/werf_build.md @@ -274,44 +274,37 @@ werf build [IMAGE_NAME...] [options] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_bundle_publish.md b/docs/_includes/reference/cli/werf_bundle_publish.md index 3b8461987a..da64f60b89 100644 --- a/docs/_includes/reference/cli/werf_bundle_publish.md +++ b/docs/_includes/reference/cli/werf_bundle_publish.md @@ -322,44 +322,37 @@ werf bundle publish [IMAGE_NAME...] [options] with commas: key1=val1,key2=val2). Also, can be defined with $WERF_SET_STRING or $WERF_SET_STRING_* (e.g. $WERF_SET_STRING_1=key1=val1, $WERF_SET_STRING_2=key2=val2) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_config.md b/docs/_includes/reference/cli/werf_compose_config.md index 75b1f39ab6..94d9dfc64e 100644 --- a/docs/_includes/reference/cli/werf_compose_config.md +++ b/docs/_includes/reference/cli/werf_compose_config.md @@ -266,44 +266,37 @@ werf compose config [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_down.md b/docs/_includes/reference/cli/werf_compose_down.md index 946da9edb6..76913a2e30 100644 --- a/docs/_includes/reference/cli/werf_compose_down.md +++ b/docs/_includes/reference/cli/werf_compose_down.md @@ -259,44 +259,37 @@ werf compose down [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_run.md b/docs/_includes/reference/cli/werf_compose_run.md index ecd19fda0e..94dced3729 100644 --- a/docs/_includes/reference/cli/werf_compose_run.md +++ b/docs/_includes/reference/cli/werf_compose_run.md @@ -256,44 +256,37 @@ werf compose run [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_compose_up.md b/docs/_includes/reference/cli/werf_compose_up.md index 0bf1cb3056..6cfbd63366 100644 --- a/docs/_includes/reference/cli/werf_compose_up.md +++ b/docs/_includes/reference/cli/werf_compose_up.md @@ -267,44 +267,37 @@ werf compose up [IMAGE_NAME...] [options] [--docker-compose-options="OPTIONS"] [ cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_converge.md b/docs/_includes/reference/cli/werf_converge.md index f91478f3df..41bec32ddd 100644 --- a/docs/_includes/reference/cli/werf_converge.md +++ b/docs/_includes/reference/cli/werf_converge.md @@ -515,44 +515,37 @@ werf converge --repo registry.mydomain.com/web --env production with commas: key1=val1,key2=val2). Also, can be defined with $WERF_SET_STRING or $WERF_SET_STRING_* (e.g. $WERF_SET_STRING_1=key1=val1, $WERF_SET_STRING_2=key2=val2) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_export.md b/docs/_includes/reference/cli/werf_export.md index 7d75b449a3..0d85d73d29 100644 --- a/docs/_includes/reference/cli/werf_export.md +++ b/docs/_includes/reference/cli/werf_export.md @@ -236,44 +236,37 @@ werf export [IMAGE_NAME...] [options] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_helm_get_autogenerated_values.md b/docs/_includes/reference/cli/werf_helm_get_autogenerated_values.md index f0c6ff8fd4..87acad8053 100644 --- a/docs/_includes/reference/cli/werf_helm_get_autogenerated_values.md +++ b/docs/_includes/reference/cli/werf_helm_get_autogenerated_values.md @@ -209,6 +209,31 @@ werf helm get-autogenerated-values [IMAGE_NAME...] [options] $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) --set-docker-config-json-value=false Shortcut to set current docker config into the .Values.global.werf.dockerconfigjson + --sign-build-file-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --skip-tls-verify-registry=false Skip TLS certificate validation when accessing a registry (default $WERF_SKIP_TLS_VERIFY_REGISTRY) diff --git a/docs/_includes/reference/cli/werf_kube_run.md b/docs/_includes/reference/cli/werf_kube_run.md index a14b11d5e4..2374f2eea7 100644 --- a/docs/_includes/reference/cli/werf_kube_run.md +++ b/docs/_includes/reference/cli/werf_kube_run.md @@ -303,44 +303,37 @@ werf kube-run [options] [IMAGE_NAME] [-- COMMAND ARG...] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_lint.md b/docs/_includes/reference/cli/werf_lint.md index ce660a9ea9..eada3f49dc 100644 --- a/docs/_includes/reference/cli/werf_lint.md +++ b/docs/_includes/reference/cli/werf_lint.md @@ -417,6 +417,31 @@ werf lint [IMAGE_NAME...] [options] with commas: key1=val1,key2=val2). Also, can be defined with $WERF_SET_STRING or $WERF_SET_STRING_* (e.g. $WERF_SET_STRING_1=key1=val1, $WERF_SET_STRING_2=key2=val2) + --sign-build-file-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) -L, --skip-dependencies-repo-refresh=false Do not refresh helm chart repositories locally cached index --skip-tls-verify-helm-dependencies=false diff --git a/docs/_includes/reference/cli/werf_plan.md b/docs/_includes/reference/cli/werf_plan.md index f4a3c1793f..dbeb772060 100644 --- a/docs/_includes/reference/cli/werf_plan.md +++ b/docs/_includes/reference/cli/werf_plan.md @@ -500,44 +500,37 @@ werf plan --repo registry.mydomain.com/web --env production Show sensitive diff lines ($WERF_SHOW_SENSITIVE_DIFFS by default) --show-verbose-crd-diffs=false Show verbose CRD diff lines ($WERF_SHOW_VERBOSE_CRD_DIFFS by default) - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_render.md b/docs/_includes/reference/cli/werf_render.md index ef267827b8..aa36bec92e 100644 --- a/docs/_includes/reference/cli/werf_render.md +++ b/docs/_includes/reference/cli/werf_render.md @@ -401,44 +401,37 @@ werf render [IMAGE_NAME...] [options] $WERF_SET_STRING_1=key1=val1, $WERF_SET_STRING_2=key2=val2) -s, --show-only=[] only show manifests rendered from the given templates - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_run.md b/docs/_includes/reference/cli/werf_run.md index 7a6cf955fa..d85753a68e 100644 --- a/docs/_includes/reference/cli/werf_run.md +++ b/docs/_includes/reference/cli/werf_run.md @@ -209,44 +209,37 @@ werf run [options] [IMAGE_NAME] [-- COMMAND ARG...] $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) --shell=false Use predefined docker options and command for debug - --sign-cert="" - The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) - --sign-elf-files=false - Enable ELF files signing (default $WERF_SIGN_ELF_FILES). - When enabled, the private signing key must be specified with --sign-key option and the - certificate must be specified with --sign-cert option - --sign-files=true - Sign the files declared with the sign directive of werf.yaml (default $WERF_SIGN_FILES - or true). - When enabled and at least one image declares files to sign, - the private signing key must be specified with --sign-files-key option and - the certificate must be specified with --sign-files-cert option. - Disable to build such images without signatures - --sign-files-cert="" + --sign-build-file-cert="" The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM - (default $WERF_SIGN_FILES_CERT) - --sign-files-intermediates="" + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" The intermediate certificates of the files signing key as path to PEM file or - base64-encoded PEM (default $WERF_SIGN_FILES_INTERMEDIATES) - --sign-files-key="" + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" The private key to sign files with as path to PEM file, base64-encoded PEM or - hashivault://[KEY] (default $WERF_SIGN_FILES_KEY) - --sign-files-vault-addr="" + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" The Vault address a hashivault:// files signing key is resolved against (default - $WERF_SIGN_FILES_VAULT_ADDR, then $VAULT_ADDR) - --sign-files-vault-auth-jwt="" + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" The JWT to log in to Vault with for the files signing key; when unset the Vault authentication is taken from the environment, as for --sign-key (default - $WERF_SIGN_FILES_VAULT_AUTH_JWT) - --sign-files-vault-auth-path="" - The Vault JWT auth mount for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_PATH, then jwt) - --sign-files-vault-auth-role="" - The Vault JWT role for --sign-files-vault-auth-jwt (default - $WERF_SIGN_FILES_VAULT_AUTH_ROLE) - --sign-files-vault-transit-path="" + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" The Vault transit engine mount of the files signing key (default - $WERF_SIGN_FILES_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) + --sign-cert="" + The leaf certificate as path to PEM file or base64-encoded PEM (default $WERF_SIGN_CERT) + --sign-elf-files=false + Enable ELF files signing (default $WERF_SIGN_ELF_FILES). + When enabled, the private signing key must be specified with --sign-key option and the + certificate must be specified with --sign-cert option --sign-intermediates="" The intermediate certificates as path to PEM file or base64-encoded PEM (default $WERF_SIGN_INTERMEDIATES) diff --git a/docs/_includes/reference/cli/werf_sbom_get.md b/docs/_includes/reference/cli/werf_sbom_get.md index 97ba599c3c..2f536b1d3f 100644 --- a/docs/_includes/reference/cli/werf_sbom_get.md +++ b/docs/_includes/reference/cli/werf_sbom_get.md @@ -215,6 +215,31 @@ werf sbom get [IMAGE_NAME] [options] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) + --sign-build-file-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --skip-tls-verify-registry=false Skip TLS certificate validation when accessing a registry (default $WERF_SKIP_TLS_VERIFY_REGISTRY) diff --git a/docs/_includes/reference/cli/werf_stages_copy.md b/docs/_includes/reference/cli/werf_stages_copy.md index 6859f290be..6125a152ba 100644 --- a/docs/_includes/reference/cli/werf_stages_copy.md +++ b/docs/_includes/reference/cli/werf_stages_copy.md @@ -235,6 +235,31 @@ werf stages copy [options] cache. Also, can be specified with $WERF_SECONDARY_REPO or $WERF_SECONDARY_REPO_* (e.g. $WERF_SECONDARY_REPO_1=..., $WERF_SECONDARY_REPO_2=...) + --sign-build-file-cert="" + The leaf certificate of the files signing key as path to PEM file or base64-encoded PEM + (default $WERF_SIGN_BUILD_FILE_CERT) + --sign-build-file-intermediates="" + The intermediate certificates of the files signing key as path to PEM file or + base64-encoded PEM (default $WERF_SIGN_BUILD_FILE_INTERMEDIATES) + --sign-build-file-key="" + The private key to sign files with as path to PEM file, base64-encoded PEM or + hashivault://[KEY] (default $WERF_SIGN_BUILD_FILE_KEY) + --sign-build-file-vault-addr="" + The Vault address a hashivault:// files signing key is resolved against (default + $WERF_SIGN_BUILD_FILE_VAULT_ADDR, then $VAULT_ADDR) + --sign-build-file-vault-auth-jwt="" + The JWT to log in to Vault with for the files signing key; when unset the Vault + authentication is taken from the environment, as for --sign-key (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT) + --sign-build-file-vault-auth-path="" + The Vault JWT auth mount for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH, then jwt) + --sign-build-file-vault-auth-role="" + The Vault JWT role for --sign-build-file-vault-auth-jwt (default + $WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE) + --sign-build-file-vault-transit-path="" + The Vault transit engine mount of the files signing key (default + $WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH, then $TRANSIT_SECRET_ENGINE_PATH) --skip-tls-verify-registry=false Skip TLS certificate validation when accessing a registry (default $WERF_SKIP_TLS_VERIFY_REGISTRY) diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index d44ee99595..232381d268 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -117,19 +117,24 @@ The key and the certificate are never part of `werf.yaml`; they come from the co | Flag | Environment variable | Description | |---|---|---| -| `--sign-files` | `WERF_SIGN_FILES` | process `sign` directives (default `true`); `false` skips them and builds the image without the signatures | -| `--sign-files-key` | `WERF_SIGN_FILES_KEY` | the private key: a path to a PEM file, a base64-encoded PEM, or `hashivault://[KEY]` | -| `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | the signing certificate: a path to a PEM file or a base64-encoded PEM | -| `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | the intermediate certificates: a path to a PEM file or a base64-encoded PEM | -| `--sign-files-vault-addr` | `WERF_SIGN_FILES_VAULT_ADDR` | the Vault address a `hashivault://` key is resolved against; unset → `VAULT_ADDR` | -| `--sign-files-vault-transit-path` | `WERF_SIGN_FILES_VAULT_TRANSIT_PATH` | the transit engine mount of the key; unset → `TRANSIT_SECRET_ENGINE_PATH` | -| `--sign-files-vault-auth-jwt` | `WERF_SIGN_FILES_VAULT_AUTH_JWT` | the JWT to log in to Vault with; unset → the authentication `--sign-key` uses, from the environment | -| `--sign-files-vault-auth-role` | `WERF_SIGN_FILES_VAULT_AUTH_ROLE` | the Vault JWT role for that JWT | -| `--sign-files-vault-auth-path` | `WERF_SIGN_FILES_VAULT_AUTH_PATH` | the JWT auth mount for that JWT; unset → `jwt` | - -The `--sign-files-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`; each option left unset falls back to what `--sign-key` would use. - -A build of an image that declares `sign` fails before any stage runs when the key or the certificate is missing and `--sign-files` is not `false`. Commands that do not build images (`lint`, `stages copy`, `sbom get`) have no `--sign-files` flag and treat it as off: they accept such a `werf.yaml` without a key. +| `--sign-build-file-key` | `WERF_SIGN_BUILD_FILE_KEY` | the private key: a path to a PEM file, a base64-encoded PEM, or `hashivault://[KEY]` | +| `--sign-build-file-cert` | `WERF_SIGN_BUILD_FILE_CERT` | the signing certificate: a path to a PEM file or a base64-encoded PEM | +| `--sign-build-file-intermediates` | `WERF_SIGN_BUILD_FILE_INTERMEDIATES` | the intermediate certificates: a path to a PEM file or a base64-encoded PEM | +| `--sign-build-file-vault-addr` | `WERF_SIGN_BUILD_FILE_VAULT_ADDR` | the Vault address a `hashivault://` key is resolved against; unset → `VAULT_ADDR` | +| `--sign-build-file-vault-transit-path` | `WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH` | the transit engine mount of the key; unset → `TRANSIT_SECRET_ENGINE_PATH` | +| `--sign-build-file-vault-auth-jwt` | `WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT` | the JWT to log in to Vault with; unset → the authentication `--sign-key` uses, from the environment | +| `--sign-build-file-vault-auth-role` | `WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE` | the Vault JWT role; qualifies the JWT above and is rejected without it | +| `--sign-build-file-vault-auth-path` | `WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH` | the JWT auth mount; qualifies the JWT above and is rejected without it; unset → `jwt` | + +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. + +There is no way to build such an image unsigned: a declared `sign` is a requirement of the image, not a build-time choice. + +The certificate and the private key are needed at different moments. The certificate is the public identity of the signature and takes part in the stage digest, so every command that works out the image's stages asks for it — including `--require-built-images`, which only resolves an already built image. The private key produces the signature and is needed only where a signature is actually produced: resolving, rendering or deploying an image built earlier needs no key and no access to the KMS. + +A build of an image that declares `sign` fails before any stage runs when the certificate is missing. Without the private key it fails at the first signature it would have to produce. With `hashivault://` the private key never leaves the KMS. The certificate is not fetched from the KMS: when it is issued by a PKI (Vault PKI, for example), the pipeline retrieves the PEM itself and passes it as a path or base64. The stage container always runs with `network: none`, whatever the image's `network` setting, and none of the image's commands run in it, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index 228019ce19..577764d3ef 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -117,19 +117,24 @@ werf читает файл из собранного к этому момент | Флаг | Переменная окружения | Описание | |---|---|---| -| `--sign-files` | `WERF_SIGN_FILES` | обрабатывать директивы `sign` (по умолчанию `true`); `false` пропускает их и собирает образ без подписей | -| `--sign-files-key` | `WERF_SIGN_FILES_KEY` | приватный ключ: путь к PEM-файлу, PEM в base64 или `hashivault://[KEY]` | -| `--sign-files-cert` | `WERF_SIGN_FILES_CERT` | сертификат подписанта: путь к PEM-файлу или PEM в base64 | -| `--sign-files-intermediates` | `WERF_SIGN_FILES_INTERMEDIATES` | промежуточные сертификаты: путь к PEM-файлу или PEM в base64 | -| `--sign-files-vault-addr` | `WERF_SIGN_FILES_VAULT_ADDR` | адрес Vault, в котором ищется ключ `hashivault://`; не задан → `VAULT_ADDR` | -| `--sign-files-vault-transit-path` | `WERF_SIGN_FILES_VAULT_TRANSIT_PATH` | mount transit-движка с ключом; не задан → `TRANSIT_SECRET_ENGINE_PATH` | -| `--sign-files-vault-auth-jwt` | `WERF_SIGN_FILES_VAULT_AUTH_JWT` | JWT для входа в Vault; не задан → та же аутентификация из окружения, что у `--sign-key` | -| `--sign-files-vault-auth-role` | `WERF_SIGN_FILES_VAULT_AUTH_ROLE` | роль Vault JWT для этого JWT | -| `--sign-files-vault-auth-path` | `WERF_SIGN_FILES_VAULT_AUTH_PATH` | mount JWT-аутентификации для этого JWT; не задан → `jwt` | - -Опции `--sign-files-vault-*` нужны, чтобы подписант файлов и подписант `--sign-key` могли жить в разных mount Vault и входить под разными ролями в рамках одного `werf build`; каждая незаданная опция берёт то, что использовал бы `--sign-key`. - -Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если ключ или сертификат не заданы, а `--sign-files` не равен `false`. Команды, которые не собирают образы (`lint`, `stages copy`, `sbom get`), не имеют флага `--sign-files` и считают его выключенным: такой `werf.yaml` они принимают без ключа. +| `--sign-build-file-key` | `WERF_SIGN_BUILD_FILE_KEY` | приватный ключ: путь к PEM-файлу, PEM в base64 или `hashivault://[KEY]` | +| `--sign-build-file-cert` | `WERF_SIGN_BUILD_FILE_CERT` | сертификат подписанта: путь к PEM-файлу или PEM в base64 | +| `--sign-build-file-intermediates` | `WERF_SIGN_BUILD_FILE_INTERMEDIATES` | промежуточные сертификаты: путь к PEM-файлу или PEM в base64 | +| `--sign-build-file-vault-addr` | `WERF_SIGN_BUILD_FILE_VAULT_ADDR` | адрес Vault, в котором ищется ключ `hashivault://`; не задан → `VAULT_ADDR` | +| `--sign-build-file-vault-transit-path` | `WERF_SIGN_BUILD_FILE_VAULT_TRANSIT_PATH` | mount transit-движка с ключом; не задан → `TRANSIT_SECRET_ENGINE_PATH` | +| `--sign-build-file-vault-auth-jwt` | `WERF_SIGN_BUILD_FILE_VAULT_AUTH_JWT` | JWT для входа в Vault; не задан → та же аутентификация из окружения, что у `--sign-key` | +| `--sign-build-file-vault-auth-role` | `WERF_SIGN_BUILD_FILE_VAULT_AUTH_ROLE` | роль Vault JWT; уточняет JWT выше, без него отвергается | +| `--sign-build-file-vault-auth-path` | `WERF_SIGN_BUILD_FILE_VAULT_AUTH_PATH` | mount JWT-аутентификации; уточняет JWT выше, без него отвергается; не задан → `jwt` | + +Опции `--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 сами по себе отвергаются. + +Собрать такой образ без подписей нельзя: объявленная директива `sign` — требование образа, а не опция сборки. + +Сертификат и приватный ключ нужны в разные моменты. Сертификат — публичная идентичность подписи, он участвует в дайджесте стадии, поэтому его запрашивает любая команда, которая строит цепочку стадий образа, включая `--require-built-images`, которая только находит уже собранный образ. Приватный ключ создаёт подпись и нужен лишь там, где подпись действительно создаётся: чтобы найти, отрендерить или задеплоить собранный ранее образ, ни ключ, ни доступ к KMS не требуются. + +Сборка образа с директивой `sign` завершается ошибкой ещё до запуска стадий, если не задан сертификат. Без приватного ключа она падает на первой подписи, которую должна была бы создать. С `hashivault://` приватный ключ не покидает KMS. Сертификат из KMS не запрашивается: если его выпускает PKI (например, Vault PKI), пайплайн сам получает PEM и передаёт его путём или в base64. Контейнер стадии всегда запускается с `network: none` независимо от настройки `network` образа, и команды образа в нём не выполняются, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index a2a3642657..aa50f8f128 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -64,7 +64,6 @@ type BuildOptions struct { SbomSigningOptions signing.SbomSigningOptions VexSigningOptions signing.VexSigningOptions ELFSigningOptions signing.ELFSigningOptions - FilesSigningOptions signing.FilesSigningOptions VerityAnnotationOptions verify_annotation.Options ReportPath string diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index 7072143093..3454dfb993 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -149,7 +149,7 @@ sign: } enabled := func() signing.FilesSigningOptions { - o := signing.NewFilesSigningOptions(&signing.Signer{}) + o := signing.NewFilesSigningOptions("cert", &signing.Signer{}) o.Enabled = true return o } @@ -170,7 +170,7 @@ sign: }) It("generates no stage when files signing is disabled", func() { - image := newImage(context.Background(), "", signing.NewFilesSigningOptions(&signing.Signer{})) + image := newImage(context.Background(), "", signing.NewFilesSigningOptions("cert", &signing.Signer{})) for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { Expect(stageNames(image)).NotTo(ContainElement(name)) diff --git a/pkg/build/signing/files_signing.go b/pkg/build/signing/files_signing.go index 28c895c1fa..625e06ee8e 100644 --- a/pkg/build/signing/files_signing.go +++ b/pkg/build/signing/files_signing.go @@ -16,8 +16,14 @@ import ( "github.com/smallstep/pkcs7" ) +// FilesSigningOptions carries what the sign directive needs at two different moments. +// Cert is the public identity of a signature and takes part in the stage digest, so it +// is known wherever the stage chain is built, including a command that only resolves an +// already signed image. The signer holds the private key and is nil unless one was +// configured: it is required only where a signature is actually produced. type FilesSigningOptions struct { Enabled bool + Cert string signer *Signer } @@ -26,8 +32,8 @@ func (o FilesSigningOptions) Signer() *Signer { return o.signer } -func NewFilesSigningOptions(signer *Signer) FilesSigningOptions { - return FilesSigningOptions{signer: signer} +func NewFilesSigningOptions(cert string, signer *Signer) FilesSigningOptions { + return FilesSigningOptions{Cert: cert, signer: signer} } // SignDetachedPKCS7 wraps an RSASSA-PKCS1-v1_5/SHA-256 signature of content into a diff --git a/pkg/build/signing/files_signing_test.go b/pkg/build/signing/files_signing_test.go index a254d622dd..d5c72c16a5 100644 --- a/pkg/build/signing/files_signing_test.go +++ b/pkg/build/signing/files_signing_test.go @@ -68,32 +68,27 @@ func newTestKeyPair() (*rsa.PrivateKey, []byte) { return key, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) } -func newTestSigner(ctx context.Context) (*Signer, *rsa.PrivateKey) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - Expect(err).NotTo(HaveOccurred()) - - template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: "files-signing-test"}, - NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, - } - certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) - Expect(err).NotTo(HaveOccurred()) - +func encryptedKeyPEM(key *rsa.PrivateKey) []byte { keyDER, err := x509.MarshalPKCS8PrivateKey(key) Expect(err).NotTo(HaveOccurred()) encryptedKey, err := encrypted.Encrypt(keyDER, nil) Expect(err).NotTo(HaveOccurred()) - keyPEM := pem.EncodeToMemory(&pem.Block{Type: signver.PrivateKeyPemType, Bytes: encryptedKey}) - certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + return pem.EncodeToMemory(&pem.Block{Type: signver.PrivateKeyPemType, Bytes: encryptedKey}) +} + +func newTestSignerWithCert(ctx context.Context) (*Signer, *rsa.PrivateKey, []byte) { + key, certPEM := newTestKeyPair() signer, err := NewSigner(ctx, SignerOptions{ - KeyRef: base64.StdEncoding.EncodeToString(keyPEM), + KeyRef: base64.StdEncoding.EncodeToString(encryptedKeyPEM(key)), CertRef: base64.StdEncoding.EncodeToString(certPEM), }) Expect(err).NotTo(HaveOccurred()) + return signer, key, certPEM +} + +func newTestSigner(ctx context.Context) (*Signer, *rsa.PrivateKey) { + signer, key, _ := newTestSignerWithCert(ctx) return signer, key } @@ -172,6 +167,18 @@ var _ = Describe("SignDetachedPKCS7", func() { Expect(err).To(HaveOccurred(), "openssl must reject the signature over different content") }) + It("reads the certificate of a reference exactly as the signer reports it", func(ctx SpecContext) { + signer, _, certPEM := newTestSignerWithCert(ctx) + + certFile := filepath.Join(GinkgoT().TempDir(), "leaf.pem") + Expect(os.WriteFile(certFile, certPEM, 0o600)).To(Succeed()) + + for _, ref := range []string{base64.StdEncoding.EncodeToString(certPEM), certFile} { + Expect(ReadCertificatePEM(ref)).To(Equal(signer.Cert()), + "the stage digest must not depend on how the certificate was passed, nor on whether a key was configured") + } + }) + It("fails when the signer is not configured", func(ctx SpecContext) { _, err := SignDetachedPKCS7(ctx, content, &Signer{}) Expect(err).To(MatchError(ContainSubstring("signer is not configured"))) diff --git a/pkg/build/signing/signer.go b/pkg/build/signing/signer.go index 450640a4d6..f9f10cbe0c 100644 --- a/pkg/build/signing/signer.go +++ b/pkg/build/signing/signer.go @@ -1,13 +1,16 @@ package signing import ( + "bytes" "context" "crypto/sha256" "crypto/x509" + "encoding/pem" "fmt" "sync" "github.com/deckhouse/delivery-kit-sdk/pkg/signver" + "github.com/deckhouse/delivery-kit-sdk/pkg/signver/blob" "github.com/deckhouse/delivery-kit-sdk/pkg/signver/hashivault" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -97,6 +100,37 @@ func (s *Signer) Chain() string { return string(s.sv.Chain) } +// ReadCertificatePEM resolves a certificate reference — a file path, base64 or raw PEM, +// DER as well — into the same normalized PEM a Signer built from that reference reports. +// The certificate is the public identity of a signature, so it has to be readable +// without the private key the signer needs. +func ReadCertificatePEM(ref string) (string, error) { + data, err := blob.LoadBase64OrFile(ref) + if err != nil { + return "", fmt.Errorf("read certificate %q: %w", ref, err) + } + + if bytes.HasPrefix(data, []byte("-----")) { + decoded, _ := pem.Decode(data) + if decoded == nil || decoded.Type != "CERTIFICATE" { + return "", fmt.Errorf("certificate %q is not a PEM certificate", ref) + } + data = decoded.Bytes + } + + cert, err := x509.ParseCertificate(data) + if err != nil { + return "", fmt.Errorf("parse certificate %q: %w", ref, err) + } + + certPEM, err := cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return "", fmt.Errorf("encode certificate %q: %w", ref, err) + } + + return string(certPEM), nil +} + func NewSigner(ctx context.Context, opts SignerOptions) (*Signer, error) { if opts.IsZero() { return &Signer{}, nil diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index 96d598e310..3075a3ef51 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -94,7 +94,7 @@ func (s *SignFilesStage) digest() string { for _, sign := range s.signs { args = append(args, "Path", sign.Path) } - args = append(args, "Cert", s.options.Signer().Cert()) + args = append(args, "Cert", s.options.Cert) return util.Sha256Hash(args...) } @@ -103,6 +103,10 @@ func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb contai return err } + if s.options.Signer() == nil { + return fmt.Errorf("image %q declares files to sign and this stage is not built yet: specify the private key with --sign-build-file-key", s.imageName) + } + signaturesDir := filepath.Join(s.imageTmpDir, "sign", string(s.Name())) if err := os.MkdirAll(signaturesDir, os.ModePerm); err != nil { return fmt.Errorf("create signatures dir %s: %w", signaturesDir, err) diff --git a/pkg/build/stage/sign_files_test.go b/pkg/build/stage/sign_files_test.go index 76a3fcaabf..d206620fd1 100644 --- a/pkg/build/stage/sign_files_test.go +++ b/pkg/build/stage/sign_files_test.go @@ -47,17 +47,17 @@ var _ = Describe("SignFilesStage", func() { } enabledOptions := func(signer *signing.Signer) signing.FilesSigningOptions { - options := signing.NewFilesSigningOptions(signer) + options := signing.NewFilesSigningOptions(signer.Cert(), signer) options.Enabled = true return options } - digestOf := func(signer *signing.Signer, paths ...string) string { + digestOfOptions := func(options signing.FilesSigningOptions, paths ...string) string { imageConfig := &config.StapelImageBase{} for _, p := range paths { imageConfig.Sign = append(imageConfig.Sign, &config.Sign{Path: p, After: "install"}) } - stage := GenerateSignFilesAfterInstallStage(imageConfig, enabledOptions(signer), &BaseStageOptions{}) + stage := GenerateSignFilesAfterInstallStage(imageConfig, options, &BaseStageOptions{}) Expect(stage).NotTo(BeNil()) digest, err := stage.GetDependencies(context.Background(), nil, nil, nil, nil, nil) @@ -68,6 +68,10 @@ var _ = Describe("SignFilesStage", func() { return digest } + digestOf := func(signer *signing.Signer, paths ...string) string { + return digestOfOptions(enabledOptions(signer), paths...) + } + It("keeps the digest stable for the same paths and signer and changes it with either", func() { signerA := newSigner("a") signerB := newSigner("b") @@ -78,6 +82,15 @@ var _ = Describe("SignFilesStage", func() { Expect(digestOf(signerB, "/out/x")).NotTo(Equal(base), "a different signing certificate must change the digest") }) + It("identifies the stage by the certificate alone, so no private key is needed to resolve it", func() { + signer := newSigner("a") + + withoutKey := signing.NewFilesSigningOptions(signer.Cert(), nil) + withoutKey.Enabled = true + + Expect(digestOfOptions(withoutKey, "/out/x")).To(Equal(digestOf(signer, "/out/x"))) + }) + It("is generated only for the position the directive names and only when enabled", func() { imageConfig := &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", Before: "install"}}} options := enabledOptions(newSigner("a")) diff --git a/test/e2e/build/sign_files_test.go b/test/e2e/build/sign_files_test.go index 8c35fbbe95..ebdd6d0f82 100644 --- a/test/e2e/build/sign_files_test.go +++ b/test/e2e/build/sign_files_test.go @@ -40,8 +40,8 @@ type signFilesTestOptions struct { var _ = Describe("sign directive", Label("e2e", "sign-files", "simple"), func() { signArgs := []string{ - "--sign-files-key", base64.StdEncoding.EncodeToString(signFilesKeyData), - "--sign-files-cert", base64.StdEncoding.EncodeToString(signFilesCertData), + "--sign-build-file-key", base64.StdEncoding.EncodeToString(signFilesKeyData), + "--sign-build-file-cert", base64.StdEncoding.EncodeToString(signFilesCertData), } // readSignedFiles returns the signed file and its .p7s out of the built image. @@ -94,6 +94,29 @@ var _ = Describe("sign directive", Label("e2e", "sign-files", "simple"), func() Expect(err).To(Succeed(), string(out)) } + DescribeTable("resolves a built image by its certificate alone, without the private key", + func(ctx SpecContext, testOpts signFilesTestOptions) { + setupEnv(testOpts.setupEnvOptions) + + SuiteData.InitTestRepo(ctx, "repo0", "sign_files/state") + werfProject := werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0")) + + By("building with key and certificate") + werfProject.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: signArgs}}) + + By("resolving the same image with the certificate only") + out := werfProject.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: []string{ + "--require-built-images", + "--sign-build-file-cert", base64.StdEncoding.EncodeToString(signFilesCertData), + }}}) + Expect(out).To(ContainSubstring("Use previously built image for sysext")) + }, + backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ + ContainerBackendMode: "docker", + WithLocalRepo: true, + }}), + ) + DescribeTable("signs the declared file after the named stage", func(ctx SpecContext, testOpts signFilesTestOptions) { setupEnv(testOpts.setupEnvOptions) @@ -145,24 +168,25 @@ var _ = Describe("sign directive", Label("e2e", "sign-files", "simple"), func() }}), ) - DescribeTable("refuses to build without a key unless signing is switched off", + DescribeTable("refuses to build an image whose signatures it cannot produce", func(ctx SpecContext, testOpts signFilesTestOptions) { setupEnv(testOpts.setupEnvOptions) SuiteData.InitTestRepo(ctx, "repo0", "sign_files/state") werfProject := werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0")) - By("no key: the build stops before any stage") + By("no certificate: the build stops before any stage, the signature has no identity") out, err := werfProject.BuildWithErr(ctx, &werf.BuildOptions{}) Expect(err).To(HaveOccurred()) Expect(out).To(ContainSubstring("images sysext declare files to sign")) Expect(out).NotTo(ContainSubstring("Building stage")) - By("--sign-files=false: the image is built without the signature") - out, err = werfProject.BuildWithErr(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: []string{"--sign-files=false"}}}) - Expect(err).To(HaveOccurred(), "the setup stage asserts the .p7s is present, so it must fail") - Expect(out).NotTo(ContainSubstring("signFilesAfterInstall")) - Expect(out).To(ContainSubstring("Building stage sysext/setup")) + By("certificate without a key: the stage that must sign refuses to build") + out, err = werfProject.BuildWithErr(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: []string{ + "--sign-build-file-cert", base64.StdEncoding.EncodeToString(signFilesCertData), + }}}) + Expect(err).To(HaveOccurred()) + Expect(out).To(ContainSubstring("specify the private key with --sign-build-file-key")) }, backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ ContainerBackendMode: "docker", From d6aacb96431b9732b2b33cd8349f4be96cfcb687 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 22 Oct 2026 11:25:03 +0300 Subject: [PATCH 15/22] fix(build): keep the signing stages offline whatever --backend-network says Both stapel builders let the build-time network win over the one a stage asked for, so --backend-network=bridge opened up the sign stages the conveyor had pinned to none. The pin is what makes the signature trustworthy: nothing of the user's runs in those containers and they must reach nothing. Resolve the networking where the build options of a stage are assembled, and cover the precedence instead of the getter alone. Also pin what the Vault options really do: an explicit JWT replaces the environment authentication as a whole, the role and the auth mount do not fall back to it one by one. The documentation promised a per-option fallback. Signed-off-by: Radmir Khurum --- pkg/build/build_phase.go | 20 +++++++++++-- pkg/build/signing/signer_vault_test.go | 12 ++++++++ pkg/build/stage/base.go | 6 ++++ pkg/build/stage/packages_test.go | 6 +--- pkg/build/stage_build_options_test.go | 41 ++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 pkg/build/stage_build_options_test.go diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index aa50f8f128..144b1e2f1b 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -1957,6 +1957,23 @@ func (phase *BuildPhase) buildStage(ctx context.Context, img *image.Image, stg s return reusedPublishedStage, nil } +// stageBuildOptions resolves the options one stage is built with. A stage that pins its +// own networking — the sign stages run isolated from the network — keeps it: both stapel +// builders let the build-time network win over the one the stage asked for, so +// --backend-network would otherwise open up a stage that must stay closed. +func stageBuildOptions(buildOptions container_backend.BuildOptions, targetPlatform string, stg stage.Interface) container_backend.BuildOptions { + opts := buildOptions + opts.TargetPlatform = targetPlatform + + if n, ok := stg.(interface{ NetworkOverride() string }); ok { + if override := n.NetworkOverride(); override != "" { + opts.Network = override + } + } + + return opts +} + // atomicBuildStageImage reports whether a stage published by another process has been reused // instead of the image this process was building. func (phase *BuildPhase) atomicBuildStageImage(ctx context.Context, img *image.Image, stg stage.Interface) (bool, error) { @@ -1964,8 +1981,7 @@ func (phase *BuildPhase) atomicBuildStageImage(ctx context.Context, img *image.I if stg.IsBuildable() { if err := logboek.Context(ctx).Streams().DoErrorWithTag(fmt.Sprintf("%s/%s", img.LogName(), stg.Name()), img.LogTagStyle(), func() error { - opts := phase.ImageBuildOptions - opts.TargetPlatform = img.TargetPlatform + opts := stageBuildOptions(phase.ImageBuildOptions, img.TargetPlatform, stg) if err := stageImage.Builder.Build(ctx, opts); err != nil { err = container_backend.SanitizeError(err) return fmt.Errorf("error building stage %s: %w", stg.Name(), err) diff --git a/pkg/build/signing/signer_vault_test.go b/pkg/build/signing/signer_vault_test.go index 251ade6ca2..1768ee9fad 100644 --- a/pkg/build/signing/signer_vault_test.go +++ b/pkg/build/signing/signer_vault_test.go @@ -122,6 +122,18 @@ var _ = Describe("NewSigner with hashivault", func() { Expect(vault.keyReads).To(Equal([]string{"/v1/env-transit/keys/engine-key|s.fake"})) }) + It("takes the authentication as a whole from the JWT option, without borrowing role or mount from the environment", func(ctx SpecContext) { + _, err := NewSigner(ctx, SignerOptions{ + KeyRef: "hashivault://engine-key", + CertRef: certRef, + Vault: VaultOptions{Address: vault.URL, TransitPath: "dh-engine-signer", AuthJWT: "files-jwt"}, + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(vault.logins).To(Equal([]fakeVaultLogin{{path: "/v1/auth/jwt/login", role: "", jwt: "files-jwt"}}), + "an explicit JWT replaces the environment authentication entirely: the role and the mount do not fall back to it one by one") + }) + It("keeps the environment authentication when only the address and transit mount are given", func(ctx SpecContext) { _, err := NewSigner(ctx, SignerOptions{ KeyRef: "hashivault://engine-key", diff --git a/pkg/build/stage/base.go b/pkg/build/stage/base.go index 5c6865888e..da4905b392 100644 --- a/pkg/build/stage/base.go +++ b/pkg/build/stage/base.go @@ -131,6 +131,12 @@ func (s *BaseStage) SetNetworkOverride(network string) { s.networkOverride = network } +// NetworkOverride returns the networking mode the stage pins for itself, empty when it +// accepts whatever the image and the command line ask for. +func (s *BaseStage) NetworkOverride() string { + return s.networkOverride +} + func (s *BaseStage) NeedsNetwork() bool { return s.needsNetwork } diff --git a/pkg/build/stage/packages_test.go b/pkg/build/stage/packages_test.go index 6818aa9999..6f9f8b421e 100644 --- a/pkg/build/stage/packages_test.go +++ b/pkg/build/stage/packages_test.go @@ -130,11 +130,7 @@ var _ = Describe("NetworkOverride enforcement", func() { s.SetNetworkOverride(override) } - effective := s.network - if s.networkOverride != "" { - effective = s.networkOverride - } - Expect(effective).To(Equal(expectedEffective)) + Expect(s.Network()).To(Equal(expectedEffective)) }, Entry("override=none takes priority over host", "host", "none", "none"), diff --git a/pkg/build/stage_build_options_test.go b/pkg/build/stage_build_options_test.go new file mode 100644 index 0000000000..f9deb86c9a --- /dev/null +++ b/pkg/build/stage_build_options_test.go @@ -0,0 +1,41 @@ +package build + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/werf/werf/v3/pkg/build/signing" + "github.com/werf/werf/v3/pkg/build/stage" + "github.com/werf/werf/v3/pkg/config" + "github.com/werf/werf/v3/pkg/container_backend" +) + +var _ = Describe("stageBuildOptions", func() { + signStage := func() stage.Interface { + options := signing.NewFilesSigningOptions("cert", &signing.Signer{}) + options.Enabled = true + + s := stage.GenerateSignFilesAfterInstallStage( + &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", After: "install"}}}, + options, + &stage.BaseStageOptions{}, + ) + Expect(s).NotTo(BeNil()) + return s + } + + It("keeps a stage that pins its network closed even when the build asks for another one", func() { + opts := stageBuildOptions(container_backend.BuildOptions{Network: "bridge"}, "linux/amd64", signStage()) + + Expect(opts.Network).To(Equal("none"), "--backend-network must not open up a stage that signs files") + Expect(opts.TargetPlatform).To(Equal("linux/amd64")) + }) + + It("leaves the build network alone for a stage that pins nothing", func() { + plain := stage.NewBaseStage("install", &stage.BaseStageOptions{}) + + opts := stageBuildOptions(container_backend.BuildOptions{Network: "bridge"}, "linux/amd64", plain) + + Expect(opts.Network).To(Equal("bridge")) + }) +}) From e75094cefb99560b7f6180c87161870141e3b57d Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 22 Oct 2026 11:30:19 +0300 Subject: [PATCH 16/22] feat(build): sign at the end of the image when the directive names no stage A sign entry had to name a stage, which forced the user to know which stage happens to produce the file. Signing a file the image ships is the common case and it belongs after the image's filesystem is complete: that is what a dependent image importing the result gets. Make before and after optional, reject them together, and add the stage that covers the entries naming neither, right after the git stages and before the image spec. Signed-off-by: Radmir Khurum --- cmd/werf/common/cmd_data.go | 1 - docs/pages_en/usage/build/stapel/imports.md | 4 ++- docs/pages_ru/usage/build/stapel/imports.md | 4 ++- pkg/build/image/stapel.go | 2 ++ pkg/build/image/stapel_test.go | 6 ++-- pkg/build/stage/sign_files.go | 28 +++++++++++++++---- pkg/config/raw_stapel_image_test.go | 10 ++----- pkg/config/sign.go | 5 ++-- pkg/config/werf_schema_test.go | 12 ++++---- schemas/werf.json | 20 +++++-------- .../state_default_position/werf.yaml | 27 ++++++++++++++++++ test/e2e/build/sign_files_test.go | 21 ++++++++++++++ 12 files changed, 99 insertions(+), 41 deletions(-) create mode 100644 test/e2e/build/_fixtures/sign_files/state_default_position/werf.yaml diff --git a/cmd/werf/common/cmd_data.go b/cmd/werf/common/cmd_data.go index 351c8e5e9c..b793182c1d 100644 --- a/cmd/werf/common/cmd_data.go +++ b/cmd/werf/common/cmd_data.go @@ -123,7 +123,6 @@ type CmdData struct { SignCert *string SignIntermediates *string - SignFiles *bool SignBuildFileKey *string SignBuildFileCert *string SignBuildFileIntermediates *string diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index 232381d268..a56b6aed26 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -97,7 +97,9 @@ 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. 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. +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. + +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 diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index 577764d3ef..b35e612173 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -97,7 +97,9 @@ werf создаёт только родительский каталог для ## Подписание файлов внутри образа -Директива `sign` подписывает файл, который производит образ, и кладёт подпись рядом с ним, не давая сборочной стадии доступ к ключу подписи. Она занимает в цепочке стадий то же место, что и `import`: файл должен существовать после стадии, указанной в `before` или `after`, а подпись доступна последующим стадиям. +Директива `sign` подписывает файл, который производит образ, и кладёт подпись рядом с ним, не давая сборочной стадии доступ к ключу подписи. + +Если не указаны ни `before`, ни `after`, файл подписывается в конце образа, когда его файловая система уже собрана, — обычно именно это и означает «подписанный артефакт образа», и именно это получает другой образ, импортирующий результат. Указание стадии переносит подпись раньше: тогда директива занимает в цепочке стадий то же место, что и `import`, файл должен существовать после стадии из `before` или `after`, а подпись доступна последующим стадиям. Указывать обе — ошибка конфигурации. ```yaml image: sysext diff --git a/pkg/build/image/stapel.go b/pkg/build/image/stapel.go index 158074bf7d..8ff3a06fb8 100644 --- a/pkg/build/image/stapel.go +++ b/pkg/build/image/stapel.go @@ -142,6 +142,8 @@ func initStages(ctx context.Context, image *Image, metaConfig *config.Meta, stap stages = append(stages, stage.NewGitLatestPatchStage(gitPatchStageOptions, baseStageOptions)) } + stages = appendIfExist(stages, stage.GenerateSignFilesStage(imageBaseConfig, opts.FilesSigningOptions, baseStageOptions)) + if imageBaseConfig.ImageSpec != nil { stages = appendIfExist(stages, stage.GenerateImageSpecStage(imageBaseConfig.ImageSpec, baseStageOptions)) } diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index 3454dfb993..d3991825a9 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -135,6 +135,7 @@ sign: before: setup - path: /out/c.roothash after: setup +- path: /out/d.roothash `, sbomBlock) } @@ -166,13 +167,14 @@ sign: stage.SignFilesBeforeSetup, stage.Setup, stage.SignFilesAfterSetup, + stage.SignFiles, })) }) It("generates no stage when files signing is disabled", func() { image := newImage(context.Background(), "", signing.NewFilesSigningOptions("cert", &signing.Signer{})) - for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { + for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup, stage.SignFiles} { Expect(stageNames(image)).NotTo(ContainElement(name)) } }) @@ -181,7 +183,7 @@ sign: func(sbomBlock string) { image := newImage(context.Background(), sbomBlock, enabled()) - for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup} { + for _, name := range []stage.StageName{stage.SignFilesBeforeInstall, stage.SignFilesAfterInstall, stage.SignFilesBeforeSetup, stage.SignFilesAfterSetup, stage.SignFiles} { s := image.GetStage(name) Expect(s).NotTo(BeNil(), string(name)) Expect(s.(*stage.SignFilesStage).Network()).To(Equal("none"), string(name)) diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index 3075a3ef51..eb43161d31 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -26,6 +26,7 @@ const ( SignFilesAfterInstall StageName = "signFilesAfterInstall" SignFilesBeforeSetup StageName = "signFilesBeforeSetup" SignFilesAfterSetup StageName = "signFilesAfterSetup" + SignFiles StageName = "signFiles" ) // SignFilesStage signs the files the sign directive places at one position of the @@ -42,29 +43,44 @@ type SignFilesStage struct { var _ Interface = (*SignFilesStage)(nil) func GenerateSignFilesBeforeInstallStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { - return generateSignFilesStage(imageBaseConfig, SignFilesBeforeInstall, &getImportsOptions{Before: Install}, options, baseStageOptions) + return generateSignFilesStage(imageBaseConfig, SignFilesBeforeInstall, atStageBoundary(&getImportsOptions{Before: Install}), options, baseStageOptions) } func GenerateSignFilesAfterInstallStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { - return generateSignFilesStage(imageBaseConfig, SignFilesAfterInstall, &getImportsOptions{After: Install}, options, baseStageOptions) + return generateSignFilesStage(imageBaseConfig, SignFilesAfterInstall, atStageBoundary(&getImportsOptions{After: Install}), options, baseStageOptions) } func GenerateSignFilesBeforeSetupStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { - return generateSignFilesStage(imageBaseConfig, SignFilesBeforeSetup, &getImportsOptions{Before: Setup}, options, baseStageOptions) + return generateSignFilesStage(imageBaseConfig, SignFilesBeforeSetup, atStageBoundary(&getImportsOptions{Before: Setup}), options, baseStageOptions) } func GenerateSignFilesAfterSetupStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { - return generateSignFilesStage(imageBaseConfig, SignFilesAfterSetup, &getImportsOptions{After: Setup}, options, baseStageOptions) + return generateSignFilesStage(imageBaseConfig, SignFilesAfterSetup, atStageBoundary(&getImportsOptions{After: Setup}), options, baseStageOptions) } -func generateSignFilesStage(imageBaseConfig *config.StapelImageBase, name StageName, position *getImportsOptions, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { +// GenerateSignFilesStage covers the entries that name no stage: they are signed once the +// image's filesystem is complete, so that whatever imports the image gets the signature +// along with the file. +func GenerateSignFilesStage(imageBaseConfig *config.StapelImageBase, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { + return generateSignFilesStage(imageBaseConfig, SignFiles, func(s *config.Sign) bool { + return s.Before == "" && s.After == "" + }, options, baseStageOptions) +} + +func atStageBoundary(position *getImportsOptions) func(*config.Sign) bool { + return func(s *config.Sign) bool { + return (position.After != "" && s.After == string(position.After)) || (position.Before != "" && s.Before == string(position.Before)) + } +} + +func generateSignFilesStage(imageBaseConfig *config.StapelImageBase, name StageName, matches func(*config.Sign) bool, options signing.FilesSigningOptions, baseStageOptions *BaseStageOptions) *SignFilesStage { if !options.Enabled { return nil } var signs []*config.Sign for _, s := range imageBaseConfig.Sign { - if (position.After != "" && s.After == string(position.After)) || (position.Before != "" && s.Before == string(position.Before)) { + if matches(s) { signs = append(signs, s) } } diff --git a/pkg/config/raw_stapel_image_test.go b/pkg/config/raw_stapel_image_test.go index f566599b14..dcdbea6def 100644 --- a/pkg/config/raw_stapel_image_test.go +++ b/pkg/config/raw_stapel_image_test.go @@ -76,6 +76,7 @@ var _ = Describe("rawStapelImage", func() { "sign": []map[string]interface{}{ {"path": "/out/a.roothash", "after": "install"}, {"path": "/out/b.roothash", "before": "setup"}, + {"path": "/out/c.roothash"}, }, }, &StapelImage{ @@ -87,6 +88,7 @@ var _ = Describe("rawStapelImage", func() { Sign: []*Sign{ {Path: "/out/a.roothash", After: "install"}, {Path: "/out/b.roothash", Before: "setup"}, + {Path: "/out/c.roothash"}, }, platform: []string{}, @@ -375,14 +377,6 @@ var _ = Describe("rawStapelImage", func() { "sign": []map[string]interface{}{{"path": "/", "after": "install"}}, }, ), - Entry( - "with sign missing before/after", - map[string]interface{}{ - "image": "image1", - "from": "alpine", - "sign": []map[string]interface{}{{"path": "/out/x.roothash"}}, - }, - ), Entry( "with sign both before and after", map[string]interface{}{ diff --git a/pkg/config/sign.go b/pkg/config/sign.go index 2efbad664a..278b74fa26 100644 --- a/pkg/config/sign.go +++ b/pkg/config/sign.go @@ -6,7 +6,8 @@ import ( ) // Sign declares a file of the image whose detached PKCS#7 signature werf writes -// next to it as .p7s, right after the stage named by Before or After. +// next to it as .p7s, right after the stage named by Before or After, or at +// the end of the image when neither is set. type Sign struct { Path string Before string @@ -31,8 +32,6 @@ func (c *Sign) validate() error { return newDetailedConfigError(fmt.Sprintf("invalid `path: %s` for sign: expected an absolute clean file path!", c.Path), c.raw, c.raw.rawStapelImage.doc) case c.Before != "" && c.After != "": return newDetailedConfigError("specify only one stage using `before: install|setup` or `after: install|setup` for sign!", c.raw, c.raw.rawStapelImage.doc) - case c.Before == "" && c.After == "": - return newDetailedConfigError("stage is not specified with `before: install|setup` or `after: install|setup` for sign!", c.raw, c.raw.rawStapelImage.doc) case c.Before != "" && checkInvalidRelation(c.Before): return newDetailedConfigError(fmt.Sprintf("invalid stage `before: %s` for sign: expected install or setup!", c.Before), c.raw, c.raw.rawStapelImage.doc) case c.After != "" && checkInvalidRelation(c.After): diff --git a/pkg/config/werf_schema_test.go b/pkg/config/werf_schema_test.go index 170ecccafc..40be21dfab 100644 --- a/pkg/config/werf_schema_test.go +++ b/pkg/config/werf_schema_test.go @@ -357,6 +357,12 @@ packages: - type: go-mod workdir: /app manager: "" +`), + Entry("sign naming no stage, which signs at the end of the image", ` +image: app +from: alpine +sign: +- path: /out/x `), ) @@ -667,12 +673,6 @@ image: app from: alpine sign: - after: install -`), - Entry("sign without stage", ` -image: app -from: alpine -sign: -- path: /out/x `), Entry("sign with both before and after", ` image: app diff --git a/schemas/werf.json b/schemas/werf.json index 69988fb722..f9d7878db9 100644 --- a/schemas/werf.json +++ b/schemas/werf.json @@ -1507,7 +1507,7 @@ ] }, "sign": { - "description": "Signs a file the image produces at a user stage boundary and lays the detached PKCS#7 signature down next to it as `.p7s`. Exactly one of `before` or `after` must be set.\nhttps://werf.io/docs/latest/usage/build/stapel/imports.html#signing-files-inside-the-image", + "description": "Signs a file the image produces at a user stage boundary and lays the detached PKCS#7 signature down next to it as `.p7s`. Set at most one of `before` or `after`; with neither the file is signed at the end of the image.\nhttps://werf.io/docs/latest/usage/build/stapel/imports.html#signing-files-inside-the-image", "type": "object", "additionalProperties": false, "required": [ @@ -1528,18 +1528,12 @@ "$ref": "#/definitions/stageName" } }, - "oneOf": [ - { - "required": [ - "before" - ] - }, - { - "required": [ - "after" - ] - } - ] + "not": { + "required": [ + "before", + "after" + ] + } }, "stapelImage": { "description": "Stapel image section: builds an image with werf's own assembly instructions. Define as many as you need.\nhttps://werf.io/docs/latest/usage/build/stapel/base.html", diff --git a/test/e2e/build/_fixtures/sign_files/state_default_position/werf.yaml b/test/e2e/build/_fixtures/sign_files/state_default_position/werf.yaml new file mode 100644 index 0000000000..5f3e3d74e0 --- /dev/null +++ b/test/e2e/build/_fixtures/sign_files/state_default_position/werf.yaml @@ -0,0 +1,27 @@ +project: werf-test-e2e-sign-files-default +configVersion: 1 +--- +image: sysext +from: alpine:3.20 +shell: + setup: + - mkdir -p /out + - printf '%s' 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b > /out/ext.roothash +sign: +- path: /out/ext.roothash +--- +image: consumer +from: alpine:3.20 +import: +- image: sysext + add: /out/ext.roothash + to: /imported/ext.roothash + after: install +- image: sysext + add: /out/ext.roothash.p7s + to: /imported/ext.roothash.p7s + after: install +shell: + setup: + - test -s /imported/ext.roothash + - test -s /imported/ext.roothash.p7s diff --git a/test/e2e/build/sign_files_test.go b/test/e2e/build/sign_files_test.go index ebdd6d0f82..365e6284d3 100644 --- a/test/e2e/build/sign_files_test.go +++ b/test/e2e/build/sign_files_test.go @@ -94,6 +94,27 @@ var _ = Describe("sign directive", Label("e2e", "sign-files", "simple"), func() Expect(err).To(Succeed(), string(out)) } + DescribeTable("signs at the end of the image when the directive names no stage", + func(ctx SpecContext, testOpts signFilesTestOptions) { + setupEnv(testOpts.setupEnvOptions) + + SuiteData.InitTestRepo(ctx, "repo0", "sign_files/state_default_position") + werfProject := report.NewProjectWithReport(werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0"))) + + buildOut, buildReport := werfProject.BuildWithReport(ctx, SuiteData.GetBuildReportPath("report0.json"), &werf.WithReportOptions{CommonOptions: werf.CommonOptions{ExtraArgs: signArgs}}) + Expect(buildOut).To(ContainSubstring("Building stage sysext/signFiles")) + Expect(buildOut).To(ContainSubstring("Signing " + signFilesPath)) + Expect(buildOut).To(ContainSubstring("Building stage consumer/setup"), + "the dependent image imports the file together with its signature, so the signature must exist before the import") + + expectSystemdProfile(readSignedFiles(buildReport.Images["sysext"].DockerImageName)) + }, + backendEntry("with repo using Docker", signFilesTestOptions{setupEnvOptions: setupEnvOptions{ + ContainerBackendMode: "docker", + WithLocalRepo: true, + }}), + ) + DescribeTable("resolves a built image by its certificate alone, without the private key", func(ctx SpecContext, testOpts signFilesTestOptions) { setupEnv(testOpts.setupEnvOptions) From 38190d8cbba4347a14e87bfb1e94640778a96722 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Thu, 22 Oct 2026 14:24:47 +0300 Subject: [PATCH 17/22] fix(build): bind the sign stage digest to the signature format The stage digest covered the signed paths, their position and the certificate but not the format of the signature itself, so changing how werf encodes the signature would keep serving the old digest and hand back a cached layer in the superseded format. Add a format identifier to the signing package and fold it into the digest, so a format change invalidates the cached sign stages on its own instead of needing a manual cache version bump. Signed-off-by: Radmir Khurum --- pkg/build/signing/files_signing.go | 7 +++++++ pkg/build/stage/sign_files.go | 2 +- pkg/build/stage/sign_files_test.go | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/build/signing/files_signing.go b/pkg/build/signing/files_signing.go index 625e06ee8e..244debdcba 100644 --- a/pkg/build/signing/files_signing.go +++ b/pkg/build/signing/files_signing.go @@ -36,6 +36,13 @@ func NewFilesSigningOptions(cert string, signer *Signer) FilesSigningOptions { return FilesSigningOptions{Cert: cert, signer: signer} } +// SignatureFormat identifies the on-disk signature format SignDetachedPKCS7 produces. +// It is part of the sign stage digest: bump it whenever the produced bytes change in a +// way a verifier can observe (digest or encryption algorithm, authenticated attributes, +// the embedded certificate set, the CMS profile), so the change invalidates cached sign +// stages instead of serving a signature in a stale format under the old digest. +const SignatureFormat = "detached-pkcs7-rsa-sha256-v1" + // SignDetachedPKCS7 wraps an RSASSA-PKCS1-v1_5/SHA-256 signature of content into a // detached CMS SignedData with the leaf certificate embedded and no authenticated // attributes: the profile systemd-keyutil writes and the kernel dm-verity signature diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index eb43161d31..5f402fcf0a 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -106,7 +106,7 @@ func (s *SignFilesStage) GetContentDependencies(_ context.Context, _ Conveyor, _ } func (s *SignFilesStage) digest() string { - args := []string{string(s.Name())} + args := []string{string(s.Name()), "Format", signing.SignatureFormat} for _, sign := range s.signs { args = append(args, "Path", sign.Path) } diff --git a/pkg/build/stage/sign_files_test.go b/pkg/build/stage/sign_files_test.go index d206620fd1..50b7393efb 100644 --- a/pkg/build/stage/sign_files_test.go +++ b/pkg/build/stage/sign_files_test.go @@ -16,6 +16,7 @@ import ( . "github.com/onsi/gomega" "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/werf/common-go/pkg/util" "github.com/werf/werf/v3/pkg/build/signing" "github.com/werf/werf/v3/pkg/config" ) @@ -82,6 +83,16 @@ var _ = Describe("SignFilesStage", func() { Expect(digestOf(signerB, "/out/x")).NotTo(Equal(base), "a different signing certificate must change the digest") }) + It("binds the digest to the signature format, so a format change invalidates the cache", func() { + signer := newSigner("a") + + imageConfig := &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", After: "install"}}} + stage := GenerateSignFilesAfterInstallStage(imageConfig, enabledOptions(signer), &BaseStageOptions{}) + expected := util.Sha256Hash(string(stage.Name()), "Format", signing.SignatureFormat, "Path", "/out/x", "Cert", signer.Cert()) + + Expect(digestOf(signer, "/out/x")).To(Equal(expected), "the signature format must take part in the digest") + }) + It("identifies the stage by the certificate alone, so no private key is needed to resolve it", func() { signer := newSigner("a") From 3bb006fb9c9752b2b0de26aba0b619e3cc0382b1 Mon Sep 17 00:00:00 2001 From: Aleksei Igrychev Date: Thu, 22 Oct 2026 15:04:44 +0100 Subject: [PATCH 18/22] fix(build): isolate file signature artifacts by target platform Signed-off-by: Aleksei Igrychev --- pkg/build/stage/sign_files.go | 6 +- pkg/build/stage/sign_files_test.go | 90 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/pkg/build/stage/sign_files.go b/pkg/build/stage/sign_files.go index 5f402fcf0a..2eb7da2356 100644 --- a/pkg/build/stage/sign_files.go +++ b/pkg/build/stage/sign_files.go @@ -18,6 +18,7 @@ import ( "github.com/werf/werf/v3/pkg/build/signing" "github.com/werf/werf/v3/pkg/config" "github.com/werf/werf/v3/pkg/container_backend" + "github.com/werf/werf/v3/pkg/slug" "github.com/werf/werf/v3/pkg/stapel" ) @@ -123,7 +124,10 @@ func (s *SignFilesStage) PrepareImage(ctx context.Context, c Conveyor, cb contai return fmt.Errorf("image %q declares files to sign and this stage is not built yet: specify the private key with --sign-build-file-key", s.imageName) } - signaturesDir := filepath.Join(s.imageTmpDir, "sign", string(s.Name())) + // The platform variants of one image are independent graph nodes built in parallel and + // share imageTmpDir: without the platform in the path the signature of one platform + // replaces the other's before its container has installed it. + signaturesDir := filepath.Join(s.imageTmpDir, "sign", slug.Slug(s.targetPlatform), string(s.Name())) if err := os.MkdirAll(signaturesDir, os.ModePerm); err != nil { return fmt.Errorf("create signatures dir %s: %w", signaturesDir, err) } diff --git a/pkg/build/stage/sign_files_test.go b/pkg/build/stage/sign_files_test.go index 50b7393efb..e33937c07c 100644 --- a/pkg/build/stage/sign_files_test.go +++ b/pkg/build/stage/sign_files_test.go @@ -8,19 +8,43 @@ import ( "crypto/x509/pkix" "encoding/base64" "encoding/pem" + "fmt" "math/big" + "os" + "path" + "path/filepath" + "strings" "time" "github.com/deckhouse/delivery-kit-sdk/pkg/signver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/smallstep/pkcs7" "github.com/werf/common-go/pkg/util" "github.com/werf/werf/v3/pkg/build/signing" "github.com/werf/werf/v3/pkg/config" + "github.com/werf/werf/v3/pkg/container_backend" + "github.com/werf/werf/v3/pkg/image" ) +type signFilesContainerBackendStub struct { + *ContainerBackendStub + + contentByTargetPlatform map[string][]byte +} + +var _ container_backend.ContainerBackend = (*signFilesContainerBackendStub)(nil) + +func (backend *signFilesContainerBackendStub) ReadFileFromImage(_ context.Context, _, filePath string, opts container_backend.ReadFileFromImageOpts) ([]byte, error) { + content, found := backend.contentByTargetPlatform[opts.TargetPlatform] + if !found { + return nil, fmt.Errorf("read %s: no image built for platform %q", filePath, opts.TargetPlatform) + } + return content, nil +} + var _ = Describe("SignFilesStage", func() { newSigner := func(cn string) *signing.Signer { key, err := rsa.GenerateKey(rand.Reader, 2048) @@ -102,6 +126,72 @@ var _ = Describe("SignFilesStage", func() { Expect(digestOfOptions(withoutKey, "/out/x")).To(Equal(digestOf(signer, "/out/x"))) }) + It("keeps the signature of every platform in its own directory, so a concurrent platform build cannot replace a signature waiting for its container", func(ctx SpecContext) { + const signedPath = "/out/ext.roothash" + + contentByTargetPlatform := map[string][]byte{ + "linux/amd64": []byte("amd64 root hash"), + "linux/arm64": []byte("arm64 root hash"), + } + backend := &signFilesContainerBackendStub{ + ContainerBackendStub: NewContainerBackendStub(), + contentByTargetPlatform: contentByTargetPlatform, + } + conveyor := NewConveyorStub(NewGiterminismManagerStub(NewLocalGitRepoStub("9d8059842b6fde712c58315ca0ab4713d90761c0"), NewGiterminismInspectorStub()), nil, nil) + imageConfig := &config.StapelImageBase{Name: "sysext", Sign: []*config.Sign{{Path: signedPath}}} + imageTmpDir := filepath.Join(GinkgoT().TempDir(), "image", imageConfig.Name) + options := enabledOptions(newSigner("a")) + + prepare := func(targetPlatform string) string { + stage := GenerateSignFilesStage(imageConfig, options, &BaseStageOptions{ + TargetPlatform: targetPlatform, + ImageName: imageConfig.Name, + ImageTmpDir: imageTmpDir, + ContainerWerfDir: "/.werf", + }) + Expect(stage).NotTo(BeNil()) + + prevBuiltImage := container_backend.NewLegacyStageImage(nil, "previous-"+targetPlatform, backend, targetPlatform) + prevBuiltImage.SetStageDesc(&image.StageDesc{Info: &image.Info{}}) + stageImage := NewStageImage(backend, prevBuiltImage.Name(), container_backend.NewLegacyStageImage(prevBuiltImage, "result-"+targetPlatform, backend, targetPlatform)) + + Expect(stage.PrepareImage(ctx, conveyor, backend, &StageImage{Image: prevBuiltImage}, stageImage, nil)).To(Succeed()) + + runOptions := stageImage.Builder.LegacyStapelStageBuilder().Container().RunOptions().(*container_backend.LegacyStageImageContainerOptions) + var signaturesDirs []string + for _, volume := range runOptions.Volume { + hostPath, containerPath, _ := strings.Cut(volume, ":") + if strings.HasPrefix(containerPath, "/.werf/sign/") { + signaturesDirs = append(signaturesDirs, hostPath) + } + } + Expect(signaturesDirs).To(HaveLen(1), "the stage must mount exactly one signatures directory") + + return filepath.Join(signaturesDirs[0], "0", path.Base(signedPath)+".p7s") + } + + verifies := func(signaturePath string, content []byte) error { + der, err := os.ReadFile(signaturePath) + if err != nil { + return err + } + p7, err := pkcs7.Parse(der) + if err != nil { + return err + } + p7.Content = content + return p7.Verify() + } + + amd64Signature := prepare("linux/amd64") + Expect(verifies(amd64Signature, contentByTargetPlatform["linux/amd64"])).To(Succeed()) + + arm64Signature := prepare("linux/arm64") + Expect(verifies(arm64Signature, contentByTargetPlatform["linux/arm64"])).To(Succeed()) + Expect(verifies(amd64Signature, contentByTargetPlatform["linux/amd64"])).To(Succeed(), + "preparing another platform must not overwrite the signature the first platform still has to install") + }) + It("is generated only for the position the directive names and only when enabled", func() { imageConfig := &config.StapelImageBase{Sign: []*config.Sign{{Path: "/out/x", Before: "install"}}} options := enabledOptions(newSigner("a")) From 11ada483c3dcc912ff919849a4091fe3c8f48339 Mon Sep 17 00:00:00 2001 From: Aleksei Igrychev Date: Thu, 22 Oct 2026 15:04:45 +0100 Subject: [PATCH 19/22] fix(config): reject overlapping signing inputs and outputs Signed-off-by: Aleksei Igrychev --- pkg/config/raw_stapel_image.go | 16 ++++++-- pkg/config/raw_stapel_image_test.go | 59 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/pkg/config/raw_stapel_image.go b/pkg/config/raw_stapel_image.go index 0afe538320..3eabe098c7 100644 --- a/pkg/config/raw_stapel_image.go +++ b/pkg/config/raw_stapel_image.go @@ -196,16 +196,26 @@ func (c *rawStapelImage) toStapelImageBaseDirective(ctx context.Context, gitermi return nil, err } - signPaths := map[string]struct{}{} + signByPath := map[string]*Sign{} + signByOutputPath := map[string]*Sign{} for _, rawSign := range c.RawSign { signDirective, err := rawSign.toDirective() if err != nil { return nil, err } - if _, exist := signPaths[signDirective.Path]; exist { + if _, exist := signByPath[signDirective.Path]; exist { return nil, newDetailedConfigError(fmt.Sprintf("duplicated `path: %s` for sign!", signDirective.Path), rawSign, c.doc) } - signPaths[signDirective.Path] = struct{}{} + // All entries read the previous image, so an output cannot also be a signed + // input in the same stage. + if other, exist := signByPath[signDirective.OutputPath()]; exist && other.Before == signDirective.Before && other.After == signDirective.After { + return nil, newDetailedConfigError(fmt.Sprintf("conflicting `path: %s` for sign: its signature `%s` is signed at the same stage!", signDirective.Path, signDirective.OutputPath()), rawSign, c.doc) + } + if other, exist := signByOutputPath[signDirective.Path]; exist && other.Before == signDirective.Before && other.After == signDirective.After { + return nil, newDetailedConfigError(fmt.Sprintf("conflicting `path: %s` for sign: it is the signature of `path: %s` signed at the same stage!", signDirective.Path, other.Path), rawSign, c.doc) + } + signByPath[signDirective.Path] = signDirective + signByOutputPath[signDirective.OutputPath()] = signDirective imageBase.Sign = append(imageBase.Sign, signDirective) } diff --git a/pkg/config/raw_stapel_image_test.go b/pkg/config/raw_stapel_image_test.go index dcdbea6def..b2e278012a 100644 --- a/pkg/config/raw_stapel_image_test.go +++ b/pkg/config/raw_stapel_image_test.go @@ -91,6 +91,32 @@ var _ = Describe("rawStapelImage", func() { {Path: "/out/c.roothash"}, }, + platform: []string{}, + final: true, + }, + }, + ), + Entry( + "should handle signing the signature of an earlier stage", + map[string]interface{}{ + "image": "image1", + "from": "alpine:latest", + "sign": []map[string]interface{}{ + {"path": "/out/a.roothash", "after": "install"}, + {"path": "/out/a.roothash.p7s"}, + }, + }, + &StapelImage{ + StapelImageBase: &StapelImageBase{ + Name: "image1", + From: "alpine:latest", + Git: &GitManager{}, + Secrets: []Secret{}, + Sign: []*Sign{ + {Path: "/out/a.roothash", After: "install"}, + {Path: "/out/a.roothash.p7s"}, + }, + platform: []string{}, final: true, }, @@ -404,5 +430,38 @@ var _ = Describe("rawStapelImage", func() { }, }, ), + Entry( + "with sign path signed before its own signature at the same stage", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{ + {"path": "/out/x.roothash", "after": "install"}, + {"path": "/out/x.roothash.p7s", "after": "install"}, + }, + }, + ), + Entry( + "with sign signature signed before its own path at the same stage", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{ + {"path": "/out/x.roothash.p7s", "after": "install"}, + {"path": "/out/x.roothash", "after": "install"}, + }, + }, + ), + Entry( + "with sign path and its signature at the end of the image", + map[string]interface{}{ + "image": "image1", + "from": "alpine", + "sign": []map[string]interface{}{ + {"path": "/out/x.roothash"}, + {"path": "/out/x.roothash.p7s"}, + }, + }, + ), ) }) From ed865ef9a004c02219cd2e0c915fbee7b281b9dd Mon Sep 17 00:00:00 2001 From: Aleksei Igrychev Date: Thu, 22 Oct 2026 15:04:45 +0100 Subject: [PATCH 20/22] fix(cli): validate Vault auth for certificate-only signing resolution Signed-off-by: Aleksei Igrychev --- cmd/werf/common/files_signing_test.go | 67 +++++++++++++++++---- cmd/werf/common/signature.go | 14 ++--- docs/pages_en/usage/build/stapel/imports.md | 6 +- docs/pages_ru/usage/build/stapel/imports.md | 6 +- 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/cmd/werf/common/files_signing_test.go b/cmd/werf/common/files_signing_test.go index 447ca167a0..2ee5eea156 100644 --- a/cmd/werf/common/files_signing_test.go +++ b/cmd/werf/common/files_signing_test.go @@ -27,6 +27,10 @@ var _ = Describe("files signing options", func() { return &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: name}} } + imageBasedOn := func(name, baseImageName string) config.ImageInterface { + return &config.StapelImage{StapelImageBase: &config.StapelImageBase{Name: name, From: baseImageName}} + } + newConfig := func(images ...config.ImageInterface) *config.WerfConfig { return config.NewWerfConfig(&config.Meta{}, images) } @@ -43,6 +47,12 @@ var _ = Describe("files signing options", func() { return imagesToProcess } + withoutImages := func(werfConfig *config.WerfConfig) config.ImagesToProcess { + imagesToProcess, err := config.NewImagesToProcess(werfConfig, nil, false, true) + Expect(err).NotTo(HaveOccurred()) + return imagesToProcess + } + certPEM := func() string { key, err := rsa.GenerateKey(rand.Reader, 2048) Expect(err).NotTo(HaveOccurred()) @@ -68,18 +78,24 @@ var _ = Describe("files signing options", func() { Entry("certificate that cannot be read", &CmdData{SignBuildFileCert: lo.ToPtr("c")}, "read the files signing certificate"), ) - It("refuses a vault role or auth path that qualifies no jwt", func() { - werfConfig := newConfig(signingImage("app")) - cert := base64.StdEncoding.EncodeToString([]byte(certPEM())) + DescribeTable("refuses a vault role or auth path that qualifies no jwt", + func(vaultAuth CmdData) { + werfConfig := newConfig(signingImage("app")) + cert := lo.ToPtr(base64.StdEncoding.EncodeToString([]byte(certPEM()))) - for _, cmdData := range []*CmdData{ - {SignBuildFileKey: lo.ToPtr("k"), SignBuildFileCert: lo.ToPtr(cert), SignBuildFileVaultAuthRole: lo.ToPtr("r")}, - {SignBuildFileKey: lo.ToPtr("k"), SignBuildFileCert: lo.ToPtr(cert), SignBuildFileVaultAuthPath: lo.ToPtr("fox")}, - } { - _, err := getFilesSigningOptions(context.Background(), cmdData, werfConfig, allImages(werfConfig)) - Expect(err).To(MatchError(ContainSubstring("qualify --sign-build-file-vault-auth-jwt"))) - } - }) + for _, keyRef := range []string{"", "k"} { + cmdData := vaultAuth + cmdData.SignBuildFileCert = cert + cmdData.SignBuildFileKey = lo.ToPtr(keyRef) + + _, err := getFilesSigningOptions(context.Background(), &cmdData, werfConfig, allImages(werfConfig)) + Expect(err).To(MatchError(ContainSubstring("qualify --sign-build-file-vault-auth-jwt")), "--sign-build-file-key %q", keyRef) + } + }, + Entry("role", CmdData{SignBuildFileVaultAuthRole: lo.ToPtr("r")}), + Entry("auth path", CmdData{SignBuildFileVaultAuthPath: lo.ToPtr("fox")}), + Entry("role and auth path", CmdData{SignBuildFileVaultAuthRole: lo.ToPtr("r"), SignBuildFileVaultAuthPath: lo.ToPtr("fox")}), + ) It("is disabled when no image being processed declares files to sign", func() { werfConfig := newConfig(plainImage("app")) @@ -109,4 +125,33 @@ var _ = Describe("files signing options", func() { Expect(options.Cert).To(Equal(cert)) Expect(options.Signer()).To(BeNil(), "no private key was given, so nothing can sign") }) + + Describe("the conveyor the command hands the build", func() { + It("signs the files of the images it is about to build", func() { + werfConfig := newConfig(signingImage("signed"), imageBasedOn("app", "signed"), plainImage("unrelated")) + cert := certPEM() + cmdData := &CmdData{SignBuildFileCert: lo.ToPtr(base64.StdEncoding.EncodeToString([]byte(cert)))} + + conveyorOptions, err := GetConveyorOptions(context.Background(), cmdData, werfConfig, someImages(werfConfig, "app")) + Expect(err).NotTo(HaveOccurred()) + Expect(conveyorOptions.FilesSigningOptions.Enabled).To(BeTrue(), "the image being built is based on a signed one") + Expect(conveyorOptions.FilesSigningOptions.Cert).To(Equal(cert)) + Expect(conveyorOptions.FilesSigningOptions.Signer()).To(BeNil()) + }) + + It("signs nothing when the build reaches no image that declares files to sign", func() { + werfConfig := newConfig(signingImage("signed"), plainImage("unrelated")) + + for name, imagesToProcess := range map[string]config.ImagesToProcess{ + "unrelated image": someImages(werfConfig, "unrelated"), + "no images": withoutImages(werfConfig), + } { + conveyorOptions, err := GetConveyorOptions(context.Background(), &CmdData{}, werfConfig, imagesToProcess) + Expect(err).NotTo(HaveOccurred(), name) + Expect(conveyorOptions.FilesSigningOptions.Enabled).To(BeFalse(), name) + Expect(conveyorOptions.FilesSigningOptions.Cert).To(BeEmpty(), name) + Expect(conveyorOptions.FilesSigningOptions.Signer()).To(BeNil(), name) + } + }) + }) }) diff --git a/cmd/werf/common/signature.go b/cmd/werf/common/signature.go index 2383993248..e662c416e4 100644 --- a/cmd/werf/common/signature.go +++ b/cmd/werf/common/signature.go @@ -218,13 +218,6 @@ func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfCon return signing.FilesSigningOptions{}, fmt.Errorf("read the files signing certificate: %w", err) } - keyRef := lo.FromPtr(commonCmdData.SignBuildFileKey) - if keyRef == "" { - options := signing.NewFilesSigningOptions(cert, nil) - options.Enabled = true - return options, nil - } - vault := signing.VaultOptions{ Address: lo.FromPtr(commonCmdData.SignBuildFileVaultAddr), TransitPath: lo.FromPtr(commonCmdData.SignBuildFileVaultTransitPath), @@ -236,6 +229,13 @@ func getFilesSigningOptions(ctx context.Context, commonCmdData *CmdData, werfCon return signing.FilesSigningOptions{}, fmt.Errorf("--sign-build-file-vault-auth-role and --sign-build-file-vault-auth-path qualify --sign-build-file-vault-auth-jwt: specify the JWT as well, or drop them to log in to Vault as --sign-key does") } + keyRef := lo.FromPtr(commonCmdData.SignBuildFileKey) + if keyRef == "" { + options := signing.NewFilesSigningOptions(cert, nil) + options.Enabled = true + return options, nil + } + signer, err := signing.NewSigner(ctx, signing.SignerOptions{ KeyRef: keyRef, CertRef: certRef, diff --git a/docs/pages_en/usage/build/stapel/imports.md b/docs/pages_en/usage/build/stapel/imports.md index a56b6aed26..4d1ae8304b 100644 --- a/docs/pages_en/usage/build/stapel/imports.md +++ b/docs/pages_en/usage/build/stapel/imports.md @@ -113,6 +113,8 @@ sign: 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. 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: @@ -130,7 +132,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 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. There is no way to build such an image unsigned: a declared `sign` is a requirement of the image, not a build-time choice. @@ -140,4 +142,4 @@ A build of an image that declares `sign` fails before any stage runs when the ce With `hashivault://` the private key never leaves the KMS. The certificate is not fetched from the KMS: when it is issued by a PKI (Vault PKI, for example), the pipeline retrieves the PEM itself and passes it as a path or base64. The stage container always runs with `network: none`, whatever the image's `network` setting, and none of the image's commands run in it, so the directive works unchanged when `build.sbom.enable` is `true` and [network is disabled in shell stages]({{ "/usage/build/sbom.html" | true_relative_url }}). -The stage is cached like any other. Its digest covers the signed paths and the signing certificate, so rotating the key rebuilds the signature and the stages after it, while a rebuild with the same key reuses the cached layer. Only the signing certificate is embedded in the `.p7s`; intermediates do not take part in the signature or the digest. +The stage is cached like any other. Its digest covers the signed paths, signing position, signing certificate and signature format. Changing any of these invalidates the sign stage and the stages after it; with unchanged build inputs, the cached layer is reused. The private key reference and Vault authentication settings do not change the cache identity. When rotating the key, supply a matching certificate. Only the signing certificate is embedded in the `.p7s`; intermediates do not take part in the signature or the digest. diff --git a/docs/pages_ru/usage/build/stapel/imports.md b/docs/pages_ru/usage/build/stapel/imports.md index b35e612173..ff563a444e 100644 --- a/docs/pages_ru/usage/build/stapel/imports.md +++ b/docs/pages_ru/usage/build/stapel/imports.md @@ -113,6 +113,8 @@ sign: 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.yaml`; они передаются только через командную строку, отдельно от ключа подписи манифеста и SBOM: @@ -130,7 +132,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 сами по себе отвергаются. +Адрес и 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. Собрать такой образ без подписей нельзя: объявленная директива `sign` — требование образа, а не опция сборки. @@ -140,4 +142,4 @@ werf читает файл из собранного к этому момент С `hashivault://` приватный ключ не покидает KMS. Сертификат из KMS не запрашивается: если его выпускает PKI (например, Vault PKI), пайплайн сам получает PEM и передаёт его путём или в base64. Контейнер стадии всегда запускается с `network: none` независимо от настройки `network` образа, и команды образа в нём не выполняются, поэтому директива работает без изменений при `build.sbom.enable: true`, когда [в shell-стадиях отключена сеть]({{ "/usage/build/sbom.html" | true_relative_url }}). -Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути и сертификат подписанта, поэтому ротация ключа пересобирает подпись и последующие стадии, а повторная сборка с тем же ключом переиспользует закэшированный слой. В `.p7s` встраивается только сертификат подписанта; промежуточные сертификаты не участвуют ни в подписи, ни в дайджесте. +Стадия кэшируется как любая другая. Её дайджест включает подписываемые пути, позицию подписания, сертификат подписанта и формат подписи. Изменение любого из этих значений инвалидирует стадию подписания и последующие стадии; при неизменных входных данных сборки переиспользуется закэшированный слой. Ссылка на приватный ключ и настройки аутентификации Vault не меняют идентификатор кэша. При ротации ключа передайте соответствующий ему сертификат. В `.p7s` встраивается только сертификат подписанта; промежуточные сертификаты не участвуют ни в подписи, ни в дайджесте. From 8a7a5bc6c45fc0d1aa43ffa2d014d9e7d6b434ac Mon Sep 17 00:00:00 2001 From: Aleksei Igrychev Date: Thu, 22 Oct 2026 15:04:45 +0100 Subject: [PATCH 21/22] test(build): guard signing stage ordering and backend options Signed-off-by: Aleksei Igrychev --- pkg/build/helpers_test.go | 13 ++++++++++ pkg/build/image/stapel_test.go | 37 +++++++++++++++++++++++++-- pkg/build/stage_build_options_test.go | 17 ++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/pkg/build/helpers_test.go b/pkg/build/helpers_test.go index 06df009f96..5cc1289492 100644 --- a/pkg/build/helpers_test.go +++ b/pkg/build/helpers_test.go @@ -3,6 +3,7 @@ package build import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -31,6 +32,18 @@ import ( "github.com/werf/werf/v3/pkg/storage/synchronization/lock_manager" ) +type optionsCapturingBuilder struct { + *stage_builder.StageBuilder + opts container_backend.BuildOptions +} + +var _ stage_builder.StageBuilderInterface = (*optionsCapturingBuilder)(nil) + +func (b *optionsCapturingBuilder) Build(_ context.Context, opts container_backend.BuildOptions) error { + b.opts = opts + return errors.New("builder stopped the build") +} + func newImage(name string, baseImageType buildImage.BaseImageType, opts buildImage.ImageOptions) *buildImage.Image { img, err := buildImage.NewImage(context.Background(), "linux/amd64", name, baseImageType, opts) gomega.Expect(err).To(gomega.Succeed()) diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index d3991825a9..de7a79fb1c 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -139,8 +139,8 @@ sign: `, sbomBlock) } - newImage := func(ctx context.Context, sbomBlock string, filesSigning signing.FilesSigningOptions) *Image { - projectDir := newProjectRepo(ctx, map[string]string{"werf.yaml": werfYaml(sbomBlock)}) + newImageFrom := func(ctx context.Context, files map[string]string, filesSigning signing.FilesSigningOptions) *Image { + projectDir := newProjectRepo(ctx, files) metaConfig, imageConfig, opts := stapelImageConfig(ctx, projectDir) opts.FilesSigningOptions = filesSigning @@ -149,6 +149,10 @@ sign: return image } + newImage := func(ctx context.Context, sbomBlock string, filesSigning signing.FilesSigningOptions) *Image { + return newImageFrom(ctx, map[string]string{"werf.yaml": werfYaml(sbomBlock)}, filesSigning) + } + enabled := func() signing.FilesSigningOptions { o := signing.NewFilesSigningOptions("cert", &signing.Signer{}) o.Enabled = true @@ -171,6 +175,35 @@ sign: })) }) + It("signs the whole image only once every git stage has put its files in place", func() { + image := newImageFrom(context.Background(), map[string]string{ + "werf.yaml": `project: test +configVersion: 1 +--- +image: app +from: alpine:3.20 +git: +- add: /src + to: /app +shell: + install: + - echo install +sign: +- path: /out/d.roothash +`, + "src/app.txt": "app\n", + }, enabled()) + + Expect(stageNames(image)).To(Equal([]stage.StageName{ + stage.From, + stage.GitArchive, + stage.Install, + stage.GitCache, + stage.GitLatestPatch, + stage.SignFiles, + }), "the final sign stage must see the files the git stages patch in last") + }) + It("generates no stage when files signing is disabled", func() { image := newImage(context.Background(), "", signing.NewFilesSigningOptions("cert", &signing.Signer{})) diff --git a/pkg/build/stage_build_options_test.go b/pkg/build/stage_build_options_test.go index f9deb86c9a..6d1a6cc19e 100644 --- a/pkg/build/stage_build_options_test.go +++ b/pkg/build/stage_build_options_test.go @@ -38,4 +38,21 @@ var _ = Describe("stageBuildOptions", func() { Expect(opts.Network).To(Equal("bridge")) }) + + It("hands the closed network to the backend building a sign stage", func(ctx SpecContext) { + srv, _ := newPublicationLockServer() + phase, img, _ := newPublicationPhase(ctx, &publicationStorageManager{primary: &publicationStorage{}}, srv.URL, false, 10) + phase.ImageBuildOptions = container_backend.BuildOptions{Network: "bridge"} + + stg := signStage() + stg.SetDigest("sign-digest") + builder := &optionsCapturingBuilder{} + stg.SetStageImage(&stage.StageImage{Builder: builder}) + + _, err := phase.atomicBuildStageImage(ctx, img, stg) + Expect(err).To(MatchError(ContainSubstring("builder stopped the build"))) + + Expect(builder.opts.Network).To(Equal("none"), "--backend-network must not reach the backend building a stage that signs files") + Expect(builder.opts.TargetPlatform).To(Equal(img.TargetPlatform)) + }) }) From 39b445fb07f6f8ad164b9b8fde730ba3c22b4493 Mon Sep 17 00:00:00 2001 From: Aleksei Igrychev Date: Thu, 22 Oct 2026 15:07:33 +0100 Subject: [PATCH 22/22] test(build): avoid helper insertion conflict with main Signed-off-by: Aleksei Igrychev --- pkg/build/helpers_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/build/helpers_test.go b/pkg/build/helpers_test.go index 5cc1289492..2638150caa 100644 --- a/pkg/build/helpers_test.go +++ b/pkg/build/helpers_test.go @@ -32,18 +32,6 @@ import ( "github.com/werf/werf/v3/pkg/storage/synchronization/lock_manager" ) -type optionsCapturingBuilder struct { - *stage_builder.StageBuilder - opts container_backend.BuildOptions -} - -var _ stage_builder.StageBuilderInterface = (*optionsCapturingBuilder)(nil) - -func (b *optionsCapturingBuilder) Build(_ context.Context, opts container_backend.BuildOptions) error { - b.opts = opts - return errors.New("builder stopped the build") -} - func newImage(name string, baseImageType buildImage.BaseImageType, opts buildImage.ImageOptions) *buildImage.Image { img, err := buildImage.NewImage(context.Background(), "linux/amd64", name, baseImageType, opts) gomega.Expect(err).To(gomega.Succeed()) @@ -65,6 +53,18 @@ func writeBuildReport(records ...ReportImageRecord) string { return path } +type optionsCapturingBuilder struct { + *stage_builder.StageBuilder + opts container_backend.BuildOptions +} + +var _ stage_builder.StageBuilderInterface = (*optionsCapturingBuilder)(nil) + +func (b *optionsCapturingBuilder) Build(_ context.Context, opts container_backend.BuildOptions) error { + b.opts = opts + return errors.New("builder stopped the build") +} + func newReportPhase(reportPath string) *BuildPhase { return &BuildPhase{ BuildPhaseOptions: BuildPhaseOptions{BuildOptions: BuildOptions{ReportPath: reportPath, ReportFormat: ReportJSON}},