diff --git a/main.go b/main.go index 1c398adc3..98571d099 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,7 @@ import ( "github.com/fluxcd/source-controller/controllers" "github.com/fluxcd/source-controller/internal/cache" "github.com/fluxcd/source-controller/internal/helm" + "github.com/fluxcd/source-controller/pkg/git" "github.com/fluxcd/source-controller/pkg/git/libgit2/managed" // +kubebuilder:scaffold:imports ) @@ -90,6 +91,7 @@ func main() { helmCacheMaxSize int helmCacheTTL string helmCachePurgeInterval string + kexAlgos []string ) flag.StringVar(&metricsAddr, "metrics-addr", envOrDefault("METRICS_ADDR", ":8080"), @@ -120,6 +122,8 @@ func main() { "The TTL of an index in the cache. Valid time units are ns, us (or µs), ms, s, m, h.") flag.StringVar(&helmCachePurgeInterval, "helm-cache-purge-interval", "1m", "The interval at which the cache is purged. Valid time units are ns, us (or µs), ms, s, m, h.") + flag.StringSliceVar(&kexAlgos, "ssh-kex-algos", []string{}, + "The list of key exchange algorithms to use for ssh connections, arranged from most preferred to the least.") clientOptions.BindFlags(flag.CommandLine) logOptions.BindFlags(flag.CommandLine) @@ -174,6 +178,7 @@ func main() { storageAdvAddr = determineAdvStorageAddr(storageAddr, setupLog) } storage := mustInitStorage(storagePath, storageAdvAddr, setupLog) + setPreferredKexAlgos(kexAlgos) if err = (&controllers.GitRepositoryReconciler{ Client: mgr.GetClient(), @@ -333,3 +338,7 @@ func envOrDefault(envName, defaultValue string) string { return defaultValue } + +func setPreferredKexAlgos(algos []string) { + git.KexAlgos = algos +} diff --git a/pkg/git/gogit/transport.go b/pkg/git/gogit/transport.go index cd59110df..6be46b0cc 100644 --- a/pkg/git/gogit/transport.go +++ b/pkg/git/gogit/transport.go @@ -26,6 +26,8 @@ import ( "github.com/fluxcd/pkg/ssh/knownhosts" "github.com/fluxcd/source-controller/pkg/git" + + gossh "golang.org/x/crypto/ssh" ) // transportAuth constructs the transport.AuthMethod for the git.Transport of @@ -58,7 +60,10 @@ func transportAuth(opts *git.AuthOptions) (transport.AuthMethod, error) { } pk.HostKeyCallback = callback } - return pk, nil + customPK := &CustomPublicKeys{ + pk: pk, + } + return customPK, nil } case "": return nil, fmt.Errorf("no transport type set") @@ -75,3 +80,28 @@ func caBundle(opts *git.AuthOptions) []byte { } return opts.CAFile } + +// CustomPublicKeys is a wrapper around ssh.PublicKeys to help us +// customize the ssh config. It implements ssh.AuthMethod. +type CustomPublicKeys struct { + pk *ssh.PublicKeys +} + +func (a *CustomPublicKeys) Name() string { + return a.pk.Name() +} + +func (a *CustomPublicKeys) String() string { + return a.pk.String() +} + +func (a *CustomPublicKeys) ClientConfig() (*gossh.ClientConfig, error) { + config, err := a.pk.ClientConfig() + if err != nil { + return nil, err + } + if len(git.KexAlgos) > 0 { + config.Config.KeyExchanges = git.KexAlgos + } + return config, nil +} diff --git a/pkg/git/gogit/transport_test.go b/pkg/git/gogit/transport_test.go index 43577d9be..729668190 100644 --- a/pkg/git/gogit/transport_test.go +++ b/pkg/git/gogit/transport_test.go @@ -22,7 +22,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/plumbing/transport/ssh" . "github.com/onsi/gomega" "github.com/fluxcd/source-controller/pkg/git" @@ -72,6 +71,7 @@ func Test_transportAuth(t *testing.T) { name string opts *git.AuthOptions wantFunc func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) + kexAlgos []string wantErr error }{ { @@ -128,10 +128,10 @@ func Test_transportAuth(t *testing.T) { Identity: []byte(privateKeyFixture), }, wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) { - tt, ok := t.(*ssh.PublicKeys) + tt, ok := t.(*CustomPublicKeys) g.Expect(ok).To(BeTrue()) - g.Expect(tt.User).To(Equal(opts.Username)) - g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) + g.Expect(tt.pk.User).To(Equal(opts.Username)) + g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) }, }, { @@ -143,10 +143,31 @@ func Test_transportAuth(t *testing.T) { Identity: []byte(privateKeyPassphraseFixture), }, wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) { - tt, ok := t.(*ssh.PublicKeys) + tt, ok := t.(*CustomPublicKeys) g.Expect(ok).To(BeTrue()) - g.Expect(tt.User).To(Equal(opts.Username)) - g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) + g.Expect(tt.pk.User).To(Equal(opts.Username)) + g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) + }, + }, + { + name: "SSH with custom key exchanges", + opts: &git.AuthOptions{ + Transport: git.SSH, + Username: "example", + Identity: []byte(privateKeyFixture), + KnownHosts: []byte(knownHostsFixture), + }, + kexAlgos: []string{"curve25519-sha256", "diffie-hellman-group-exchange-sha256"}, + wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) { + tt, ok := t.(*CustomPublicKeys) + g.Expect(ok).To(BeTrue()) + g.Expect(tt.pk.User).To(Equal(opts.Username)) + g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) + config, err := tt.ClientConfig() + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(config.Config.KeyExchanges).To(Equal( + []string{"curve25519-sha256", "diffie-hellman-group-exchange-sha256"}), + ) }, }, { @@ -168,11 +189,11 @@ func Test_transportAuth(t *testing.T) { KnownHosts: []byte(knownHostsFixture), }, wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) { - tt, ok := t.(*ssh.PublicKeys) + tt, ok := t.(*CustomPublicKeys) g.Expect(ok).To(BeTrue()) - g.Expect(tt.User).To(Equal(opts.Username)) - g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) - g.Expect(tt.HostKeyCallback).ToNot(BeNil()) + g.Expect(tt.pk.User).To(Equal(opts.Username)) + g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa")) + g.Expect(tt.pk.HostKeyCallback).ToNot(BeNil()) }, }, { @@ -202,6 +223,10 @@ func Test_transportAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) + if len(tt.kexAlgos) > 0 { + git.KexAlgos = tt.kexAlgos + } + got, err := transportAuth(tt.opts) if tt.wantErr != nil { g.Expect(err).To(Equal(tt.wantErr)) diff --git a/pkg/git/libgit2/managed/ssh.go b/pkg/git/libgit2/managed/ssh.go index 0c7f916de..31dd6cdfe 100644 --- a/pkg/git/libgit2/managed/ssh.go +++ b/pkg/git/libgit2/managed/ssh.go @@ -58,6 +58,7 @@ import ( "golang.org/x/crypto/ssh" + "github.com/fluxcd/source-controller/pkg/git" git2go "github.com/libgit2/git2go/v33" ) @@ -344,6 +345,9 @@ func cacheKeyAndConfig(remoteAddress string, cred *git2go.Credential) (string, * Auth: []ssh.AuthMethod{ssh.PublicKeys(key)}, Timeout: sshConnectionTimeOut, } + if len(git.KexAlgos) > 0 { + cfg.Config.KeyExchanges = git.KexAlgos + } return ck, cfg, nil } diff --git a/pkg/git/options.go b/pkg/git/options.go index 9b186b391..3d8a92611 100644 --- a/pkg/git/options.go +++ b/pkg/git/options.go @@ -70,6 +70,9 @@ type AuthOptions struct { CAFile []byte } +// List of custom key exchange algorithms to be used for ssh connections. +var KexAlgos []string + // Validate the AuthOptions against the defined Transport. func (o AuthOptions) Validate() error { switch o.Transport {