Files
felhom.eu/hub/internal/offsite/scanner.go
T
admin 229650b4ee 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
2026-07-09 19:14:36 +02:00

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)
}