fix(ssh): require an exec marker in the connection probe
Auth success alone does not prove the server can run commands (a server that authenticates but rejects exec channels would be accepted, yet every fleet operation runs commands). The probe now echoes a unique marker and requires it in the output — proving exec works, while still tolerating servers that omit the optional exit-status packet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
07f0a0cb00
commit
37efa64b9d
|
|
@ -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()];
|
||||
|
|
|
|||
|
|
@ -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).',
|
||||
|
||||
|
|
|
|||
|
|
@ -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).',
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue