credential; if (! $cred) { return ['ok' => false, 'error' => __('backend.no_ssh_credential', ['server' => $server->name])]; } $privateKey = null; $publicKey = null; // Only generate + switch when the panel still authenticates with a password. If the // credential is already a key, the operator just wants password auth turned off. if ($cred->auth_type !== 'key') { $key = EC::createKey('ed25519'); $publicKey = $key->getPublicKey()->toString('OpenSSH', ['comment' => 'clusev-key']); $privateKey = (string) $key->toString('OpenSSH'); // 1) Install the public key — additive, harmless while password auth is still on. try { $this->fleet->addAuthorizedKey($server, $publicKey); } catch (Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; } // 2) Snapshot the current credential BEFORE the switch. $snapshot = [ 'auth_type' => $cred->auth_type, 'secret' => $cred->secret, 'passphrase' => $cred->passphrase, ]; // 3) Switch + verify — wrapped so a DB/SSH exception triggers best-effort restore. try { $cred->forceFill(['auth_type' => 'key', 'secret' => $privateKey, 'passphrase' => null])->save(); $server->load('credential'); $probe = $this->fleet->testConnection($server); } catch (Throwable $e) { // a DB/SSH error during switch+verify — best-effort restore, never half-switched. try { $cred->forceFill($snapshot)->save(); } catch (Throwable) { } return ['ok' => false, 'error' => $e->getMessage()]; } if (! $probe['ok']) { $cred->forceFill($snapshot)->save(); return ['ok' => false, 'error' => __('backend.ssh_key_verify_failed', ['error' => $probe['error'] ?? ''])]; } $this->audit($server, 'ssh_key.autoprovision'); } // 4) Disable password auth (the hardening guard passes now: credential is key + key installed). // The credential is already the verified key, so any failure here still leaves the operator // with working key access — never a lockout. try { $res = $this->hardening->apply($server, 'ssh_password', false); } catch (Throwable $e) { return array_filter([ 'ok' => false, 'error' => $e->getMessage(), 'privateKey' => $privateKey, 'publicKey' => $publicKey, ], fn ($v) => $v !== null); } if (! $res['ok']) { return array_filter([ 'ok' => false, 'error' => $res['output'], 'privateKey' => $privateKey, 'publicKey' => $publicKey, ], fn ($v) => $v !== null); } return array_filter([ 'ok' => true, 'privateKey' => $privateKey, 'publicKey' => $publicKey, ], fn ($v) => $v !== null); } private function audit(Server $server, string $action): void { try { AuditEvent::create([ 'user_id' => Auth::id(), 'server_id' => $server->id, 'actor' => Auth::user()?->name ?? 'system', 'action' => $action, 'target' => $server->name, 'ip' => request()->ip(), ]); } catch (Throwable) { // auditing must never abort a verified cutover } } }