229650b4ee
Descriptor.HostFingerprint (SHA256, non-secret), captured at provision via an x/crypto/ssh keyscan (SSHHostKeyScanner — dials :23, grabs the host key from the handshake, no ssh binary). Fail-closed: nil scanner or scan failure → error (don't serve a descriptor the controller can't verify). Pairs with controller v0.106.0 which re-scans + refuses on mismatch (no blind TOFU). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
58 lines
1.7 KiB
Go
58 lines
1.7 KiB
Go
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)
|
|
}
|