CluPilotCloud/app/Provisioning/Steps/Host/EstablishSshTrust.php

103 lines
4.2 KiB
PHP

<?php
namespace App\Provisioning\Steps\Host;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Ssh\RemoteShell;
use App\Support\ProvisioningSettings;
use Illuminate\Support\Facades\Crypt;
/**
* First login with the one-time root password, deploy CluPilot's SSH key, pin
* the host key, verify key login, then scrub the password from the run context.
*/
class EstablishSshTrust extends HostStep
{
public function __construct(private RemoteShell $shell) {}
public function key(): string
{
return 'establish_ssh_trust';
}
public function execute(ProvisioningRun $run): StepResult
{
$host = $this->host($run);
// Idempotent: already trusted (host key pinned) and password already scrubbed.
if (filled($host->ssh_host_key) && blank($run->context('root_password'))) {
return StepResult::advance();
}
$publicKey = trim(ProvisioningSettings::sshPublicKey());
$password = Crypt::decryptString((string) $run->context('root_password'));
$this->shell->connectWithPassword($host->public_ip, 'root', $password);
$this->shell->run('mkdir -p /root/.ssh && chmod 700 /root/.ssh');
$this->shell->run(
'grep -qxF '.escapeshellarg($publicKey).' /root/.ssh/authorized_keys 2>/dev/null '.
'|| echo '.escapeshellarg($publicKey).' >> /root/.ssh/authorized_keys'
);
$host->update(['ssh_host_key' => $this->shell->hostKeyFingerprint()]);
// Verify the key works, then remove the password from state.
$this->keyLogin($this->shell, $host);
$run->forgetContext('root_password');
$this->refusePasswordLogins();
return StepResult::advance();
}
/**
* Ab hier kommt man auf diese Maschine nur noch mit dem Schlüssel.
*
* Bis hierher blieb die Anmeldung als root MIT PASSWORT erlaubt — und Port
* 22 steht während der ganzen Übernahme offen im Internet, denn die
* Host-Firewall macht ihn erst als vorletzter Schritt zu. Zwischen diesem
* Schritt und jenem lag also ein Fenster von Stunden, in dem jede Maschine
* der Welt Root-Passwörter durchprobieren durfte. Und wer später einmal das
* Notfallskript benutzt, reisst dieses Fenster wieder auf.
*
* Der richtige Zeitpunkt ist GENAU HIER: eine Zeile weiter oben hat sich
* `keyLogin()` erfolgreich mit dem Schlüssel angemeldet. Wir wissen also,
* dass der Weg hinein steht, bevor wir den anderen zumachen.
*
* `reload`, nicht `restart`: bestehende Sitzungen bleiben, auch die, über
* die wir hier gerade reden. Und `sshd -t` davor — eine Konfiguration, die
* sshd nicht laden kann, dürfte er nie zu sehen bekommen.
*
* Fehlschläge sind ausdrücklich KEIN Grund, die Übernahme abzubrechen: der
* Schlüssel liegt dann trotzdem, die Maschine ist benutzbar, und die
* Firewall macht Port 22 weiter unten ohnehin zu. Eine Härtung, die einen
* ganzen Aufbau scheitern lässt, wird beim nächsten Mal weggelassen.
*/
private function refusePasswordLogins(): void
{
// Eigene Datei unter sshd_config.d, nicht die Hauptdatei bearbeiten:
// Debians sshd_config zieht dieses Verzeichnis in der ERSTEN Zeile
// herein, und bei sshd gewinnt der erste Treffer. Was hier steht,
// schlägt damit alles, was weiter unten in der Hauptdatei steht — ohne
// dass wir eine fremde Datei umschreiben, die das nächste Systemupdate
// wieder ersetzt.
$this->shell->run('install -d -m 755 /etc/ssh/sshd_config.d');
$this->shell->putFile('/etc/ssh/sshd_config.d/99-clupilot.conf', <<<'SSHD'
# Geschrieben von CluPilot (EstablishSshTrust). Nicht von Hand ändern —
# der nächste Lauf dieses Schrittes überschreibt die Datei.
#
# Auf diese Maschine kommt man mit dem Flottenschlüssel. Ein Passwort ist ab
# hier keine Eintrittskarte mehr, auch nicht für root. Der Weg zurück, wenn der
# Schlüssel je verlorengeht, ist die Konsole des Anbieters — nicht ein
# Passwort, das jemand raten kann.
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
SSHD);
$this->shell->run('sshd -t && { systemctl reload ssh || systemctl reload sshd; }');
}
}