// Package hold generates per-run ephemeral ed25519 keypairs for the // FailedHolding flow. When a run fails, the agent asks the orchestrator // for a pubkey, drops it into /root/.ssh/authorized_keys, and reports // its LAN IP. The orchestrator stores the private key next to the run's // artifacts and surfaces `ssh -i root@` on the tile. package hold import ( "crypto/ed25519" "crypto/rand" "encoding/pem" "fmt" "os" "path/filepath" "strings" "golang.org/x/crypto/ssh" ) // Keypair bundles the PEM-encoded private key and the // authorized_keys-style public key line. type Keypair struct { PrivatePEM []byte AuthorizedKey string // "ssh-ed25519 AAAA... vetting-hold-N" } // Issue generates a new ed25519 keypair labelled for the given run. func Issue(runID int64) (*Keypair, error) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("generate ed25519: %w", err) } sshPub, err := ssh.NewPublicKey(pub) if err != nil { return nil, fmt.Errorf("ssh public key: %w", err) } blob := ssh.MarshalAuthorizedKey(sshPub) // "ssh-ed25519 AAAA...\n" line := strings.TrimRight(string(blob), "\n") if !strings.HasSuffix(line, fmt.Sprintf(" vetting-hold-%d", runID)) { line += fmt.Sprintf(" vetting-hold-%d", runID) } block, err := ssh.MarshalPrivateKey(priv, fmt.Sprintf("vetting-hold-%d", runID)) if err != nil { return nil, fmt.Errorf("marshal private key: %w", err) } return &Keypair{PrivatePEM: pem.EncodeToMemory(block), AuthorizedKey: line}, nil } // WritePrivateTo persists the PEM to the given path with 0600 perms // and returns the absolute path. The operator's shell reads this file // by path, so we keep it on disk per-run. func (kp *Keypair) WritePrivateTo(path string) (string, error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return "", err } if err := os.WriteFile(path, kp.PrivatePEM, 0o600); err != nil { return "", fmt.Errorf("write hold key: %w", err) } abs, err := filepath.Abs(path) if err != nil { return path, nil } return abs, nil }