Use ephemeral SSH keys per rebuild instead of static config keys
build-and-push / test (push) Successful in 9m57s
build-and-push / build-and-push (push) Has been cancelled

Generate a fresh ed25519 key pair at rebuild time, inject the public key
into the Proxmox answer file, use the private key for cluster join over
SSH, then remove the key from both the remote host and the database.
This eliminates the need to manage static SSH keys in config/secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 21:09:22 -04:00
parent aec31b9f8b
commit b23ef64ee1
13 changed files with 191 additions and 68 deletions
+46 -21
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log"
"os"
"time"
"golang.org/x/crypto/ssh"
@@ -14,41 +13,39 @@ type ClusterJoiner struct {
ExistingNode string
ClusterName string
JoinFingerprint string
SSHKeyPath string
}
func (c *ClusterJoiner) Join(ctx context.Context, hostIP string) error {
client, err := c.connect(hostIP)
func (c *ClusterJoiner) Join(ctx context.Context, hostIP string, privateKey string, publicKey string) error {
client, err := c.connect(hostIP, privateKey)
if err != nil {
return fmt.Errorf("ssh connect to %s: %w", hostIP, err)
}
defer client.Close()
// Join the cluster
cmd := fmt.Sprintf("pvecm add %s --force", c.ExistingNode)
log.Printf("cluster: running on %s: %s", hostIP, cmd)
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("ssh session: %w", err)
}
defer session.Close()
output, err := session.CombinedOutput(cmd)
if err != nil {
return fmt.Errorf("pvecm add failed: %w\noutput: %s", err, string(output))
if err := c.runCmd(client, cmd); err != nil {
return fmt.Errorf("pvecm add failed: %w", err)
}
log.Printf("cluster: %s joined successfully", hostIP)
// Remove the ephemeral key from authorized_keys
escaped := escapeForSed(publicKey)
removeCmd := fmt.Sprintf(`sed -i '\|%s|d' /root/.ssh/authorized_keys`, escaped)
if err := c.runCmd(client, removeCmd); err != nil {
log.Printf("cluster: warning: failed to remove ephemeral key from %s: %v", hostIP, err)
} else {
log.Printf("cluster: ephemeral key removed from %s", hostIP)
}
return nil
}
func (c *ClusterJoiner) connect(hostIP string) (*ssh.Client, error) {
keyData, err := os.ReadFile(c.SSHKeyPath)
func (c *ClusterJoiner) connect(hostIP string, privateKeyPEM string) (*ssh.Client, error) {
signer, err := ssh.ParsePrivateKey([]byte(privateKeyPEM))
if err != nil {
return nil, fmt.Errorf("read ssh key: %w", err)
}
signer, err := ssh.ParsePrivateKey(keyData)
if err != nil {
return nil, fmt.Errorf("parse ssh key: %w", err)
return nil, fmt.Errorf("parse ephemeral key: %w", err)
}
config := &ssh.ClientConfig{
User: "root",
@@ -58,3 +55,31 @@ func (c *ClusterJoiner) connect(hostIP string) (*ssh.Client, error) {
}
return ssh.Dial("tcp", hostIP+":22", config)
}
func (c *ClusterJoiner) runCmd(client *ssh.Client, cmd string) error {
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("ssh session: %w", err)
}
defer session.Close()
output, err := session.CombinedOutput(cmd)
if err != nil {
return fmt.Errorf("%w\noutput: %s", err, string(output))
}
return nil
}
func escapeForSed(s string) string {
// Trim trailing newline and escape sed delimiter
result := ""
for _, c := range s {
if c == '|' {
result += `\|`
} else if c == '\n' {
continue
} else {
result += string(c)
}
}
return result
}