1
0
mirror of synced 2026-05-30 03:40:47 +00:00

Migrate bootstrap signing to generic Signer

Bumps fluxcd/pkg/git to a pseudo-version exposing the generic
signature.Signer interface and the NewOpenPGPSigner / NewSSHSigner
constructors, and migrates pkg/bootstrap's two WithSigner call sites
accordingly.

Adds a parallel WithSSHCommitSigning option alongside the existing
WithGitCommitSigning so callers can sign commits with an SSH private
key. PlainGitBootstrapper now dispatches through a new resolveSigner
helper that returns either an OpenPGP or SSH signer; the
repository.WithSigner option is appended conditionally to avoid the
typed-nil interface hazard the new generic field introduces.

The bootstrap path's OpenPGP entity selector is renamed and exported
as SelectOpenPGPSigningEntity so the flux CLI's pre-flight (introduced
later in this branch) can call it directly instead of carrying a
duplicate.

Also bumps image-automation-controller/api to a pseudo-version that
exposes SigningKey.Type and the SigningKeyTypeGPG/SigningKeyTypeSSH
constants; the bump is bundled here so the rest of the branch builds
incrementally. Refs fluxcd/pkg#398[1].

[1]: https://github.com/fluxcd/pkg/issues/398

Signed-off-by: Hidde Beydals <hidde@hhh.computer>
This commit is contained in:
Hidde Beydals
2026-05-29 22:10:51 +02:00
parent 8c41d5b56d
commit 88c5a7f68d
4 changed files with 92 additions and 25 deletions
+3 -2
View File
@@ -14,7 +14,7 @@ require (
github.com/fluxcd/cli-utils v1.2.0 github.com/fluxcd/cli-utils v1.2.0
github.com/fluxcd/go-git-providers v0.26.0 github.com/fluxcd/go-git-providers v0.26.0
github.com/fluxcd/helm-controller/api v1.5.5 github.com/fluxcd/helm-controller/api v1.5.5
github.com/fluxcd/image-automation-controller/api v1.1.4 github.com/fluxcd/image-automation-controller/api v1.0.1-0.20260529125431-20ebc65ab20f
github.com/fluxcd/image-reflector-controller/api v1.1.2 github.com/fluxcd/image-reflector-controller/api v1.1.2
github.com/fluxcd/kustomize-controller/api v1.8.5 github.com/fluxcd/kustomize-controller/api v1.8.5
github.com/fluxcd/notification-controller/api v1.8.4 github.com/fluxcd/notification-controller/api v1.8.4
@@ -23,7 +23,7 @@ require (
github.com/fluxcd/pkg/auth v0.45.0 github.com/fluxcd/pkg/auth v0.45.0
github.com/fluxcd/pkg/chartutil v1.24.0 github.com/fluxcd/pkg/chartutil v1.24.0
github.com/fluxcd/pkg/envsubst v1.7.0 github.com/fluxcd/pkg/envsubst v1.7.0
github.com/fluxcd/pkg/git v0.49.0 github.com/fluxcd/pkg/git v0.49.1-0.20260529122759-f46ad90373c5
github.com/fluxcd/pkg/kustomize v1.32.0 github.com/fluxcd/pkg/kustomize v1.32.0
github.com/fluxcd/pkg/oci v0.66.0 github.com/fluxcd/pkg/oci v0.66.0
github.com/fluxcd/pkg/runtime v0.106.0 github.com/fluxcd/pkg/runtime v0.106.0
@@ -166,6 +166,7 @@ require (
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/hiddeco/sshsig v0.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
+6 -4
View File
@@ -180,8 +180,8 @@ github.com/fluxcd/go-git-providers v0.26.0 h1:0DUsXc1nS9Fe4n8tXSEUCGemWzHShd66gm
github.com/fluxcd/go-git-providers v0.26.0/go.mod h1:VJDKUOhZwNAIqDF5iPtIpTr/annsDbKMkPpWiDMBdpo= github.com/fluxcd/go-git-providers v0.26.0/go.mod h1:VJDKUOhZwNAIqDF5iPtIpTr/annsDbKMkPpWiDMBdpo=
github.com/fluxcd/helm-controller/api v1.5.5 h1:xQA/9gbifMvZPGhSNKHsrkq829dI/yTBASVdYp9/s4Y= github.com/fluxcd/helm-controller/api v1.5.5 h1:xQA/9gbifMvZPGhSNKHsrkq829dI/yTBASVdYp9/s4Y=
github.com/fluxcd/helm-controller/api v1.5.5/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI= github.com/fluxcd/helm-controller/api v1.5.5/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI=
github.com/fluxcd/image-automation-controller/api v1.1.4 h1:i78AwbcICXSX+a1MQwjNA1Uxxs1e3kfi3EJ21fWzb7w= github.com/fluxcd/image-automation-controller/api v1.0.1-0.20260529125431-20ebc65ab20f h1:pjHh/w2xRd9u20J0c8H0EEoPAurJndm2XNF+/mem3EE=
github.com/fluxcd/image-automation-controller/api v1.1.4/go.mod h1:lkD/drkD6Wc+2SDjVj5KqfozEucTLFexWgby/5ft660= github.com/fluxcd/image-automation-controller/api v1.0.1-0.20260529125431-20ebc65ab20f/go.mod h1:XNWgNSF7GVZgGx6qTI+8jhiH0S+a/hNtNvBwPcxOotw=
github.com/fluxcd/image-reflector-controller/api v1.1.2 h1:VPwUgA8WyPVVs16uSkwvjOAY6pvTYgAb0fL90t0RKLE= github.com/fluxcd/image-reflector-controller/api v1.1.2 h1:VPwUgA8WyPVVs16uSkwvjOAY6pvTYgAb0fL90t0RKLE=
github.com/fluxcd/image-reflector-controller/api v1.1.2/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0= github.com/fluxcd/image-reflector-controller/api v1.1.2/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0=
github.com/fluxcd/kustomize-controller/api v1.8.5 h1:4fGPh6foGVKUUbt5OjVzbC5iTyX+Q+NS50atPboDC4w= github.com/fluxcd/kustomize-controller/api v1.8.5 h1:4fGPh6foGVKUUbt5OjVzbC5iTyX+Q+NS50atPboDC4w=
@@ -204,8 +204,8 @@ github.com/fluxcd/pkg/chartutil v1.24.0 h1:Rh9o50eJUnAioL5q2JVPxO4DjFjwHwPE5K9Lc
github.com/fluxcd/pkg/chartutil v1.24.0/go.mod h1:oycULCP00m46dxiskme1Yawe74UFLZzX0jqHb6xzdmQ= github.com/fluxcd/pkg/chartutil v1.24.0/go.mod h1:oycULCP00m46dxiskme1Yawe74UFLZzX0jqHb6xzdmQ=
github.com/fluxcd/pkg/envsubst v1.7.0 h1:PL9Nj/V2fgaMR9KYZR7mEEw+vlYgP80nFZjOQQKAfJs= github.com/fluxcd/pkg/envsubst v1.7.0 h1:PL9Nj/V2fgaMR9KYZR7mEEw+vlYgP80nFZjOQQKAfJs=
github.com/fluxcd/pkg/envsubst v1.7.0/go.mod h1:aoWeSIOamhqBZ3bHVj1GDwpdA10DXrI8yYbyjPiFly0= github.com/fluxcd/pkg/envsubst v1.7.0/go.mod h1:aoWeSIOamhqBZ3bHVj1GDwpdA10DXrI8yYbyjPiFly0=
github.com/fluxcd/pkg/git v0.49.0 h1:OtI0TjMVC/GV/yPos4waA5fAs5u3/2YEYBFuZuJA3Rc= github.com/fluxcd/pkg/git v0.49.1-0.20260529122759-f46ad90373c5 h1:ZPpsEw33U/2JmgeGIbgdAN0UX+GM01sE+cb1OuuSQwY=
github.com/fluxcd/pkg/git v0.49.0/go.mod h1:Xnrqtz60/0jqfcy5UsY9sxlvPmlvzelPsej2v3L7wW8= github.com/fluxcd/pkg/git v0.49.1-0.20260529122759-f46ad90373c5/go.mod h1:OgaHoS0iR0GuLl+f778X7NrGy1pDH7xcpF/nsCRgJ9g=
github.com/fluxcd/pkg/gittestserver v0.29.0 h1:2j03zKVL6iVn6oiUuecG/O/3Q1pULWM9JrF/HSjkpnc= github.com/fluxcd/pkg/gittestserver v0.29.0 h1:2j03zKVL6iVn6oiUuecG/O/3Q1pULWM9JrF/HSjkpnc=
github.com/fluxcd/pkg/gittestserver v0.29.0/go.mod h1:O8151jV0ppBZTb9IUXMjxh6hZpkiuLq8JQHDBPOkZFw= github.com/fluxcd/pkg/gittestserver v0.29.0/go.mod h1:O8151jV0ppBZTb9IUXMjxh6hZpkiuLq8JQHDBPOkZFw=
github.com/fluxcd/pkg/kustomize v1.32.0 h1:5lLT2dgR+JrcoJHB7/K50o0AcJikKvXcRd3r7jIYZC8= github.com/fluxcd/pkg/kustomize v1.32.0 h1:5lLT2dgR+JrcoJHB7/K50o0AcJikKvXcRd3r7jIYZC8=
@@ -352,6 +352,8 @@ github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGN
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hiddeco/sshsig v0.2.0 h1:gMWllgKCITXdydVkDL+Zro0PU96QI55LwUwebSwNTSw=
github.com/hiddeco/sshsig v0.2.0/go.mod h1:nJc98aGgiH6Yql2doqH4CTBVHexQA40Q+hMMLHP4EqE=
github.com/homeport/dyff v1.10.2 h1:XyB+D0KVwjbUFTZYIkvPtsImwkfh+ObH2CEdEHTqdr4= github.com/homeport/dyff v1.10.2 h1:XyB+D0KVwjbUFTZYIkvPtsImwkfh+ObH2CEdEHTqdr4=
github.com/homeport/dyff v1.10.2/go.mod h1:0kIjL/JOGaXigzrLY6kcl5esSStbAa99r6GzEvr7lrs= github.com/homeport/dyff v1.10.2/go.mod h1:0kIjL/JOGaXigzrLY6kcl5esSStbAa99r6GzEvr7lrs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+55 -19
View File
@@ -43,6 +43,7 @@ import (
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/repository" "github.com/fluxcd/pkg/git/repository"
"github.com/fluxcd/pkg/git/signature"
"github.com/fluxcd/pkg/kustomize/filesys" "github.com/fluxcd/pkg/kustomize/filesys"
runclient "github.com/fluxcd/pkg/runtime/client" runclient "github.com/fluxcd/pkg/runtime/client"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
@@ -67,6 +68,9 @@ type PlainGitBootstrapper struct {
gpgPassphrase string gpgPassphrase string
gpgKeyID string gpgKeyID string
sshSigningKey []byte
sshSigningPassword []byte
restClientGetter genericclioptions.RESTClientGetter restClientGetter genericclioptions.RESTClientGetter
restClientOptions *runclient.Options restClientOptions *runclient.Options
@@ -155,24 +159,27 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest
b.logger.Successf("generated component manifests") b.logger.Successf("generated component manifests")
// Write generated files and make a commit // Write generated files and make a commit
var signer *openpgp.Entity signer, err := b.resolveSigner()
if b.gpgKeyRing != nil { if err != nil {
signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) return fmt.Errorf("failed to construct commit signer: %w", err)
if err != nil {
return fmt.Errorf("failed to generate OpenPGP entity: %w", err)
}
} }
commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version)
if b.commitMessageAppendix != "" { if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
} }
commitOpts := []repository.CommitOption{
repository.WithFiles(map[string]io.Reader{
manifests.Path: strings.NewReader(manifests.Content),
}),
}
if signer != nil {
commitOpts = append(commitOpts, repository.WithSigner(signer))
}
commit, err := b.gitClient.Commit(git.Commit{ commit, err := b.gitClient.Commit(git.Commit{
Author: b.signature, Author: b.signature,
Message: commitMsg, Message: commitMsg,
}, repository.WithFiles(map[string]io.Reader{ }, commitOpts...)
manifests.Path: strings.NewReader(manifests.Content),
}), repository.WithSigner(signer))
if err != nil && err != git.ErrNoStagedFiles { if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit component manifests: %w", err) return fmt.Errorf("failed to commit component manifests: %w", err)
} }
@@ -330,24 +337,27 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options
b.logger.Successf("generated sync manifests") b.logger.Successf("generated sync manifests")
// Write generated files and make a commit // Write generated files and make a commit
var signer *openpgp.Entity signer, err := b.resolveSigner()
if b.gpgKeyRing != nil { if err != nil {
signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) return fmt.Errorf("failed to construct commit signer: %w", err)
if err != nil {
return fmt.Errorf("failed to generate OpenPGP entity: %w", err)
}
} }
commitMsg := "Add Flux sync manifests" commitMsg := "Add Flux sync manifests"
if b.commitMessageAppendix != "" { if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
} }
commitOpts := []repository.CommitOption{
repository.WithFiles(map[string]io.Reader{
kusManifests.Path: strings.NewReader(kusManifests.Content),
}),
}
if signer != nil {
commitOpts = append(commitOpts, repository.WithSigner(signer))
}
commit, err := b.gitClient.Commit(git.Commit{ commit, err := b.gitClient.Commit(git.Commit{
Author: b.signature, Author: b.signature,
Message: commitMsg, Message: commitMsg,
}, repository.WithFiles(map[string]io.Reader{ }, commitOpts...)
kusManifests.Path: strings.NewReader(kusManifests.Content),
}), repository.WithSigner(signer))
if err != nil && err != git.ErrNoStagedFiles { if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit sync manifests: %w", err) return fmt.Errorf("failed to commit sync manifests: %w", err)
} }
@@ -511,7 +521,33 @@ func (b *PlainGitBootstrapper) cleanGitRepoDir() error {
return errors.Join(errs...) return errors.Join(errs...)
} }
func getOpenPgpEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) { // resolveSigner returns a signature.Signer derived from the configured
// commit-signing options, or (nil, nil) when no signing has been
// configured. GPG and SSH signing are mutually exclusive; if both have
// been configured the GPG path wins (the caller is responsible for
// rejecting the combination at flag-validation time).
func (b *PlainGitBootstrapper) resolveSigner() (signature.Signer, error) {
switch {
case b.gpgKeyRing != nil:
entity, err := SelectOpenPGPSigningEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID)
if err != nil {
return nil, fmt.Errorf("failed to load OpenPGP signing entity: %w", err)
}
return signature.NewOpenPGPSigner(entity)
case len(b.sshSigningKey) > 0:
return signature.NewSSHSigner(b.sshSigningKey, b.sshSigningPassword)
default:
return nil, nil
}
}
// SelectOpenPGPSigningEntity selects a single OpenPGP entity from the
// given keyring and decrypts its private key with the provided
// passphrase. When keyID is empty the keyring must contain exactly one
// entity; otherwise the entity with the matching 16-character key ID
// is selected. Returns an error if no matching entity is found, the
// matching entity has no private key, or decryption fails.
func SelectOpenPGPSigningEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) {
if len(keyRing) == 0 { if len(keyRing) == 0 {
return nil, fmt.Errorf("empty GPG key ring") return nil, fmt.Errorf("empty GPG key ring")
} }
+28
View File
@@ -145,6 +145,34 @@ func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper) o.applyGit(b.PlainGitBootstrapper)
} }
// WithSSHCommitSigning configures the bootstrapper to sign commits with
// an SSH private key. pem is the PEM-encoded private key (typically the
// OpenSSH "-----BEGIN OPENSSH PRIVATE KEY-----" format produced by
// ssh-keygen). password is the optional passphrase for the key; pass
// nil for an unencrypted key.
//
// WithSSHCommitSigning and WithGitCommitSigning are mutually exclusive;
// calling both is undefined behavior. The caller is responsible for
// rejecting that combination before constructing the bootstrapper (the
// flux CLI does this in bootstrapValidate).
func WithSSHCommitSigning(pem, password []byte) Option {
return sshCommitSigningOption{pem: pem, password: password}
}
type sshCommitSigningOption struct {
pem []byte
password []byte
}
func (o sshCommitSigningOption) applyGit(b *PlainGitBootstrapper) {
b.sshSigningKey = o.pem
b.sshSigningPassword = o.password
}
func (o sshCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
func LoadEntityListFromPath(path string) (openpgp.EntityList, error) { func LoadEntityListFromPath(path string) (openpgp.EntityList, error) {
if path == "" { if path == "" {
return nil, nil return nil, nil