Files
ClusterCanvas/service/internal/auth/ssh_connect.go
T
Squid f4dc8f63d7 Add SSH-managed node registry with connection testing and reauth.
Register hosts under Containers/VMs/Docker with encrypted key storage, and require re-authentication for sensitive account changes.
2026-07-18 16:39:10 +02:00

74 lines
1.7 KiB
Go

package auth
import (
"fmt"
"net"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
const defaultSSHPort = "22"
const sshDialTimeout = 10 * time.Second
// TestSSHConnection dials host over SSH using the given private key and username.
// Host key verification is intentionally skipped for this connectivity check;
// trust-on-first-use / known_hosts can be added later.
func TestSSHConnection(hostIP string, username string, privateKeyPEM string, passphrase string) error {
hostIP = strings.TrimSpace(hostIP)
username = strings.TrimSpace(username)
if hostIP == "" {
return fmt.Errorf("host IP is required")
}
if username == "" {
return fmt.Errorf("username is required")
}
signer, err := parseSigner(privateKeyPEM, passphrase)
if err != nil {
return err
}
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: sshDialTimeout,
}
address := net.JoinHostPort(hostIP, defaultSSHPort)
client, err := ssh.Dial("tcp", address, config)
if err != nil {
return fmt.Errorf("ssh dial %s: %w", address, err)
}
defer client.Close()
return nil
}
func parseSigner(privateKeyPEM string, passphrase string) (ssh.Signer, error) {
trimmed := strings.TrimSpace(privateKeyPEM)
if trimmed == "" {
return nil, fmt.Errorf("private key is required")
}
var (
signer ssh.Signer
err error
)
if strings.TrimSpace(passphrase) == "" {
signer, err = ssh.ParsePrivateKey([]byte(trimmed))
} else {
signer, err = ssh.ParsePrivateKeyWithPassphrase(
[]byte(trimmed),
[]byte(passphrase),
)
}
if err != nil {
return nil, fmt.Errorf("parse private key: %w", err)
}
return signer, nil
}