Use ephemeral SSH keys per rebuild instead of static config keys
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:
@@ -82,7 +82,13 @@ func (a *BootAPI) AnswerFile(w http.ResponseWriter, r *http.Request) {
|
||||
a.Runner.Transition(r.Context(), host.ID, statemachine.TriggerAnswerServed)
|
||||
}
|
||||
|
||||
answer := pxe.GenerateAnswerFile(host, st, a.Config)
|
||||
_, pubKey, _ := a.Hosts.GetEphemeralKey(r.Context(), host.ID)
|
||||
if pubKey == "" {
|
||||
http.Error(w, "no ephemeral key for host", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
answer := pxe.GenerateAnswerFile(host, st, a.Config, pubKey)
|
||||
w.Header().Set("Content-Type", "application/toml")
|
||||
w.Write([]byte(answer))
|
||||
}
|
||||
|
||||
+15
-8
@@ -18,14 +18,15 @@ import (
|
||||
)
|
||||
|
||||
type HostAPI struct {
|
||||
Hosts *store.Hosts
|
||||
Ops *store.Operations
|
||||
Locks *store.Locks
|
||||
Images *store.Images
|
||||
Runner *orchestrator.Runner
|
||||
PXE *pxe.Supervisor
|
||||
Config *config.Config
|
||||
ServerTypes *config.ServerTypeRegistry
|
||||
Hosts *store.Hosts
|
||||
Ops *store.Operations
|
||||
Locks *store.Locks
|
||||
Images *store.Images
|
||||
Runner *orchestrator.Runner
|
||||
Orchestrator *orchestrator.HostOrchestrator
|
||||
PXE *pxe.Supervisor
|
||||
Config *config.Config
|
||||
ServerTypes *config.ServerTypeRegistry
|
||||
}
|
||||
|
||||
func (a *HostAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -124,6 +125,12 @@ func (a *HostAPI) Rebuild(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.Orchestrator.PrepareRebuild(r.Context(), host.ID); err != nil {
|
||||
_ = a.Locks.Release(r.Context(), host.ID)
|
||||
writeJSONErr(w, http.StatusInternalServerError, "failed to generate SSH key: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := a.Runner.Transition(r.Context(), host.ID, statemachine.TriggerRebuildRequested); err != nil {
|
||||
_ = a.Locks.Release(r.Context(), host.ID)
|
||||
writeJSONErr(w, http.StatusConflict, err.Error())
|
||||
|
||||
+22
-20
@@ -58,17 +58,6 @@ func newTestServer(t *testing.T) *httptest.Server {
|
||||
|
||||
pxeSupervisor := pxe.NewSupervisor(pxe.SupervisorConfig{Enabled: false})
|
||||
|
||||
hostAPI := &api.HostAPI{
|
||||
Hosts: hosts,
|
||||
Ops: ops,
|
||||
Locks: locks,
|
||||
Images: images,
|
||||
Runner: runner,
|
||||
PXE: pxeSupervisor,
|
||||
Config: cfg,
|
||||
ServerTypes: serverTypes,
|
||||
}
|
||||
|
||||
hostOrch := &orchestrator.HostOrchestrator{
|
||||
Runner: runner,
|
||||
Hosts: hosts,
|
||||
@@ -79,6 +68,18 @@ func newTestServer(t *testing.T) *httptest.Server {
|
||||
ServerTypes: serverTypes,
|
||||
}
|
||||
|
||||
hostAPI := &api.HostAPI{
|
||||
Hosts: hosts,
|
||||
Ops: ops,
|
||||
Locks: locks,
|
||||
Images: images,
|
||||
Runner: runner,
|
||||
Orchestrator: hostOrch,
|
||||
PXE: pxeSupervisor,
|
||||
Config: cfg,
|
||||
ServerTypes: serverTypes,
|
||||
}
|
||||
|
||||
bootAPI := &api.BootAPI{
|
||||
Hosts: hosts,
|
||||
Images: images,
|
||||
@@ -89,15 +90,16 @@ func newTestServer(t *testing.T) *httptest.Server {
|
||||
}
|
||||
|
||||
ui := &api.UI{
|
||||
Hosts: hosts,
|
||||
Ops: ops,
|
||||
Locks: locks,
|
||||
Images: images,
|
||||
Runner: runner,
|
||||
Hub: hub,
|
||||
PXE: pxeSupervisor,
|
||||
Config: cfg,
|
||||
ServerTypes: serverTypes,
|
||||
Hosts: hosts,
|
||||
Ops: ops,
|
||||
Locks: locks,
|
||||
Images: images,
|
||||
Runner: runner,
|
||||
Orchestrator: hostOrch,
|
||||
Hub: hub,
|
||||
PXE: pxeSupervisor,
|
||||
Config: cfg,
|
||||
ServerTypes: serverTypes,
|
||||
}
|
||||
|
||||
router := httpserver.NewRouter(httpserver.Deps{
|
||||
|
||||
+17
-9
@@ -19,15 +19,16 @@ import (
|
||||
)
|
||||
|
||||
type UI struct {
|
||||
Hosts *store.Hosts
|
||||
Ops *store.Operations
|
||||
Locks *store.Locks
|
||||
Images *store.Images
|
||||
Runner *orchestrator.Runner
|
||||
Hub *events.Hub
|
||||
PXE *pxe.Supervisor
|
||||
Config *config.Config
|
||||
ServerTypes *config.ServerTypeRegistry
|
||||
Hosts *store.Hosts
|
||||
Ops *store.Operations
|
||||
Locks *store.Locks
|
||||
Images *store.Images
|
||||
Runner *orchestrator.Runner
|
||||
Orchestrator *orchestrator.HostOrchestrator
|
||||
Hub *events.Hub
|
||||
PXE *pxe.Supervisor
|
||||
Config *config.Config
|
||||
ServerTypes *config.ServerTypeRegistry
|
||||
}
|
||||
|
||||
func (u *UI) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -128,6 +129,13 @@ func (u *UI) TriggerRebuild(w http.ResponseWriter, r *http.Request) {
|
||||
Kind: model.OpRebuildProxmox,
|
||||
})
|
||||
_ = u.Locks.Acquire(r.Context(), host.ID, opID)
|
||||
|
||||
if err := u.Orchestrator.PrepareRebuild(r.Context(), host.ID); err != nil {
|
||||
_ = u.Locks.Release(r.Context(), host.ID)
|
||||
http.Error(w, "Failed to prepare rebuild: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
u.Runner.Transition(r.Context(), host.ID, statemachine.TriggerRebuildRequested)
|
||||
|
||||
hosts, _ := u.Hosts.List(r.Context())
|
||||
|
||||
Reference in New Issue
Block a user