1
0
mirror of synced 2026-06-13 17:20:48 +00:00

Migrate bootstrap signing to generic Signer

Bumps fluxcd/pkg/git to v0.52.0, which exposes 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 c438a10efc
commit 273acb1eed
4 changed files with 92 additions and 28 deletions
+3 -3
View File
@@ -14,7 +14,7 @@ require (
github.com/fluxcd/cli-utils v1.2.1 github.com/fluxcd/cli-utils v1.2.1
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.51.0 github.com/fluxcd/pkg/auth v0.51.0
github.com/fluxcd/pkg/chartutil v1.26.0 github.com/fluxcd/pkg/chartutil v1.26.0
github.com/fluxcd/pkg/envsubst v1.7.0 github.com/fluxcd/pkg/envsubst v1.7.0
github.com/fluxcd/pkg/git v0.50.0 github.com/fluxcd/pkg/git v0.52.0
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.108.0 github.com/fluxcd/pkg/runtime v0.108.0
@@ -31,7 +31,7 @@ require (
github.com/fluxcd/pkg/ssa v0.75.0 github.com/fluxcd/pkg/ssa v0.75.0
github.com/fluxcd/pkg/ssh v0.25.0 github.com/fluxcd/pkg/ssh v0.25.0
github.com/fluxcd/pkg/tar v1.2.0 github.com/fluxcd/pkg/tar v1.2.0
github.com/fluxcd/pkg/version v0.15.0 github.com/fluxcd/pkg/version v0.16.0
github.com/fluxcd/source-controller/api v1.8.5 github.com/fluxcd/source-controller/api v1.8.5
github.com/fluxcd/source-watcher/api/v2 v2.1.1 github.com/fluxcd/source-watcher/api/v2 v2.1.1
github.com/go-git/go-git/v5 v5.19.1 github.com/go-git/go-git/v5 v5.19.1
+6 -6
View File
@@ -178,8 +178,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=
@@ -202,8 +202,8 @@ github.com/fluxcd/pkg/chartutil v1.26.0 h1:bgXwDKl2uGITSDxOXe4N5HTXAL0ilk4YVbPLU
github.com/fluxcd/pkg/chartutil v1.26.0/go.mod h1:sWDcF//xpIwZ/MQupwTmuM/SgGCkfsTd9BVOQJ3cTjM= github.com/fluxcd/pkg/chartutil v1.26.0/go.mod h1:sWDcF//xpIwZ/MQupwTmuM/SgGCkfsTd9BVOQJ3cTjM=
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.50.0 h1:gbGmSTjQ6CxqFmT9ZkLfYh8kG7CHqc7gHoPqcygixK8= github.com/fluxcd/pkg/git v0.52.0 h1:dgsliHdaLADUcDO4pI0pc11N4dZ21NfDdhNcgRNuAkM=
github.com/fluxcd/pkg/git v0.50.0/go.mod h1:OgaHoS0iR0GuLl+f778X7NrGy1pDH7xcpF/nsCRgJ9g= github.com/fluxcd/pkg/git v0.52.0/go.mod h1:mOvFDxoiuz+Mm4Ux1wKeTTckvBgZFvbTK8lNxmVHzKs=
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=
@@ -220,8 +220,8 @@ github.com/fluxcd/pkg/ssh v0.25.0 h1:4Y9WmuNqyKvH759UznU5DGHRcOuoJ/dQM6sbsaDZYYM
github.com/fluxcd/pkg/ssh v0.25.0/go.mod h1:Fli2Ogu4uaIVGbCy+r0vvZlMO0RfuInyNY1q2FVIx0o= github.com/fluxcd/pkg/ssh v0.25.0/go.mod h1:Fli2Ogu4uaIVGbCy+r0vvZlMO0RfuInyNY1q2FVIx0o=
github.com/fluxcd/pkg/tar v1.2.0 h1:T6WFB5M0YRHktlrgdKNskqpdp76TVDdWTOeuWz33CFs= github.com/fluxcd/pkg/tar v1.2.0 h1:T6WFB5M0YRHktlrgdKNskqpdp76TVDdWTOeuWz33CFs=
github.com/fluxcd/pkg/tar v1.2.0/go.mod h1:Wlalp5vIVe+BbckkKkqExKcoHAeeWJPAzwK7ONeFcS0= github.com/fluxcd/pkg/tar v1.2.0/go.mod h1:Wlalp5vIVe+BbckkKkqExKcoHAeeWJPAzwK7ONeFcS0=
github.com/fluxcd/pkg/version v0.15.0 h1:E2Ju4i0vj8ZXLHKz/F4a8JTmDh7Jcg8okB0hK5rEoTM= github.com/fluxcd/pkg/version v0.16.0 h1:VR9+143LAwbyUSAaMhiJHbfsiU+fTjA9L/3dr1ucfrI=
github.com/fluxcd/pkg/version v0.15.0/go.mod h1:LEHnvLMgbTk4kelF+JHHzaG77kY9uTWodMtadPRMEW8= github.com/fluxcd/pkg/version v0.16.0/go.mod h1:2M/l90CmbDaD21JTh77hjwaUbd/YM96+Fo8x4fMdxLI=
github.com/fluxcd/source-controller/api v1.8.5 h1:mLKc9YVMk46JCt1BQbkG6irkrpBZp95kiXh2+GYB6KQ= github.com/fluxcd/source-controller/api v1.8.5 h1:mLKc9YVMk46JCt1BQbkG6irkrpBZp95kiXh2+GYB6KQ=
github.com/fluxcd/source-controller/api v1.8.5/go.mod h1:sio4t49RDx+S1etHRFAEEw8qfVuw0KKlOg8bRVlEYPM= github.com/fluxcd/source-controller/api v1.8.5/go.mod h1:sio4t49RDx+S1etHRFAEEw8qfVuw0KKlOg8bRVlEYPM=
github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI= github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI=
+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