hub v0.38.0: offsite SLICE 2 — capture the box host-key fingerprint
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
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user