45 lines
1.6 KiB
PHP
45 lines
1.6 KiB
PHP
<?php
|
|
|
|
namespace App\Support\Ssh;
|
|
|
|
use App\Models\Server;
|
|
use phpseclib3\Net\SSH2;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* TOFU (trust-on-first-use) host-key pinning. phpseclib verifies the key-exchange
|
|
* signature but does NOT pin the server's host key, so a MITM presenting its own
|
|
* valid key would be accepted. We record the host key on first connect and refuse
|
|
* the connection on any later mismatch.
|
|
*/
|
|
trait VerifiesHostKey
|
|
{
|
|
private function verifyHostKey(SSH2 $conn, Server $server): void
|
|
{
|
|
$hostKey = $conn->getServerPublicHostKey();
|
|
|
|
if ($hostKey === false) {
|
|
throw new RuntimeException("Host-Key von {$server->name} ({$server->ip}) nicht lesbar — Verbindung abgebrochen.");
|
|
}
|
|
|
|
$known = (string) ($server->ssh_host_key ?? '');
|
|
|
|
if ($known !== '') {
|
|
if (! hash_equals($known, $hostKey)) {
|
|
throw new RuntimeException("Host-Key-Mismatch fuer {$server->name} ({$server->ip}) — moegliches MITM. Verbindung abgebrochen.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Trust on first use — but ONLY pin persisted fleet servers. A transient Server (the
|
|
// "Clusev host" built on the fly from HostCredential) must never be written to the DB:
|
|
// saving it would spawn a junk fleet row on every request. The host is reached over the
|
|
// local Docker gateway (host.docker.internal), which has no network path to MITM, so
|
|
// skipping the pin there is safe.
|
|
if ($server->exists) {
|
|
$server->forceFill(['ssh_host_key' => $hostKey])->save();
|
|
}
|
|
}
|
|
}
|