Repository navigation
feat(build, stapel): sign files inside the image with a key the build container never sees #438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
968c365
ffaf63c
c8f8ccf
e750568
adac36b
c6ed7c6
f61790c
e1df4b3
6cd727d
80735a9
8f35851
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"}}, | ||
| })), | ||
| ) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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"), | ||
|
|
@@ -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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Reproduced with independent 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 Cover the unrelated-image build, |
||
| 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) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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,composeandkube-runuseGetConveyorOptionsdirectly. The image tree captures these options when the conveyor is created; subsequently passing build options toBuilddoes not restore missing signing stages.Docker reproductions of
runandcomposecompleted without producing the declared.p7sfile, while the correspondingbuildroute executed signing.kube-runhas 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.