package offsite import ( "context" "errors" "fmt" "net" "time" "golang.org/x/crypto/ssh" ) // SSHHostKeyScanner captures a box's SSH host-key fingerprint by dialing port 23 and grabbing the host key // from the handshake — BEFORE any auth (there is no credential; the callback aborts once the key is seen). // This is the hub-side equivalent of `ssh-keyscan`, using x/crypto/ssh so no ssh binary is needed in the // container. The captured fingerprint is non-secret (a public host-key identifier). type SSHHostKeyScanner struct { Timeout time.Duration } // errCaptured aborts the handshake once we have the host key (we never intended to authenticate). var errCaptured = errors.New("host key captured") func (s SSHHostKeyScanner) Fingerprint(ctx context.Context, host string, port int) (string, error) { timeout := s.Timeout if timeout == 0 { timeout = 10 * time.Second } var fp string cfg := &ssh.ClientConfig{ User: "felhom-keyscan", Auth: nil, // no auth — we abort in the host-key callback Timeout: timeout, HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error { fp = ssh.FingerprintSHA256(key) return errCaptured }, } d := net.Dialer{Timeout: timeout} conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", host, port)) if err != nil { return "", fmt.Errorf("dial: %w", err) } defer conn.Close() // ssh.NewClientConn runs the handshake; our callback captures the key then returns errCaptured, so this // returns an error — but fp is set. Any OTHER error (or no key) is a real failure. c, chans, reqs, herr := ssh.NewClientConn(conn, host, cfg) if c != nil { c.Close() } _ = chans _ = reqs if fp != "" { return fp, nil } return "", fmt.Errorf("host-key handshake: %w", herr) }