diff --git a/app/Http/Middleware/PublicSiteGate.php b/app/Http/Middleware/PublicSiteGate.php index e76e22a..b869224 100644 --- a/app/Http/Middleware/PublicSiteGate.php +++ b/app/Http/Middleware/PublicSiteGate.php @@ -57,6 +57,10 @@ class PublicSiteGate return $next($request); } + if ($this->isDownload($request)) { + return $next($request); + } + if ($this->fromManagementNetwork($request) || $this->isActiveOperator()) { return $next($request); } @@ -68,6 +72,30 @@ class PublicSiteGate ]); } + /** + * Die Datei-Domain bleibt erreichbar, auch solange die Website verborgen ist. + * + * Sonst bricht jede Host-Übernahme, und zwar unsichtbar: ein Server im + * Rettungssystem holt das Archiv mit `curl … | tar xz`, bekäme hier die + * 503-Platzhalterseite und schöbe sie in `tar`. Der Betreiber sähe einen + * Entpackfehler auf einer Maschine, an die er nur noch über die + * Anbieterkonsole kommt — und nichts davon deutete auf einen Schalter in + * der Konsole hin. Er ist per Definition weder in einem verwalteten Netz + * noch angemeldet: das ist ja gerade der Zustand, den er verlassen will. + * + * Über den ROUTENNAMEN, nicht über den Pfad: die Route existiert nur, wenn + * die Domain gebunden hat. Der Docblock oben warnt zu Recht davor, per + * Hostname auszunehmen — dort ginge es aber um das gesamte Portal, und ein + * gefälschter Host-Header hätte es aufgedeckt. Hier stehen zwei Routen + * dahinter: die Rechtsdokumente, die ohnehin öffentlich sind, und das + * Archiv, das ohne gültigen Einmal-Code mit 404 antwortet. Wer den Header + * fälscht, gewinnt damit nichts, was er nicht auch nach dem Start hätte. + */ + private function isDownload(Request $request): bool + { + return in_array($request->route()?->getName(), ['bootstrap.archive', 'files.public'], true); + } + private function fromManagementNetwork(Request $request): bool { $ranges = (array) config('admin_access.trusted_ranges', []); diff --git a/lang/de/hosts.php b/lang/de/hosts.php index 51d1be5..0347e97 100644 --- a/lang/de/hosts.php +++ b/lang/de/hosts.php @@ -135,45 +135,64 @@ return [ 'complete_host_onboarding' => 'Onboarding abschließen', ], 'takeover' => [ + 'eyebrow' => 'Host-Übernahme', 'how_title' => 'So läuft eine Host-Übernahme ab', - 'how_sub' => 'Sechs Schritte, etwa 20 bis 40 Minuten. Die ersten beiden passieren beim Anbieter — bevor hier etwas angelegt wird.', + 'how_sub' => 'Sechs Schritte, etwa 20 bis 40 Minuten. Die ersten beiden passieren beim Anbieter.', + 'rail_title' => 'Ablauf', 'badge_before' => 'vorher', - 'badge_now' => 'jetzt', - 'badge_later' => 'danach', + + // Kurz — sie stehen in der Schiene nebeneinander, nicht als Absatz. + 's1_label' => 'Server bestellen', + 's2_label' => 'Rettungssystem', + 's3_label' => 'Host anlegen', + 's4_label' => 'Zeile kopieren', + 's5_label' => 'Einfügen', + 's6_label' => 'Zusehen', 's1_title' => 'Server bestellen', - 's1_body' => 'Eine dedizierte Maschine. Keine Cloud-Instanz: ohne Hardware-Virtualisierung (/dev/kvm) startet später kein einziger Gast, und das Skript weist die Maschine gleich zu Beginn ab. Bei Hetzner CPX/CX ist das immer so, bei netcup je nach Produkt.', + 's1_body' => 'Dediziert, nicht Cloud. Ohne /dev/kvm startet kein Gast, und das Skript weist die Maschine ab.', 's2_title' => 'Rettungssystem starten', - 's2_body' => 'Im Kundenbereich des Anbieters das Rettungssystem einschalten UND den Server neu starten. Einschalten allein genügt nicht — er muss wirklich darin hochgefahren sein, sonst weigert sich das Skript. Es überschreibt Platten und prüft deshalb zuerst, ob es das darf.', + 's2_body' => 'Einschalten UND neu starten. Läuft der Server noch im alten System, weigert sich das Skript — es überschreibt Platten.', - 's3_title' => 'Host hier anlegen', - 's3_body' => 'Name, Rechenzentrum, öffentliche IP und root-Kennwort eintragen und speichern. Erst danach entsteht der Einmal-Code, und ab dann läuft seine Frist von 24 Stunden.', + 's3_title' => 'Host anlegen', + 's3_body' => 'Die Angaben unten. Danach entsteht der Einmal-Code, und seine Frist von 24 Stunden beginnt.', - 's4_title' => 'Die Befehlszeile kopieren', - 's4_body' => 'Sie erscheint direkt nach dem Speichern und wird genau einmal gezeigt.', + 's4_title' => 'Befehlszeile kopieren', + 's4_body' => 'Sie erscheint nach dem Speichern und wird genau einmal gezeigt.', 's5_title' => 'Im Rettungssystem einfügen', - 's5_body' => 'Per SSH als root auf den Server, Zeile einfügen, Eingabetaste. Es wird nichts abgetippt und nichts ausgefüllt: die Zeile trägt alles, was der Server vor dem Tunnel braucht.', - 's5_hint' => 'Die Zeile holt das Installationsskript von :url, packt es nach /opt/clupilot aus und startet es. Mehr lädt der Server nicht nach.', + 's5_body' => 'Per SSH als root, Zeile einfügen, Eingabetaste. Nichts abtippen, nichts ausfüllen.', + 's5_hint' => 'Holt das Skript von :url und startet es. Mehr lädt der Server nicht nach.', 's6_title' => 'Zusehen', - 's6_body' => 'Der Fortschritt läuft auf der Host-Seite mit, Abschnitt für Abschnitt. Bis der Tunnel steht, meldet der Server nichts — das ist so gewollt und dauert über den ersten Neustart hinweg. Danach kommt alles Vorherige auf einmal nach, mit den Zeiten von damals.', + 's6_body' => 'Der Fortschritt läuft auf der Host-Seite mit. Bis der Tunnel steht, meldet der Server nichts — danach kommt alles auf einmal nach.', + + 'requirements_title' => 'Beim Anbieter, vorher', + 'requirements_sub' => 'Beides muss erledigt sein, bevor unten gespeichert wird — der Code läuft ab dem Speichern.', + + 'form_title' => 'Der Host', + 'form_sub' => 'Vier Angaben. Den Rest holt sich der Server selbst.', + + 'command_title' => 'Die Befehlszeile', + 'command_sub' => 'Einfügen im Rettungssystem, als root. Sie trägt alles, was der Server vor dem Tunnel braucht.', + + 'after_title' => 'Danach', 'missing_title' => 'Der Tunnel ist noch nicht eingerichtet', - 'missing_body' => 'Es fehlt: :settings. Ohne diese Werte entsteht eine Befehlszeile, die sauber aussieht, läuft — und in einem Tunnel endet, der nie einen Handshake hat. Das fällt erst auf der Maschine auf. Trage sie unter Einstellungen ein, bevor du einen Host anlegst.', + 'missing_body' => 'Es fehlt: :settings. Ohne diese Werte entsteht eine Zeile, die läuft und in einem Tunnel endet, der nie einen Handshake hat — und das fällt erst auf der Maschine auf.', 'title' => ':name ist angelegt', 'subtitle' => 'Noch drei Schritte, dann macht der Server den Rest allein.', 'once_title' => 'Diese Zeile gibt es nur jetzt', - 'once_body' => 'In der Datenbank steht nur der Hash des Codes, der WireGuard-Schlüssel steht dort gar nicht. Wer die Seite verlässt oder neu lädt, bekommt sie nicht wieder, sondern legt einen neuen Code an — der alte ist damit wertlos.', + 'once_body' => 'In der Datenbank steht nur der Hash des Codes, der WireGuard-Schlüssel gar nicht. Neu laden bringt sie nicht zurück, sondern legt einen neuen Code an — der alte ist dann wertlos.', 'watch' => 'Fortschritt ansehen', 'copy' => 'Kopieren', 'copied' => 'Kopiert', - 'footnote' => 'Der Code gilt 24 Stunden und lässt sich genau einmal einlösen. Bleibt die Übernahme stehen, wird die Maschine neu aufgesetzt und nicht nachgebessert — was wo nachzusehen ist, steht im Runbook unter docs/runbooks/host-bootstrap.md.', + 'footnote' => 'Der Code gilt 24 Stunden und lässt sich einmal einlösen. Bleibt die Übernahme stehen, wird die Maschine neu aufgesetzt statt nachgebessert — siehe docs/runbooks/host-bootstrap.md.', ], ]; diff --git a/lang/en/hosts.php b/lang/en/hosts.php index 099f196..66eedb9 100644 --- a/lang/en/hosts.php +++ b/lang/en/hosts.php @@ -135,45 +135,63 @@ return [ 'complete_host_onboarding' => 'Complete onboarding', ], 'takeover' => [ + 'eyebrow' => 'Host takeover', 'how_title' => 'How a host takeover works', - 'how_sub' => 'Six steps, roughly 20 to 40 minutes. The first two happen at the provider — before anything is created here.', + 'how_sub' => 'Six steps, roughly 20 to 40 minutes. The first two happen at the provider.', + 'rail_title' => 'Procedure', 'badge_before' => 'first', - 'badge_now' => 'now', - 'badge_later' => 'then', + + 's1_label' => 'Order server', + 's2_label' => 'Rescue system', + 's3_label' => 'Add host', + 's4_label' => 'Copy line', + 's5_label' => 'Paste', + 's6_label' => 'Watch', 's1_title' => 'Order the server', - 's1_body' => 'A dedicated machine. Not a cloud instance: without hardware virtualisation (/dev/kvm) no guest will ever start, and the script turns the machine away at the very beginning. Hetzner CPX/CX never has it; netcup depends on the product.', + 's1_body' => 'Dedicated, not cloud. Without /dev/kvm no guest will start, and the script turns the machine away.', 's2_title' => 'Boot the rescue system', - 's2_body' => 'Enable the rescue system in the provider\'s panel AND restart the server. Enabling alone is not enough — it has to actually be running in it, or the script refuses. It overwrites disks, so it checks first whether it may.', + 's2_body' => 'Enable it AND restart. If the server is still in the old system the script refuses — it overwrites disks.', - 's3_title' => 'Add the host here', - 's3_body' => 'Name, datacenter, public IP and root password, then save. The one-time code is created at that point, and its 24-hour window starts running.', + 's3_title' => 'Add the host', + 's3_body' => 'The fields below. The one-time code is created after that, and its 24-hour window starts.', 's4_title' => 'Copy the command line', - 's4_body' => 'It appears right after saving and is shown exactly once.', + 's4_body' => 'It appears after saving and is shown exactly once.', 's5_title' => 'Paste it into the rescue system', - 's5_body' => 'SSH in as root, paste the line, press enter. Nothing is typed out and nothing is filled in: the line carries everything the server needs before the tunnel exists.', - 's5_hint' => 'The line fetches the installer from :url, unpacks it to /opt/clupilot and starts it. The server downloads nothing else.', + 's5_body' => 'SSH in as root, paste, press enter. Nothing typed out, nothing filled in.', + 's5_hint' => 'Fetches the installer from :url and starts it. The server downloads nothing else.', 's6_title' => 'Watch', - 's6_body' => 'Progress appears on the host page, section by section. Until the tunnel is up the server reports nothing — that is intended, and it lasts across the first reboot. Everything before it then arrives at once, carrying the times it actually happened.', + 's6_body' => 'Progress appears on the host page. Until the tunnel is up the server reports nothing — then everything arrives at once.', + + 'requirements_title' => 'At the provider, first', + 'requirements_sub' => 'Both have to be done before you save below — the code starts expiring the moment you do.', + + 'form_title' => 'The host', + 'form_sub' => 'Four fields. The server works out the rest itself.', + + 'command_title' => 'The command line', + 'command_sub' => 'Paste into the rescue system as root. It carries everything the server needs before the tunnel.', + + 'after_title' => 'Then', 'missing_title' => 'The tunnel is not set up yet', - 'missing_body' => 'Missing: :settings. Without these, the command line looks clean, runs, and ends in a tunnel that never handshakes — which only shows up on the machine. Fill them in under Settings before adding a host.', + 'missing_body' => 'Missing: :settings. Without these the line runs and ends in a tunnel that never handshakes — and that only shows up on the machine.', 'title' => ':name is created', 'subtitle' => 'Three steps left, then the server does the rest on its own.', 'once_title' => 'This line exists only now', - 'once_body' => 'The database holds only the hash of the code, and not the WireGuard key at all. Leaving or reloading this page does not bring it back — it mints a new code, and the old one becomes worthless.', + 'once_body' => 'The database holds only the hash of the code, and not the WireGuard key at all. Reloading does not bring it back — it mints a new code, and the old one becomes worthless.', 'watch' => 'Watch progress', 'copy' => 'Copy', 'copied' => 'Copied', - 'footnote' => 'The code is valid for 24 hours and can be redeemed exactly once. If the takeover stops, the machine is reinstalled rather than repaired — where to look is in the runbook at docs/runbooks/host-bootstrap.md.', + 'footnote' => 'The code is valid for 24 hours and can be redeemed once. If the takeover stops, the machine is reinstalled rather than repaired — see docs/runbooks/host-bootstrap.md.', ], ]; diff --git a/resources/views/components/admin/takeover-guide.blade.php b/resources/views/components/admin/takeover-guide.blade.php deleted file mode 100644 index 331a165..0000000 --- a/resources/views/components/admin/takeover-guide.blade.php +++ /dev/null @@ -1,109 +0,0 @@ -@props([ - // Die fertige Befehlszeile, sobald der Host angelegt ist — sonst null. - 'command' => null, - 'archiveUrl' => '', - 'hostUuid' => null, -]) - -{{-- Die Anleitung für eine Host-Übernahme, an EINER Stelle geschrieben. - - Sie steht bewusst dort, wo jemand sie braucht, bevor er anfängt: die - Schritte 1 und 2 passieren beim Anbieter, und wer sie erst nach dem Anlegen - liest, hat den Einmal-Code schon in der Zwischenablage und dessen Frist - schon laufen. Die erste Fassung dieser Seite zeigte die Anleitung erst - hinterher — das war genau dieser Fehler. - - Deshalb sind IMMER alle sechs Schritte zu sehen, auch die erledigten und - die kommenden. Eine Anleitung, die nur den aktuellen Schritt zeigt, - beantwortet die Frage „wie lange dauert das noch" nicht. --}} - -@php - $done = $command !== null; - // Schritt 3 ist das Formular auf dieser Seite; ist die Zeile da, sind 1–3 - // vorbei und 4 ist dran. - $current = $done ? 4 : 3; - - $steps = [ - 1 => ['title' => __('hosts.takeover.s1_title'), 'body' => __('hosts.takeover.s1_body')], - 2 => ['title' => __('hosts.takeover.s2_title'), 'body' => __('hosts.takeover.s2_body')], - 3 => ['title' => __('hosts.takeover.s3_title'), 'body' => __('hosts.takeover.s3_body')], - 4 => ['title' => __('hosts.takeover.s4_title'), 'body' => __('hosts.takeover.s4_body')], - 5 => ['title' => __('hosts.takeover.s5_title'), 'body' => __('hosts.takeover.s5_body')], - 6 => ['title' => __('hosts.takeover.s6_title'), 'body' => __('hosts.takeover.s6_body')], - ]; -@endphp - -
{{ __('hosts.takeover.how_sub') }}
-{{ $step['body'] }}
- - @if ($number === 4 && $done) -{{ $command }}
-
- {{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}
- @endif - - @if ($number === 6 && $hostUuid) - - @endif -{{ __('hosts.takeover.footnote') }}
-+ {{ $body }} + @if ($number === 5 && $archiveUrl) + {{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }} + @endif +
+{{ __('hosts.create_sub') }}
+{{ __('hosts.takeover.eyebrow') }}
++ {{ $command === null ? __('hosts.create_sub') : __('hosts.takeover.subtitle') }} +
+{{ __('hosts.takeover.missing_title') }}
+{{ __('hosts.takeover.missing_body', ['settings' => implode(', ', $missingSettings)]) }}
+{{ __('hosts.takeover.requirements_sub') }}
+ +{{ __('hosts.takeover.s1_body') }}
+{{ __('hosts.takeover.s2_body') }}
+{{ __('hosts.takeover.command_sub') }}
+ +{{ __('hosts.takeover.once_title') }}
+ +{{ $command }}
+ {{ __('hosts.takeover.once_body') }}
+{{ __('hosts.takeover.s5_body') }}
+{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}
+{{ __('hosts.takeover.s6_body') }}
+ ++ {{ __('hosts.takeover.footnote') }} +
{{ __('hosts.takeover.missing_title') }}
-{{ __('hosts.takeover.missing_body', ['settings' => implode(', ', $missingSettings)]) }}
-{{ __('hosts.takeover.subtitle') }}
-{{ __('hosts.takeover.once_title') }}
-{{ __('hosts.takeover.once_body') }}
-