diff --git a/app/Services/FleetService.php b/app/Services/FleetService.php index 8c221f1..0f6ceed 100644 --- a/app/Services/FleetService.php +++ b/app/Services/FleetService.php @@ -25,6 +25,9 @@ class FleetService { private const MARK = '===CLUSEV:'; + // Unique marker the connection probe echoes — proves the exec channel works. + private const PROBE_MARKER = 'CLUSEV_PROBE_OK'; + public function __construct(private readonly CredentialVault $vault) {} private function client(Server $server, int $timeout = 12): SshClient @@ -130,16 +133,20 @@ class FleetService try { $ssh = (new SshClient($this->vault, timeout: 10))->connect($server); try { - // A successful login (connect() throws otherwise) already proves the - // credential; running a trivial command also exercises the exec channel. - // Do NOT gate on the exit code: some SSH servers omit the optional - // exit-status packet, so getExitStatus() returns false even on success — - // reaching here without an exception is proof enough. - $ssh->run('true'); + // Require a unique marker in the OUTPUT to prove the exec channel really + // works — auth alone is not enough, since every fleet operation runs + // commands (a server that authenticates but rejects exec would otherwise + // be accepted). Checking the output, not the exit code, also tolerates + // SSH servers that omit the optional exit-status packet. + [$out] = $ssh->run('echo '.self::PROBE_MARKER); } finally { $ssh->disconnect(); } + if (! str_contains($out, self::PROBE_MARKER)) { + return ['ok' => false, 'error' => __('backend.ssh_probe_no_exec')]; + } + return ['ok' => true, 'error' => null]; } catch (Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; diff --git a/lang/de/backend.php b/lang/de/backend.php index 0b21181..8c388e0 100644 --- a/lang/de/backend.php +++ b/lang/de/backend.php @@ -38,6 +38,7 @@ return [ // ── HardeningService: errors + guards ──────────────────────────────── 'hardening_status_read_failed' => 'Härtungs-Status konnte nicht gelesen werden.', + 'ssh_probe_no_exec' => 'SSH-Anmeldung erfolgreich, aber es lassen sich keine Befehle ausführen.', 'ssh_password_self_lockout' => 'Clusev verbindet sich selbst per Passwort — Passwort-Login kann nicht deaktiviert werden, sonst verliert Clusev den Zugang. Hinterlege zuerst einen SSH-Key-Zugang.', 'ssh_password_no_key' => 'Kein SSH-Key hinterlegt — Passwort-Login kann nicht deaktiviert werden (Aussperrgefahr).', diff --git a/lang/en/backend.php b/lang/en/backend.php index 05bc84f..623d327 100644 --- a/lang/en/backend.php +++ b/lang/en/backend.php @@ -38,6 +38,7 @@ return [ // ── HardeningService: errors + guards ──────────────────────────────── 'hardening_status_read_failed' => 'Could not read hardening status.', + 'ssh_probe_no_exec' => 'SSH login succeeded, but commands could not be executed.', 'ssh_password_self_lockout' => 'Clusev connects itself via password — password login cannot be disabled, otherwise Clusev loses access. Set up SSH key access first.', 'ssh_password_no_key' => 'No SSH key in place — password login cannot be disabled (lockout risk).',