Skip to content

Commit 2e91d63

Browse files
committed
Validate SSH host keys in GoGetter when SSHKnownHosts is provided
Add SSHKnownHosts []byte to Auth. When set, goGitGetter passes it to fleetssh.CreateKnownHostsCallBack so the SSH host key is verified against the provided data. When not set, InsecureIgnoreHostKey is kept as the default, matching Fleet's existing policy in pkg/git/netutils.go and gitcloner. Previously the implementation unconditionally used InsecureIgnoreHostKey, so a caller providing known_hosts data had no way to enforce strict host-key checks. Add GetGogsKnownHostEntry to integrationtests/utils to extract the container's ECDSA host key. Add two bundlereader integration tests: one that clones successfully with the correct known_host entry, and one that fails when a wrong key is supplied.
1 parent cc45bf5 commit 2e91d63

4 files changed

Lines changed: 81 additions & 2 deletions

File tree

integrationtests/bundlereader/getcontent_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,47 @@ var _ = Describe("GetContent fetches files from a git repository", Label("networ
239239
Expect(err).NotTo(HaveOccurred())
240240
Expect(files).To(HaveKey("README.md"))
241241
})
242+
243+
It("returns the repository files when a correct known host entry is provided", func() {
244+
privateKey, err := os.ReadFile(tmpKeyFile)
245+
Expect(err).NotTo(HaveOccurred())
246+
247+
knownHost, err := utils.GetGogsKnownHostEntry(context.Background(), container)
248+
Expect(err).NotTo(HaveOccurred())
249+
250+
source := fmt.Sprintf("%s/%s/%s", sshBase, utils.GogsUser, repoName)
251+
Eventually(func() error {
252+
_, err = bundlereader.GetContent(
253+
context.Background(), GinkgoT().TempDir(), source, "",
254+
bundlereader.Auth{SSHPrivateKey: privateKey, SSHKnownHosts: []byte(knownHost)}, false, nil,
255+
)
256+
return err
257+
}, timeout, interval).Should(Succeed())
258+
259+
files, err := bundlereader.GetContent(
260+
context.Background(), GinkgoT().TempDir(), source, "",
261+
bundlereader.Auth{SSHPrivateKey: privateKey, SSHKnownHosts: []byte(knownHost)}, false, nil,
262+
)
263+
Expect(err).NotTo(HaveOccurred())
264+
Expect(files).To(HaveKey("README.md"))
265+
})
266+
267+
It("fails when a wrong known host entry is provided", func() {
268+
privateKey, err := os.ReadFile(tmpKeyFile)
269+
Expect(err).NotTo(HaveOccurred())
270+
271+
// A valid-looking but wrong known_hosts entry: the key is for github.com, not our gogs server.
272+
wrongKnownHost := "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
273+
274+
source := fmt.Sprintf("%s/%s/%s", sshBase, utils.GogsUser, repoName)
275+
Eventually(func() error {
276+
_, err = bundlereader.GetContent(
277+
context.Background(), GinkgoT().TempDir(), source, "",
278+
bundlereader.Auth{SSHPrivateKey: privateKey, SSHKnownHosts: []byte(wrongKnownHost)}, false, nil,
279+
)
280+
return err
281+
}, timeout, interval).Should(HaveOccurred())
282+
})
242283
})
243284
})
244285

integrationtests/utils/gogs.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,30 @@ func CreateGogsContainerWithHTTPS(ctx context.Context, assetPath string) (testco
190190

191191
return container, caBundle, client, nil
192192
}
193+
194+
// GetGogsKnownHostEntry reads the container's SSH ECDSA host public key and returns
195+
// a known_hosts format line suitable for use with CreateKnownHostsCallBack.
196+
func GetGogsKnownHostEntry(ctx context.Context, container testcontainers.Container) (string, error) {
197+
port, err := container.MappedPort(ctx, GogsSSHPort)
198+
if err != nil {
199+
return "", err
200+
}
201+
host, err := container.Host(ctx)
202+
if err != nil {
203+
return "", err
204+
}
205+
pubKeyReader, err := container.CopyFileFromContainer(ctx, "/data/ssh/ssh_host_ecdsa_key.pub")
206+
if err != nil {
207+
return "", err
208+
}
209+
defer pubKeyReader.Close()
210+
pubKeyBytes, err := io.ReadAll(pubKeyReader)
211+
if err != nil {
212+
return "", err
213+
}
214+
fields := strings.Fields(string(pubKeyBytes))
215+
if len(fields) < 2 {
216+
return "", fmt.Errorf("unexpected known_hosts key format: %q", string(pubKeyBytes))
217+
}
218+
return fmt.Sprintf("[%s]:%s %s %s", host, port.Port(), fields[0], fields[1]), nil
219+
}

internal/bundlereader/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Auth struct {
1818
Password string `json:"password,omitempty"`
1919
CABundle []byte `json:"caBundle,omitempty"`
2020
SSHPrivateKey []byte `json:"sshPrivateKey,omitempty"`
21+
SSHKnownHosts []byte `json:"sshKnownHosts,omitempty"`
2122
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
2223
BasicHTTP bool `json:"basicHTTP,omitempty"`
2324
// remember to update Hash() when adding/modifying fields
@@ -46,6 +47,7 @@ func (a Auth) Hash() string {
4647
[]byte(a.Password),
4748
a.CABundle,
4849
a.SSHPrivateKey,
50+
a.SSHKnownHosts,
4951
{toByte(a.InsecureSkipVerify)},
5052
{toByte(a.BasicHTTP)},
5153
} {

internal/bundlereader/gitgetter.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
1919
gossh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
2020
"github.com/hashicorp/go-getter/v2"
21+
fleetssh "github.com/rancher/fleet/internal/ssh"
2122
"golang.org/x/crypto/ssh"
2223
)
2324

@@ -243,8 +244,16 @@ func (g *goGitGetter) setAuth(opts *gogit.CloneOptions, u *url.URL, sshKeyPEM []
243244
if err != nil {
244245
return fmt.Errorf("parsing SSH private key: %w", err)
245246
}
246-
//nolint:gosec // G106: InsecureIgnoreHostKey matches go-getter's prior behaviour
247-
pubKeys.HostKeyCallback = ssh.InsecureIgnoreHostKey()
247+
if len(g.auth.SSHKnownHosts) > 0 {
248+
pubKeys.HostKeyCallback, err = fleetssh.CreateKnownHostsCallBack(g.auth.SSHKnownHosts)
249+
if err != nil {
250+
return fmt.Errorf("creating known_hosts callback: %w", err)
251+
}
252+
} else {
253+
//nolint:gosec // G106: InsecureIgnoreHostKey is the default when no known_hosts
254+
// are provided, matching Fleet's behaviour in pkg/git/netutils.go and gitcloner.
255+
pubKeys.HostKeyCallback = ssh.InsecureIgnoreHostKey()
256+
}
248257
opts.Auth = pubKeys
249258
return nil
250259
}

0 commit comments

Comments
 (0)