Add the central site registry and remote updates

Every installation now reports address and version to sites.kant-it.de
every 15 minutes and picks up update orders there (ADR 123). Reports and
answers are HMAC-signed with a per-installation key sent on first
contact; an order can only install a published release with checksum.
The next request after the file swap runs the migrations, so a remote
update finishes without anyone in the admin.

The platform itself lives in zentrale/ (plain PHP + SQLite, not part of
the release archive): overview with outdated, silent and unassigned
installations, single and bulk updates, per-site detail and history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-14 10:43:13 +02:00
1 parent 32cb77b72e
commit 723680b7de
39 files changed
+2355 -27

No files matched your search

+19 -1
View File
@@ -1,6 +1,24 @@
# Architekturentscheidungen
Stand: 12.10.2026. Arbeitstitel KantSites; Produktname offen.
Stand: 14.10.2026. Arbeitstitel KantSites; Produktname offen.
## ADR 123 – Zentrale auf sites.kant-it.de: Meldung, Übersicht, Update aus der Ferne (14.10.2026)
Anlass: Umut: „Alle KantSites melden sich bei meiner Hauptplattform an … ich kann sehen, welche Domains welche Versionen nutzen, und kann von remote Aktualisierung anstoßen.“ Entschieden: `sites.kant-it.de`, ohne Lizenzschlüssel, Updates einzeln und für alle.
**Meldung (CMS).** Neue Aufgabe `zentrale` (`App\Ops\Tasks\ZentraleTask`, alle 15 Minuten) ruft `App\Ops\Zentrale::melden()` auf: `POST https://sites.kant-it.de/api/melden` mit Kennung, Adresse (`SITE_ORIGIN`), Fassung, PHP-Version, Datenbankart, Git/Archiv, Wartungszustand und Updatestand. Keine Inhalte, keine Besucherdaten, keine Zugangsdaten. Kennung und Schlüssel (32 bzw. 64 Hexzeichen, zufällig) liegen in `var/zentrale.json` und bleiben bei Updates erhalten. Abschalten mit `ZENTRALE_URL=aus`; Installationen mit lokalem `SITE_ORIGIN` (localhost, 127.0.0.1, `*.test`, `*.localhost`, `*.invalid`) melden nie.
**Echtheit ohne Lizenzschlüssel.** Beim ersten Kontakt schickt die Installation ihren Schlüssel über HTTPS mit (Vertrauen beim ersten Kontakt). Danach ist jede Meldung mit HMAC-SHA256 über den Rohtext unterschrieben (`X-KantSites-Signatur`), jede Antwort der Zentrale ebenso; die Installation verwirft Antworten ohne gültige Unterschrift. Eine bekannte Kennung mit fremdem Schlüssel weist die Zentrale ab (403). Kennt die Zentrale eine Kennung nicht mehr (409), meldet sich die Installation einmal mit Schlüssel neu an. Ein Auftrag kann nur „Update auf eine höhere Fassung der Form 1.2.3“ sein und läuft über `ArchiveUpdate` mit Prüfsumme der Releaseseite – mehr kann auch eine kompromittierte Zentrale nicht auslösen.
**Pull statt Push.** Die Zentrale verbindet sich nie mit einer Website. Sie merkt sich den Auftrag; die Website holt ihn mit ihrer nächsten Meldung ab. Keine Installation braucht dafür eine von außen erreichbare Steuerschnittstelle, und es geht auch hinter Firewalls.
**Update aus der Ferne.** `ZentraleTask` führt `ArchiveUpdate::run()` aus (Sicherung, Archiv, Prüfsumme, Dateitausch, Wartung an) und markiert den offenen zweiten Schritt mit `fern` (`ArchiveUpdate::markiereFern()`). Den zweiten Schritt erledigt die nächste Anfrage mit dem neuen Code: `RemoteUpdateFinishSubscriber` (kernel.request, Priorität 48, also vor der Wartungsseite) nimmt die Updatesperre, führt `finish()` mit der Funktionsprüfung aus und gibt die Website frei oder schaltet zurück. Damit das nicht auf den nächsten Besucher wartet, ruft die Aufgabe die eigene Startseite einmal auf (bis 300 s). Das Ergebnis geht mit der nächsten Meldung an die Zentrale und wird nach der Quittung aus `var/zentrale.json` gelöscht. Git-Arbeitskopien lehnen Aufträge ab. Die Funktionsprüfung nach den Migrationen ist dafür aus `SystemController` in `App\Ops\Funktionspruefung` gewandert; die Verwaltung nutzt denselben Dienst.
**Zentrale.** Eigener Ordner `zentrale/` im Repository, nicht Teil des Releasearchivs (`scripts/build-release.php` nimmt nur die aufgezählten Einträge). Reines PHP ab 8.1 mit SQLite (`var/zentrale.db`), ohne Framework und Composer, damit sie per FTP auf jedes Hosting passt. Anmeldung mit `password_hash`, Sitzungscookie `__Host-` mit SameSite=Strict, CSRF-Token auf jedem Formular, fünf Fehlversuche je IP in 15 Minuten, strenge CSP ohne Skripte, `noindex`. Das erste Konto legt an, wer den `einrichtungscode` aus `config.php` kennt. Die Übersicht zeigt Domain, Kunde, Fassung, letzte Meldung und Auftragsstand, zählt aktuelle, veraltete, verstummte (über 48 h) und nicht zugeordnete Installationen und bietet „Auf x.y.z“ je Zeile und „Alle aktualisieren“ mit Bestätigung (ohne verstummte, wartende und Git-Installationen). Die neueste Fassung kommt von derselben Releaseseite wie bei den Installationen, 30 Minuten zwischengespeichert. Fremde Nutzung erkennt man an „Unbekannt“ (kein Kunde zugeordnet) und am Hinweis „vorher …“ bei einer Kennung, die sich unter neuer Domain meldet.
**Grenzen.** Wer den Code verändert und die Meldung ausbaut, bleibt unsichtbar; ohne Lizenzschlüssel ist das kein Schutz, nur eine Übersicht. Installationen vor 1.36.0 erscheinen erst nach einem Update von Hand. Erreicht eine Website sich selbst nicht, steht die Wartungsseite bis zum nächsten Besuch oder Cron-Aufruf. Die Zentrale speichert die IP-Adresse der letzten Meldung; das gehört in die Datenschutzerklärung beziehungsweise Auftragsverarbeitung mit den Kunden.
**Folgen.** Keine Migration im CMS: `var/zentrale.json` entsteht beim ersten Lauf, die Aufgabe trägt sich über ihr Etikett selbst in `scheduled_task` ein. Das Schema der Zentrale migriert sich über `PRAGMA user_version` (`zentrale/app/Datenbank.php`). Protokollprüfung ohne Netz: `php scripts/zentrale-check.php`; der Ablauf mit Dateitausch und Abschluss steht in `tests/ArchiveUpdateTest.php`.
## ADR 122 – E-Mail-Versand als eigene Seite, Probe nicht mehr unter /.well-known (12.10.2026)
+12
View File
@@ -1,5 +1,17 @@
# Projektstatus
## Zentrale sites.kant-it.de: Meldung und Update aus der Ferne (14.10.2026)
**Anlass:** Umut: Alle KantSites sollen sich bei einer Hauptplattform melden; dort Domains und Fassungen sehen und Updates anstoßen. Entschieden: `sites.kant-it.de`, ohne Lizenzschlüssel, Updates einzeln und für alle; Umut lädt die Plattform selbst hoch.
**Erledigt:** CMS: Aufgabe „Bei der Zentrale melden“ (`zentrale`, 15 min) mit `App\Ops\Zentrale` (HMAC-signierte Meldung, Schlüssel beim ersten Kontakt, Antwort wird geprüft), Auftrag „Update“ über `ArchiveUpdate`, Abschluss durch die nächste Anfrage (`RemoteUpdateFinishSubscriber`), Funktionsprüfung als Dienst `Funktionspruefung`. `ZENTRALE_URL` (Standard `https://sites.kant-it.de`, `aus` schaltet ab), lokale Adressen melden nie. Plattform in `zentrale/` (PHP 8.1+, SQLite, ohne Composer): Einrichten mit Code aus `config.php`, Anmeldung mit Sperre nach Fehlversuchen, Übersicht mit Zählern, „Unbekannt“ für Installationen ohne Kunde, Domainwechsel sichtbar, Update je Zeile und „Alle aktualisieren“ mit Bestätigung, Detailseite mit Stand, Kunde/Notiz, Auftragsverlauf, Zurückziehen und Entfernen. Anleitung: `zentrale/README.md`. ADR 123.
**Geprüft:** PHPUnit `ArchiveUpdateTest` (16 Tests, darunter der ganze Fernablauf mit Dateitausch, Abschluss durch die Folgeanfrage und Ergebnis an die Zentrale; gefälschte Antwort; lokale Installationen melden nicht), `TaskScreenTest`, `SchedulerTest`, `SystemScreenTest` grün. `php scripts/zentrale-check.php`: 14/14 (CMS-Klasse gegen echte API: Anmeldung, Auftrag abholen, nicht doppelt, Ergebnis, 403 bei falscher Unterschrift und fremdem Schlüssel, Neuanmeldung, Domainwechsel). Plattform lokal im Browser (Port 8115, Demodaten): Einrichten, Anmelden, Übersicht mit echter neuester Fassung 1.35.1 von git.kant-it.de, Einzelauftrag, Zurückziehen, „Alle aktualisieren“, Detailseite, 375 px ohne Querscrollen, keine Konsolenfehler, CSP und Cookie-Flags. Nicht geprüft: echter Lauf über das Internet gegen sites.kant-it.de (gibt es noch nicht), Selbstaufruf auf einem echten Hosting, Lighthouse der Plattform (interne Seite, `noindex`). Volle PHPUnit-Kette: 685 Tests grün (ein Fund unterwegs: `zentrale` musste in die Ausschlussliste von `build-release.php`), php-cs-fixer ohne Befund.
**Bekannte Lücken:** Installationen vor 1.36.0 melden sich erst nach einem Update von Hand. Ohne Lizenzschlüssel bleibt unsichtbar, wer die Meldung ausbaut. Die IP-Adresse der letzten Meldung wird gespeichert – Hinweis in Datenschutz/AV-Vertrag nötig. Lokal fehlt PHP 8.4 ein Zertifikatsspeicher; die Launch-Konfiguration `zentrale` setzt dafür `curl.cainfo` auf das Bündel von Git.
**Nächster Schritt:** Release 1.36.0 bauen und veröffentlichen, `zentrale/` nach sites.kant-it.de hochladen (Docroot `public/`, HTTPS, `config.php`), erstes Konto anlegen, kant-it.de auf 1.36.0 heben und die erste Meldung sowie ein Fern-Update mit 1.36.1 prüfen.
## Redesign-Entwurf EBI Immobilien, Karlsruhe (13.10.2026)
**Anlass:** Umut: „einfacher Rebuild und Redesign von ebi-immobilien.de, soll vollendet aussehen, aber nicht übertrieben clean“.
+4
View File
@@ -97,6 +97,10 @@ Der Weg für den einen Fall, in dem die Karte im Bildschirm „System" noch nich
Das ist einmalig: Ab 1.4.3 kennt jede Installation den Weg über das Archiv, und dann entfällt das alles. Dann steht im selben Bildschirm die Karte „Update aus dem Releasearchiv", und die Schritte 2 bis 8 erledigt sie selbst.
## Der vierte Weg: aus der Zentrale
Ab 1.36.0 meldet sich jede Installation bei `sites.kant-it.de` und holt dort angestoßene Updates ab. Es läuft derselbe Archivweg wie über den Knopf: Sicherung, Archiv mit Prüfsumme, Dateitausch, Migrationen mit dem neuen Code, Prüfung der Startseite, sonst Zurückschalten. Den zweiten Schritt erledigt die nächste Anfrage an die Website; die Aufgabe ruft dafür die eigene Startseite einmal auf. Git-Arbeitskopien lehnen Aufträge ab. Einzelheiten: ADR 123, `zentrale/README.md`.
## Ein Release veröffentlichen
`php scripts/build-release.php` baut auf einem Rechner mit Composer das Archiv – Anwendung plus `vendor/` ohne Entwicklungspakete – und schreibt die SHA-256-Summe in eine Kopie von `install.php`. Beides gehört anschließend an das Git-Release des passenden Tags: