Skip to content

Commit 999529a

Browse files
authored
feat(scanner): detect host key change (future-architect#1406)
* feat(scanner): detect host key change * chore(scanner): add testcase
1 parent 847d820 commit 999529a

File tree

2 files changed

+357
-81
lines changed

2 files changed

+357
-81
lines changed

scanner/scanner.go

+163-81
Original file line numberDiff line numberDiff line change
@@ -346,119 +346,201 @@ func validateSSHConfig(c *config.ServerInfo) error {
346346
if err != nil {
347347
return xerrors.Errorf("Failed to lookup ssh binary path. err: %w", err)
348348
}
349-
sshKeygenBinaryPath, err := ex.LookPath("ssh-keygen")
350-
if err != nil {
351-
return xerrors.Errorf("Failed to lookup ssh-keygen binary path. err: %w", err)
352-
}
353349

354-
sshConfigCmd := []string{sshBinaryPath, "-G"}
355-
if c.SSHConfigPath != "" {
356-
sshConfigCmd = append(sshConfigCmd, "-F", c.SSHConfigPath)
357-
}
358-
if c.Port != "" {
359-
sshConfigCmd = append(sshConfigCmd, "-p", c.Port)
360-
}
361-
if c.User != "" {
362-
sshConfigCmd = append(sshConfigCmd, "-l", c.User)
363-
}
364-
if len(c.JumpServer) > 0 {
365-
sshConfigCmd = append(sshConfigCmd, "-J", strings.Join(c.JumpServer, ","))
366-
}
367-
sshConfigCmd = append(sshConfigCmd, c.Host)
368-
cmd := strings.Join(sshConfigCmd, " ")
369-
logging.Log.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
370-
r := localExec(*c, cmd, noSudo)
371-
if !r.isSuccess() {
372-
return xerrors.Errorf("Failed to print SSH configuration. err: %w", r.Error)
373-
}
374-
375-
var (
376-
hostname string
377-
strictHostKeyChecking string
378-
globalKnownHosts string
379-
userKnownHosts string
380-
proxyCommand string
381-
proxyJump string
382-
)
383-
for _, line := range strings.Split(r.Stdout, "\n") {
384-
switch {
385-
case strings.HasPrefix(line, "user "):
386-
user := strings.TrimPrefix(line, "user ")
387-
logging.Log.Debugf("Setting SSH User:%s for Server:%s ...", user, c.GetServerName())
388-
c.User = user
389-
case strings.HasPrefix(line, "hostname "):
390-
hostname = strings.TrimPrefix(line, "hostname ")
391-
case strings.HasPrefix(line, "port "):
392-
port := strings.TrimPrefix(line, "port ")
393-
logging.Log.Debugf("Setting SSH Port:%s for Server:%s ...", port, c.GetServerName())
394-
c.Port = port
395-
case strings.HasPrefix(line, "stricthostkeychecking "):
396-
strictHostKeyChecking = strings.TrimPrefix(line, "stricthostkeychecking ")
397-
case strings.HasPrefix(line, "globalknownhostsfile "):
398-
globalKnownHosts = strings.TrimPrefix(line, "globalknownhostsfile ")
399-
case strings.HasPrefix(line, "userknownhostsfile "):
400-
userKnownHosts = strings.TrimPrefix(line, "userknownhostsfile ")
401-
case strings.HasPrefix(line, "proxycommand "):
402-
proxyCommand = strings.TrimPrefix(line, "proxycommand ")
403-
case strings.HasPrefix(line, "proxyjump "):
404-
proxyJump = strings.TrimPrefix(line, "proxyjump ")
405-
}
406-
}
350+
sshConfigCmd := buildSSHConfigCmd(sshBinaryPath, c)
351+
logging.Log.Debugf("Executing... %s", strings.Replace(sshConfigCmd, "\n", "", -1))
352+
configResult := localExec(*c, sshConfigCmd, noSudo)
353+
if !configResult.isSuccess() {
354+
return xerrors.Errorf("Failed to print SSH configuration. err: %w", configResult.Error)
355+
}
356+
sshConfig := parseSSHConfiguration(configResult.Stdout)
357+
c.User = sshConfig.user
358+
logging.Log.Debugf("Setting SSH User:%s for Server:%s ...", sshConfig.user, c.GetServerName())
359+
c.Port = sshConfig.port
360+
logging.Log.Debugf("Setting SSH Port:%s for Server:%s ...", sshConfig.port, c.GetServerName())
407361
if c.User == "" || c.Port == "" {
408362
return xerrors.New("Failed to find User or Port setting. Please check the User or Port settings for SSH")
409363
}
410-
if strictHostKeyChecking == "false" || proxyCommand != "" || proxyJump != "" {
364+
365+
if sshConfig.strictHostKeyChecking == "false" {
366+
return nil
367+
}
368+
if sshConfig.proxyCommand != "" || sshConfig.proxyJump != "" {
369+
logging.Log.Debug("known_host check under Proxy is not yet implemented")
411370
return nil
412371
}
413372

414373
logging.Log.Debugf("Checking if the host's public key is in known_hosts...")
415374
knownHostsPaths := []string{}
416-
for _, knownHosts := range []string{userKnownHosts, globalKnownHosts} {
417-
for _, knownHost := range strings.Split(knownHosts, " ") {
418-
if knownHost != "" && knownHost != "/dev/null" {
419-
knownHostsPaths = append(knownHostsPaths, knownHost)
420-
}
375+
for _, knownHost := range append(sshConfig.userKnownHosts, sshConfig.globalKnownHosts...) {
376+
if knownHost != "" && knownHost != "/dev/null" {
377+
knownHostsPaths = append(knownHostsPaths, knownHost)
421378
}
422379
}
423380
if len(knownHostsPaths) == 0 {
424381
return xerrors.New("Failed to find any known_hosts to use. Please check the UserKnownHostsFile and GlobalKnownHostsFile settings for SSH")
425382
}
426383

384+
sshKeyscanBinaryPath, err := ex.LookPath("ssh-keyscan")
385+
if err != nil {
386+
return xerrors.Errorf("Failed to lookup ssh-keyscan binary path. err: %w", err)
387+
}
388+
sshScanCmd := strings.Join([]string{sshKeyscanBinaryPath, "-p", c.Port, sshConfig.hostname}, " ")
389+
r := localExec(*c, sshScanCmd, noSudo)
390+
if !r.isSuccess() {
391+
return xerrors.Errorf("Failed to ssh-keyscan. cmd: %s, err: %w", sshScanCmd, r.Error)
392+
}
393+
serverKeys := parseSSHScan(r.Stdout)
394+
395+
sshKeygenBinaryPath, err := ex.LookPath("ssh-keygen")
396+
if err != nil {
397+
return xerrors.Errorf("Failed to lookup ssh-keygen binary path. err: %w", err)
398+
}
427399
for _, knownHosts := range knownHostsPaths {
428-
if c.Port != "" && c.Port != "22" {
429-
cmd := fmt.Sprintf("%s -F %s -f %s", sshKeygenBinaryPath, fmt.Sprintf("\"[%s]:%s\"", hostname, c.Port), knownHosts)
430-
logging.Log.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
431-
if r := localExec(*c, cmd, noSudo); r.isSuccess() {
432-
return nil
400+
var hostname string
401+
if sshConfig.hostKeyAlias != "" {
402+
hostname = sshConfig.hostKeyAlias
403+
} else {
404+
if c.Port != "" && c.Port != "22" {
405+
hostname = fmt.Sprintf("\"[%s]:%s\"", sshConfig.hostname, c.Port)
406+
} else {
407+
hostname = sshConfig.hostname
433408
}
434409
}
435410
cmd := fmt.Sprintf("%s -F %s -f %s", sshKeygenBinaryPath, hostname, knownHosts)
436411
logging.Log.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
437412
if r := localExec(*c, cmd, noSudo); r.isSuccess() {
438-
return nil
413+
keyType, clientKey, err := parseSSHKeygen(r.Stdout)
414+
if err != nil {
415+
return xerrors.Errorf("Failed to parse ssh-keygen result. stdout: %s, err: %w", r.Stdout, r.Error)
416+
}
417+
if serverKey, ok := serverKeys[keyType]; ok && serverKey == clientKey {
418+
return nil
419+
}
420+
return xerrors.Errorf("Failed to find the server key that matches the key registered in the client. The server key may have been changed. Please exec `$ %s` and `$ %s` or `$ %s`",
421+
fmt.Sprintf("%s -R %s -f %s", sshKeygenBinaryPath, hostname, knownHosts),
422+
strings.Join(buildSSHBaseCmd(sshBinaryPath, c, nil), " "),
423+
buildSSHKeyScanCmd(sshKeyscanBinaryPath, c.Port, knownHostsPaths[0], sshConfig))
439424
}
440425
}
426+
return xerrors.Errorf("Failed to find the host in known_hosts. Please exec `$ %s` or `$ %s`",
427+
strings.Join(buildSSHBaseCmd(sshBinaryPath, c, nil), " "),
428+
buildSSHKeyScanCmd(sshKeyscanBinaryPath, c.Port, knownHostsPaths[0], sshConfig))
429+
}
441430

442-
sshConnArgs := []string{}
443-
sshKeyScanArgs := []string{"-H"}
431+
func buildSSHBaseCmd(sshBinaryPath string, c *config.ServerInfo, options []string) []string {
432+
cmd := []string{sshBinaryPath}
433+
if len(options) > 0 {
434+
cmd = append(cmd, options...)
435+
}
444436
if c.SSHConfigPath != "" {
445-
sshConnArgs = append(sshConnArgs, "-F", c.SSHConfigPath)
437+
cmd = append(cmd, "-F", c.SSHConfigPath)
446438
}
447439
if c.KeyPath != "" {
448-
sshConnArgs = append(sshConnArgs, "-i", c.KeyPath)
440+
cmd = append(cmd, "-i", c.KeyPath)
449441
}
450442
if c.Port != "" {
451-
sshConnArgs = append(sshConnArgs, "-p", c.Port)
452-
sshKeyScanArgs = append(sshKeyScanArgs, "-p", c.Port)
443+
cmd = append(cmd, "-p", c.Port)
453444
}
454445
if c.User != "" {
455-
sshConnArgs = append(sshConnArgs, "-l", c.User)
446+
cmd = append(cmd, "-l", c.User)
447+
}
448+
if len(c.JumpServer) > 0 {
449+
cmd = append(cmd, "-J", strings.Join(c.JumpServer, ","))
450+
}
451+
cmd = append(cmd, c.Host)
452+
return cmd
453+
}
454+
455+
func buildSSHConfigCmd(sshBinaryPath string, c *config.ServerInfo) string {
456+
return strings.Join(buildSSHBaseCmd(sshBinaryPath, c, []string{"-G"}), " ")
457+
}
458+
459+
func buildSSHKeyScanCmd(sshKeyscanBinaryPath, port, knownHosts string, sshConfig sshConfiguration) string {
460+
cmd := []string{sshKeyscanBinaryPath}
461+
if sshConfig.hashKnownHosts == "yes" {
462+
cmd = append(cmd, "-H")
463+
}
464+
if port != "" {
465+
cmd = append(cmd, "-p", port)
466+
}
467+
return strings.Join(append(cmd, sshConfig.hostname, ">>", knownHosts), " ")
468+
}
469+
470+
type sshConfiguration struct {
471+
hostname string
472+
hostKeyAlias string
473+
hashKnownHosts string
474+
user string
475+
port string
476+
strictHostKeyChecking string
477+
globalKnownHosts []string
478+
userKnownHosts []string
479+
proxyCommand string
480+
proxyJump string
481+
}
482+
483+
func parseSSHConfiguration(stdout string) sshConfiguration {
484+
sshConfig := sshConfiguration{}
485+
for _, line := range strings.Split(stdout, "\n") {
486+
switch {
487+
case strings.HasPrefix(line, "user "):
488+
sshConfig.user = strings.TrimPrefix(line, "user ")
489+
case strings.HasPrefix(line, "hostname "):
490+
sshConfig.hostname = strings.TrimPrefix(line, "hostname ")
491+
case strings.HasPrefix(line, "hostkeyalias "):
492+
sshConfig.hostKeyAlias = strings.TrimPrefix(line, "hostkeyalias ")
493+
case strings.HasPrefix(line, "hashknownhosts "):
494+
sshConfig.hashKnownHosts = strings.TrimPrefix(line, "hashknownhosts ")
495+
case strings.HasPrefix(line, "port "):
496+
sshConfig.port = strings.TrimPrefix(line, "port ")
497+
case strings.HasPrefix(line, "stricthostkeychecking "):
498+
sshConfig.strictHostKeyChecking = strings.TrimPrefix(line, "stricthostkeychecking ")
499+
case strings.HasPrefix(line, "globalknownhostsfile "):
500+
sshConfig.globalKnownHosts = strings.Split(strings.TrimPrefix(line, "globalknownhostsfile "), " ")
501+
case strings.HasPrefix(line, "userknownhostsfile "):
502+
sshConfig.userKnownHosts = strings.Split(strings.TrimPrefix(line, "userknownhostsfile "), " ")
503+
case strings.HasPrefix(line, "proxycommand "):
504+
sshConfig.proxyCommand = strings.TrimPrefix(line, "proxycommand ")
505+
case strings.HasPrefix(line, "proxyjump "):
506+
sshConfig.proxyJump = strings.TrimPrefix(line, "proxyjump ")
507+
}
508+
}
509+
return sshConfig
510+
}
511+
512+
func parseSSHScan(stdout string) map[string]string {
513+
keys := map[string]string{}
514+
for _, line := range strings.Split(stdout, "\n") {
515+
if line == "" || strings.HasPrefix(line, "# ") {
516+
continue
517+
}
518+
if ss := strings.Split(line, " "); len(ss) == 3 {
519+
keys[ss[1]] = ss[2]
520+
}
521+
}
522+
return keys
523+
}
524+
525+
func parseSSHKeygen(stdout string) (string, string, error) {
526+
for _, line := range strings.Split(stdout, "\n") {
527+
if line == "" || strings.HasPrefix(line, "# ") {
528+
continue
529+
}
530+
531+
// HashKnownHosts yes
532+
if strings.HasPrefix(line, "|1|") {
533+
ss := strings.Split(line, "|")
534+
if ss := strings.Split(ss[len(ss)-1], " "); len(ss) == 3 {
535+
return ss[1], ss[2], nil
536+
}
537+
} else {
538+
if ss := strings.Split(line, " "); len(ss) == 3 {
539+
return ss[1], ss[2], nil
540+
}
541+
}
456542
}
457-
sshConnArgs = append(sshConnArgs, c.Host)
458-
sshKeyScanArgs = append(sshKeyScanArgs, fmt.Sprintf("%s >> %s", hostname, knownHostsPaths[0]))
459-
sshConnCmd := fmt.Sprintf("ssh %s", strings.Join(sshConnArgs, " "))
460-
sshKeyScancmd := fmt.Sprintf("ssh-keyscan %s", strings.Join(sshKeyScanArgs, " "))
461-
return xerrors.Errorf("Failed to find the host in known_hosts. Please exec `$ %s` or `$ %s`", sshConnCmd, sshKeyScancmd)
543+
return "", "", xerrors.New("Failed to parse ssh-keygen result. err: public key not found")
462544
}
463545

464546
func (s Scanner) detectContainerOSes(hosts []osTypeInterface) (actives, inactives []osTypeInterface) {

0 commit comments

Comments
 (0)