Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/werf/build/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/werf/bundle/publish/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions cmd/werf/common/cmd_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ 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

VerifyManifest *bool
VerifyRoots *[]string

Expand Down
7 changes: 7 additions & 0 deletions cmd/werf/common/conveyor_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass file-signing options through every build entry point before constructing the conveyor.

This assignment exists only in GetConveyorOptionsWithParallel. run, compose and kube-run use GetConveyorOptions directly. The image tree captures these options when the conveyor is created; subsequently passing build options to Build does not restore missing signing stages.

Docker reproductions of run and compose completed without producing the declared .p7s file, while the corresponding build route executed signing. kube-run has the same wiring defect by inspection, but was not exercised against a cluster.

Initialize signing options before conveyor construction on all three routes and add CLI regression coverage. Otherwise an image declaring mandatory signing can silently become unsigned depending on the command used.

conveyorOptions.VerityAnnotationOptions = buildStagesOptions.VerityAnnotationOptions

return conveyorOptions, nil
Expand Down Expand Up @@ -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)
Expand All @@ -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),
}
Expand Down
43 changes: 43 additions & 0 deletions cmd/werf/common/files_signing_test.go
Original file line number Diff line number Diff line change
@@ -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"}},
})),
)
})
99 changes: 99 additions & 0 deletions cmd/werf/common/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -45,6 +46,52 @@ 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 SetupELFSigningOptions(cmdData *CmdData, cmd *cobra.Command) {
cmdData.SignELFFiles = new(bool)
cmd.Flags().BoolVarP(cmdData.SignELFFiles, "sign-elf-files", "", util.GetBoolEnvironmentDefaultFalse("WERF_SIGN_ELF_FILES"),
Expand Down Expand Up @@ -154,6 +201,58 @@ 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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope signing requirements to the selected dependency graph and defer private signer initialization until signing is needed.

This scans every configured image and immediately requires a key/certificate and constructs the signer. GetBuildOptions invokes it before some commands decide whether they need to build images at all.

Reproduced with independent signed and plain images: werf build plain fails demanding the key for signed; werf render --without-images fails for the same reason. render --require-built-images also requires signing material even though its task is to resolve an already-built image.

Restrict requirements to selected images and their dependencies, skip image signing setup for operations that do not use images, and separate public certificate/cache-identity resolution from private signing capability. Already-built resolution must retain the same signed stage chain/tag. Using --sign-files=false would change the chain and is not an acceptable workaround; the agreed API removes that bypass altogether.

Cover the unrelated-image build, render --without-images, and already-built signed-image resolution without private key/Vault signing access.

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, ", "))
}

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: vault,
})
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)

Expand Down
1 change: 1 addition & 0 deletions cmd/werf/compose/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/werf/converge/converge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/werf/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions cmd/werf/kube_run/kube_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions cmd/werf/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/werf/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/werf/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions docs/_data/werf_yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<path>.p7s`"
ru: "Абсолютный путь к подписываемому файлу; отсоединённая подпись PKCS#7 записывается рядом как `<path>.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"
Expand Down
32 changes: 32 additions & 0 deletions docs/_includes/reference/cli/werf_build.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,38 @@ 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-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)
Expand Down
Loading
Loading