CluPilotCloud/app/Services/Terminal/ServerIdentity.php

76 lines
2.7 KiB
PHP

<?php
namespace App\Services\Terminal;
use App\Models\Operator;
use App\Services\Secrets\SecretVault;
use App\Services\Ssh\Keypair;
use phpseclib3\Crypt\PublicKeyLoader;
use Throwable;
/**
* Die SSH-Kennung, mit der das Terminal auf den CluPilot-Server selbst kommt.
*
* Bewusst eine eigene, nicht der Flottenschlüssel: dessen öffentliche Hälfte
* liegt auf JEDEM übernommenen Host in `/root/.ssh/authorized_keys`. Ein
* Schlüssel für beides hieße, dass ein Host, der einmal in falsche Hände gerät,
* dieselbe Kennung trägt wie die Tür zur Steuerungsebene.
*
* Nur die private Hälfte wird aufbewahrt. Die öffentliche wird daraus
* abgeleitet, wann immer sie gebraucht wird — zwei gespeicherte Hälften können
* auseinanderlaufen, eine abgeleitete nie.
*/
final class ServerIdentity
{
/** Der Tresor-Eintrag. Steht hier, nicht verstreut bei den Aufrufern. */
public const VAULT_KEY = 'server.private_key';
public static function exists(): bool
{
return filled(app(SecretVault::class)->get(self::VAULT_KEY));
}
/**
* Erzeugt eine neue Kennung und ersetzt eine bestehende.
*
* Ersetzen sperrt aus: die alte öffentliche Hälfte steht auf dem Wirt in
* `authorized_keys` und wird von hier aus nicht angefasst. Zwischen dem
* Erzeugen und dem nächsten `install-agent.sh` erreicht das Terminal den
* Server nicht. Deshalb hängt an dieser Methode eine Rückfrage (R23), und
* deshalb sagt die Einstellungsseite danach, was noch zu tun ist.
*/
public static function generate(Operator $by): void
{
app(SecretVault::class)->put(self::VAULT_KEY, Keypair::generate()->privateKey, $by);
}
/**
* Die öffentliche Hälfte als eine Zeile `ssh-ed25519 AAAA… clupilot`, oder
* null, wenn es keine Kennung gibt.
*
* Genau diese Zeile holt sich der root-eigene Schritt auf dem Wirt über
* `php artisan clupilot:server-terminal-key`. Sie ist öffentlich — das
* Kommando gibt nichts preis, was nicht ohnehin in einer authorized_keys
* stünde.
*/
public static function publicKey(): ?string
{
$private = (string) app(SecretVault::class)->get(self::VAULT_KEY);
if (blank($private)) {
return null;
}
try {
return PublicKeyLoader::load($private)
->getPublicKey()
->toString('OpenSSH', ['comment' => Keypair::COMMENT]);
} catch (Throwable) {
// Unlesbar gespeichert heißt „keine Kennung". Nicht werfen: die
// Aufrufer sind ein Kommando, das eine Zeile ausgeben soll, und
// eine Seite, die nie mit einer Fehlerseite aufgehen darf.
return null;
}
}
}