Changelog
Versionen nach SemVer. Die aktuelle Version steht in VERSION.
Unveröffentlicht
- MySQL 8:
groupsist dort ein reserviertes Wort. Tabelle in Schema, Migration, Code und Demo-SQL mit Backticks (MariaDB und SQLite unverÀndert). Auf MariaDB und SQLite getestet, auf MySQL 8 noch nicht. - Node ohne Docker:
bin/TLS-node-doktor.bash(Voraussetzungen, Dump-VorprĂŒfung, DB, Rechte, Apache, FPM-Pool, https-Gegenprobe), Pool-Vorlagedeploy/node/tlsgate-pool.conf.example, Abschnitt indeploy/README.md, Handoutdocs/HANDOUT_NODE_OHNE_DOCKER.md.
v3.16.0 â 2026-10-12
Seit v3.15.0 (8b05cba): NetBox-Connector Schritt 4 (GerĂ€te-Zertifikate), NetBox-Spiegel im Web und Verwaltung der NetBox-ZugĂ€nge im Web, Buchen und Beenden im Web (Katalog und Dashboard), Fehlermeldungen mit dem Text von NetBox; dazu die Befunde aus dem Live-Test gegen seeds.crumbforest.io (docs/HANDOUT_NETBOX_LIVETEST.md). VORLĂUFIG, nicht von der Crew bestĂ€tigt. Getaggt ist der Commit, der in der VM gelaufen ist (@<commit> im Seitenkopf).
NetBox-ZugĂ€nge im Web verwalten (public/netbox.php, Entscheidung N9 in docs/DESIGN_NETBOX_CONNECTOR.md, Spec docs/superpowers/specs/2026-10-12-netbox-zugaenge-ui-design.md). Branko, 12.10.: Beim Live-Test gegen seeds.crumbforest.io standen die Fehler nur in der CLI-Ausgabe, und drei TestzugĂ€nge mussten per SQL gelöscht werden: âwarum bauen wir das nicht so aus wie SOAP-ZugĂ€ngeâ. VORLĂUFIG, nicht von der Crew bestĂ€tigt.
- Im Web: Zugang anlegen (Token als Passwortfeld, nie zurĂŒck ins HTML), Token tauschen, an/aus, Verbindung testen, löschen, Kontobindung an/aus. Alle Aktionen stehen als
web.netbox_cred_<op>im Audit (ok, denied mit reason, fail). Die Lesesicht bleibt unverÀndert, der Sync bleibt auf der CLI. - Rechte:
group_adminverwaltet nur die eigene Gruppe und setzt nie die Kontobindung. ZugĂ€nge mit Kontobindung darf er nur testen und an/aus schalten. Die Kontobindung schaltet nuradmin. Ein Zugang einer fremden Gruppe gilt als âUnbekannter Zugangâ. Löschen und Kontobindung brauchen ein Pflicht-HĂ€kchen. - Verbindung testen (
NetBoxClient::probe): liest Version, GerÀteanzahl des Tenants und je eine Zeile Interfaces/IP-Adressen und schreibt nichts. Nur HTTP 403 gilt als fehlendes Recht. Andere Fehler (z. B. 502) zeigen den Text von NetBox. - CLI:
netbox_cli.php zugang delete <gruppe> <label>löscht nur, wenn weder gespiegelte GerÀte noch Ereignisse noch Zertifikate daran hÀngen, sonst muss man abschalten. Neu ist auchzugang konto-bindung <gruppe> <label> an|aus; die Meldung nennt die laufenden Buchungen mit Anker auf GerÀte dieses Zugangs (DeviceMirror::anchoredBookings). Der Tenant bleibt unverÀnderlich. - Fehlermeldung mit dem Text von NetBox (
NetBoxClient::get): z. B. âHTTP 400 fĂŒr /api/dcim/devices/: tenant: Select a valid choice ⊠(Tenant-Slug prĂŒfen)â oder âHTTP 403 âŠ: Invalid tokenâ. HTML-Fehlerseiten erscheinen ohne Tags, gekĂŒrzt auf 200 Zeichen; ein zurĂŒckgespiegelter Token wird durch***ersetzt. - Live-Befunde und Folgen fĂŒr die Ansible-Rolle (u. a. RZ ohne Tenants):
docs/HANDOUT_NETBOX_LIVETEST.md. - Tests:
test_netbox79,test_netbox_ui44, VMp828 (neu: Verbindung testen im Web gegen die Fake-NetBox, eigenes Testkontop8nb).
Buchen und Beenden im Web (docs/DESIGN_KATALOG_BUCHUNGEN.md). Branko, 12.10.: Das Dashboard zeigte Zuweisung, VerfĂŒgbarkeit und Preis, aber man konnte nichts buchen. VORLĂUFIG.
- Katalog (
katalog.php, nuradmin): Im Abschnitt âBuchen fĂŒr eine Personâ der eigenen Gruppe wĂ€hlt man Person, Produkt · Kategorie (nur freigegebene, verfĂŒgbare, aktive Paare) und âgehört zuâ, dazu Anker, Systemakte, Preis und SchĂ€tzung. Die Buchung landet ĂŒberBookings::addim Zustandoffered. Darunter stehen die laufenden Buchungen der Gruppe als Baum (CatalogAdmin::groupBookings, ohne Systemakte), jeweils mit âbeendenâ und Pflicht-HĂ€kchen. Ein Grundprodukt lĂ€sst sich erst beenden, wenn darunter keine ZusĂ€tze mehr laufen. - Dashboard: Unter âZusatz buchenâ bucht man eigene ZusĂ€tze unter eigenen laufenden Grundprodukten (
Tiles::buildâadd). Ohne Preis: Der kommt aus der Freigabe und wird nur angezeigt; Preis und Anker aus dem POST werden ignoriert.userundgroup_adminsehen keine Kategorien der Achseaccess,adminnur diese (mit Systemakte). Eigene ZusĂ€tze lassen sich mit Pflicht-HĂ€kchen beenden. Bookings::end:readonlybeendet nichts mehr (rolle). Mit$nurZusatz(Dashboard) lassen sich nur ZusĂ€tze beenden (nur_zusatz). Die CLI ist unverĂ€ndert.- âwartenâ statt âwartetnâ im Hinweis auf offene Buchungen.
- Tests:
test_katalog188,test_bookings80,test_dashboard_tiles29 (POST nuractivate,add,end, kein SQL in der Seite).
Test-VM: docker_vm/README.md startet jetzt mit docker_vm/build.sh statt docker compose ⊠up --build. Sonst bleibt app/BUILD leer, und run.sh und p8.sh werden rot.
Schritt 4 des NetBox-Connectors: GerĂ€te-Zertifikate der Node-CA (docs/DESIGN_NETBOX_CONNECTOR.md §6). VORLĂUFIG, nicht von der Crew bestĂ€tigt. Branko, 11.10.: ânein, kein mac/wg ins zertifikat â schritt 4 startenâ.
node_ca.php device-issue|device-list|device-cert|device-revoke,Netbox\DeviceCert,NodeCa::signDevice. Ausgestellt wird nur fĂŒr ein GerĂ€t im Spiegel der eigenen Gruppe, das nichtwegist, nur bei Kontobindung (Regel b) und nur, solange es kein gĂŒltiges Zertifikat hat (kein stilles Umbinden).- Im Zertifikat steht nur der GerĂ€tename (CN + SAN DNS), nur
clientAuth, 90 Tage. Keine MAC, kein wg-Key, keine NetBox-ID. Das CSR-Subject muss genauCN=<gerĂ€tename>sein, die CSR-Signatur wird geprĂŒft. device_certs,device_cert_revocations(append-only, Migration0007). Widerrufene GerĂ€te stehen in derselben CRL wie widerrufene Bestellungen.OrderStore::parseCsrist jetzt öffentlich. Der Doktor kennt 24 Tabellen.netbox.php(Wurzel): Spalte âZertifikatâ je GerĂ€t.- Nicht im Frontend
native_php: Die Node-CA ist dort bewusst nicht enthalten. Gespiegelt werden nur die Tabellen. Die ozm-Knoten können deshalb noch nicht ausstellen, der Ort ist offen. - Tests:
test_device_cert42 (neu),test_netbox_ui18.
NetBox-Spiegel im Web, nur lesen (public/netbox.php, Link âNetBoxâ in der Navigation fĂŒr group_admin/admin; docs/DESIGN_NETBOX_CONNECTOR.md §5). Branko, 11.10.: Er sah als admin@ozm03 keinen NetBox-Link, weil Schritt 1â3 nur auf der CLI liefen. Die Seite zeigt ZugĂ€nge (ohne Token), den Spiegel je Zugang (MAC, VPN-IP, wg-Key gekĂŒrzt, Link aufs GerĂ€t in NetBox), die letzten Ereignisse und den Anker-Bericht der Buchungen. Sie hat keine Aktion: Anlegen (Token ĂŒber STDIN) und sync bleiben auf der Node, weil der Abruf nach auĂen geht. Jeder Aufruf steht als web.netbox_view im Audit. Test: test_netbox_ui 17.
v3.15.0 â 2026-10-11
Seit v3.14.0 (e494a87): NetBox-Connector je Gruppe (Schritt 1â3) und die RĂŒckmeldung von OZMAI zu v3.13.0. VORLĂUFIG, nicht von der Crew bestĂ€tigt. Getaggt ist der Commit, der in der VM gelaufen ist (@<commit> im Seitenkopf).
SSL-<n>-SchlĂŒssel nur fĂŒr die bestĂ€tigte Form (OZMAI, 11.10.: âDass auch ssl-04 auf 4 normalisiert wird, ist eure Implementierungsregel. Sie sollte nicht still als Provider-Garantie gelesen werden.â)
CatalogLoader::refKey: nurSSL-<n>und<n>â<n>, wie Branko es bestĂ€tigt hat.ssl-4(Kleinschreibung) undSSL-04(fĂŒhrende Null) bleiben jetzt wie geliefert, vorher wurden sie auf4normalisiert. Der gelieferte Wert wird weiter nie ĂŒberschrieben.- Grund einer Abweichung (fĂŒr den Dry-Run in der VPN-Umgebung): Die Meldung
ABWEICHUNGim Abgleich undinventory_cli.php status(unbekannt_grund) unterscheiden jetztkein_katalogeintrag(âSchlĂŒssel 4: kein Katalogeintrag, Produktliste importiert?â) vonform_nicht_normalisiert(Form ist nichtSSL-<n>/<n>).CatalogLoader::refMissReason. - Tests:
test_stammdaten35,test_inventory72.
NetBox-Connector je Gruppe, Schritt 1â3 (docs/DESIGN_NETBOX_CONNECTOR.md). Branko, 11.10.: Das Gate ist die Ansible-Node, NetBox und Dienste laufen schon, SOAP braucht es nur fĂŒr gekaufte Zertifikate. Es fehlt der Connector an der BrĂŒcke, um Produkte, Dienste und ZugĂ€nge innerhalb der Gruppe an GerĂ€te zu binden. Danach kommt als erste Passkante die eigene Crumbforest-CA. VORLĂUFIG, nicht von der Crew bestĂ€tigt. Nur lesen, NetBox bleibt fĂŒhrend.
- Zugang (
netbox_credentials,NetBoxStore, Zwecknetboxim WurzelschlĂŒssel): URL (https, ohne Query) + Token (STDIN, versiegelt, an Gruppe und Label gebunden) + Tenant-Slug +konto_bindung.netbox_cli.php zugang add|replace|enable|disable|list.user_cli.php key status|migratekennen die NetBox-Blobs.TLSGATE_NETBOX_HOSTSschrĂ€nkt die Hosts ein. - Spiegel (
netbox_devices,netbox_device_eventsappend-only;DeviceMirror,NetBoxClient):netbox_cli.php fetch|load|sync|status|events|geraete. Gelesen werdendcim/devices?tenant=,dcim/interfaces(MAC ausprimary_mac_address, 4.2+, sonst das alte Feld),ipam/ip-addresses(VPN-IP im VRFCrumbVPNoder anwg0) und der wg-Key aus dem Kommentar (wg-pub <key>, Konvention vonnetbox-mobile-clients.yml). Ausgegeben werden nur diese Felder. Wie beim Registrar-Spiegel ist der erste Lauf die Baseline, danach werden nur Ănderungen protokolliert. - Fail closed: Die Zeilenzahl muss dem
countvon NetBox entsprechen. Jede Zeile muss den eigenen Tenant tragen, eine fremde Zeile lehnt alles ab. Doppelte oder ungĂŒltige Zeilen (auch eine ungĂŒltige MAC) brechen ab. Eine leere oder halbierte Liste geht nur mit--forcedurch. Der Token geht nur an den hinterlegten Host: kein Redirect, und einnext-Link auf einen fremden Ursprung wird abgelehnt. TLS wird geprĂŒft. Jeder Lauf steht alscli.netbox_sync/cli.netbox_fetchmitctx.buildund Tenant im Audit. - Anker (
netbox_cli.php anker [--json]): Buchungen mitnetbox:<id>(empfohlen, GerĂ€te-Slot),mac:<MAC>(hart an Hardware) oderwg:<key>werden gegen den Spiegel geprĂŒft:ok,unbekannt(Abweichung, nichts wird angelegt),weg,mehrdeutig. Dazu HARDWARE GEĂNDERT, wenn sich die MAC des GerĂ€ts seit der Buchung geĂ€ndert hat. Die MAC ist eine beobachtete Tatsache, kein SchlĂŒssel. Es gibt kein stilles Umbinden; der Reset (widerrufen + neu ausstellen) ist eine bewusste Handlung und kommt in Schritt 4. Freitext-Anker bleiben ungeprĂŒft. - Regel (b) (Branko 11.10.): ZugĂ€nge ohne
konto_bindung(Standard, KrĂŒmel-Tenants) werden nur gespiegelt.Bookings::addlehnt einen Anker auf ein solches GerĂ€t ab (kein_kontobezug), eine ungĂŒltige Ankerform ebenfalls (anker_ungueltig). Der Bericht meldet Ă€ltere Buchungen mitREGEL B. - Schema: drei Tabellen, Migration
0006(MariaDB). Der Doktor kennt 22 Tabellen. - Fake-NetBox in der VM (
docker_vm/fake-netbox,https://netbox.fake.test, Fake-CA). Szenarien wÀhlt der Token: Grundstand, Zero verloren, fremder Tenant, fremdernext-Host. - Tests:
tests/test_netbox.sh(56; Gegenproben ohne Tenant-PrĂŒfung, ohne Regel b, ohne UrsprungsprĂŒfung, ohnecountje rot).docker_vm/tests/p8.sh(neu) gegen MariaDB und Fake-NetBox,p5erwartet 22 Tabellen. Befund beim Testen: Buchung und Hardware-Wechsel in derselben Sekunde wurden nicht gemeldet (ts > created_at), jetzt>=, also im Zweifel melden. - Nicht gebaut: Schritt 4 (Node-CA stellt fĂŒr ein gespiegeltes GerĂ€t aus, Reset bei Hardware-Wechsel), Web-Ansicht, VMs (
virtualization/âŠ), Tags â FĂ€higkeiten. Nicht belegt: der Lauf gegen die echte NetBox (seeds.crumbforest.io).
v3.14.0 â 2026-10-11
Seit v3.13.0 (00bad35): Monitor fĂŒr offene Buchungen, nur lesend. VORLĂUFIG, nicht von der Crew bestĂ€tigt. Getaggt ist der Commit, der in der VM gelaufen ist (@<commit> im Seitenkopf).
Monitor fĂŒr offene Buchungen (Zustand offered: freigeschaltet, vom Inhaber noch nicht aktiviert). Branko, 10.10.: Der bewusst offene Prozess gehört in den Monitor. Nur lesen.
Bookings::open([gruppe]): Àlteste zuerst, je Buchung Gruppe, Konto alsuser_id+ Rolle, Produkt, Kategorie, Grundprodukt (parent),since,days,waiting_on: inhaber, dazufreigabe_aktiv/produkt_aktiv. Ist eins davonfalse, ist es eine Sackgasse: Der Inhaber sieht die Buchung, die Quelle ist aber abgeschaltet. Ohne E-Mail, Systemakte, Anker, Preis und SchÀtzung.- Export:
bin/export.phpschreibt das Feldopen_bookingsin den Monitor-Vertrag (Zusatzfeld, Version bleibt 1). Meldung und Auditcli.exportzÀhlen die offenen Buchungen. - Monitor-Seite
resources/mock/sample.html?data=âŠ: Tabelle âOffene Buchungen (offered)â, nurtextContent. Farben: unter 1 Tag tĂŒrkis, ab 1 Tag gelb, ab 7 Tagen rot. Die Schwellen sind eine Annahme des Entwicklers. Ein alter Export ohne das Feld wird als solcher benannt, die Seite behauptet dann nicht âkeineâ. - Bash:
booking_cli.php open [<gruppe>] [--older-than <tage>] [--json]. Die Textausgabe zeigt die E-Mail (Operator, nur root/sudo),--jsonist dieselbe Form wie im Export, ohne E-Mail, zum Weiterreichen an andere Werkzeuge. Auch in der VM ĂŒberinstance-cli.sh <name> booking_cli.php open. tests/test_open_bookings.sh(26), drei absichtlich kaputte Fassungen je rot (alle ZustĂ€nde, E-Mail immer, Sackgasse nie).p7+3 gegen MariaDB.- Nicht gebaut: Alarm oder Benachrichtigung, ein Cron, der den Export schreibt. Im Dashboard sieht der Inhaber seine offenen Buchungen schon (Hinweis ĂŒber âMeine Buchungenâ). In der VM gibt es
export.phpnicht (Spiegelnative_phpohneMonitorExport).
v3.13.0 â 2026-10-11
Seit v3.12.0 (5e064de): Seite Katalog mit Löschen und Audit, âaktivierenâ im Dashboard, Instanzname auf der Anmeldeseite, Storno-Grund und Ablehnungen im Audit, Code-Stand im Audit, SSL-<n> = ID <n>. VORLĂUFIG, nicht von der Crew bestĂ€tigt. Getaggt ist genau der Commit, der in der VM gelaufen ist (@<commit> im Seitenkopf).
Achtung, geĂ€ndertes Verhalten: order_cli.php order cancel <id> <grund> verlangt jetzt einen Grund. Ein Aufruf ohne Grund bricht mit Nutzungshinweis ab und Ă€ndert nichts (vorher Standard âabgebrochenâ).
Code-Stand im Audit (Commit-Stempel). Anlass: OZMAI 10.10. âVERSION_LABEL â EXACT_IMPLEMENTATION_STATEâ â im PrĂŒfbeleg eines SOAP-Laufs soll stehen, welcher Commit lief, nicht nur die Version.
Config::build()/buildStamp(): Quelle 1 ist die DateiBUILDnebenVERSION(commit=<sha>,dirty=0|1), geschrieben beim Image-Bau. Quelle 2 ist eine Arbeitskopie mit.git(HEAD direkt gelesen, ohnegitaufzurufen; Worktree undpacked-refseingeschlossen). Kurzform:<12 Hex>(Bau aus dem Commit),<12 Hex>-dirty(Bau mit Abweichung oder ohne Angabe),<12 Hex>-git(Arbeitskopie, Ănderungen ungeprĂŒft), sonstunbekannt.- Audit: Aktionen, die den Registrar berĂŒhren oder seine Antwort verarbeiten, tragen
ctx.build:ssl.*,cli.inventory_sync,cli.order_tick,cli.cred_test,web.cred_test,cli.catalog_import_products. Auch LĂ€ufe ohne Ănderung,dry_run,rejectedunddenied. Andere Aktionen bleiben unverĂ€ndert. - Bau:
docker_vm/build.sh(stattdocker compose ⊠up -d --build) gibt Commit unddirtyals Build-Args ins Image (.gitliegt nicht im Build-Kontext);instance.shmeldet den Code-Stand. Ohnebuild.shsteht âunbekanntâ im Image, nichts Erfundenes.BUILDist nicht versioniert. - Seitenkopf:
v<Version> @<Stand>nach der Anmeldung. tests/test_build_stamp.sh(25), ohne Stempel 5 rot;docker_vm/tests/run.sh+4 (BUILD = Host-Commit,dirty=0, Auditssl.exportProductsmitctx.build, Seitenkopf).- Nicht verdrahtet: Ansible-Rolle/Betrieb (dort gilt: kein
BUILDâ Arbeitskopie oder âunbekanntâ). Image-Digest nicht erfasst (OZMAI: KĂŒr).
SSL-<n> im Bestand = ID <n> der Produktliste, bestĂ€tigt (Branko, 11.10.: Die Order ĂŒbergibt als Option nur die Ziffer, die SOAP-Details geben sie mit PrĂ€fix aus). Anlass: Der Live-Lauf vom 9.10. meldete SSL-4 und SSL-80 als unbekannt.
CatalogLoader::refKey():SSL-4,ssl-04,4â4; andere Formen bleiben wie geliefert. Gespeichert wird weiter der gelieferte Wert (BestandSSL-4, Import4).- Verglichen wird ĂŒber den SchlĂŒssel: unbekannte Produkte beim Abgleich (
Inventory::unknownProducts), Produktname aufbestand.php, Bestand â Katalog und Zeilenzahl aufstammdaten.php, Löschsperreim_bestandim Katalog (genau4undSSL-4, statt vorherLIKE '%-4'). stammdaten.php: âEintrag der Registrar-Produktlisteâ ohne Fragezeichen.- Tests:
test_inventory(Katalog4trifft BestandSSL-4),test_stammdaten34 (+2),p7ohne Fragezeichen. Gegenproben ohne SchlĂŒssel je rot.
Seite âKatalogâ (katalog.php), nur Rolle admin: Produkte und Kategorien pflegen, Zuordnungen setzen und lösen, Freigabe fĂŒr die eigene Gruppe. VORLĂUFIG, nicht von der Crew bestĂ€tigt.
- Kategorien: anlegen (Slug, Name, Achse, Preisart, Oberkategorie), bearbeiten, ab- und einschalten. Produkte: anlegen mit mindestens einer aktiven Kategorie, Name und Art bearbeiten, ab- und einschalten. Zuordnung Produkt â Kategorie setzen und lösen. Freigabe fĂŒr die Gruppe des Admins setzen (mit optionaler Preisreferenz, ĂŒber
Bookings::offer) und zurĂŒcknehmen. Erst danach erscheint ein Produkt auf dem Dashboard der Gruppe und ist buchbar. - Löschen ist endgĂŒltig (Branko, 9.10.: keine inaktiven DatensĂ€tze vorhalten). Der gelöschte Datensatz steht im Audit (
catalog.product_deletemit Kategorien und Freigaben,catalog.category_delete). Abgelehnt, solange etwas darauf zeigt: Buchungen in jedem Zustand, auch beendete, denn sie bleiben die Quelle der Berechnung (in_buchungen), Verbrauch (hat_verbrauch), Registrar-Bestand mit der Registrar-ID, gezĂ€hlt alsSSL-4und4(im_bestand), zugeordnete Produkte (hat_produkte), Unterkategorien (hat_unterkategorien). FĂŒr diesen Fall bleibt abschalten: Neu bucht niemand mehr, die Quelle bleibt. Freigabe zurĂŒcknehmen: ohne jede Buchung wird die Zeile gelöscht, mit nur beendeten Buchungen bleibt sie abgeschaltet stehen. - Slugs sind nach dem Anlegen fest (von Branko bestĂ€tigt).
- Regeln (
src/Catalog/CatalogAdmin.php, Code im Auditcatalog.*alsreason): nur admin (nur_admin); Achseaccessnur mit Preisartnone(access_ohne_preis); Achse und Preisart einer Kategorie bleiben, solange laufende Buchungen sie tragen (kategorie_in_gebrauch), die Art eines Produkts ebenso (produkt_in_gebrauch); kein Kreis bei Oberkategorien (parent_zyklus); die letzte Kategorie eines Produkts bleibt (letzte_kategorie), eine von einer laufenden Buchung genutzte Zuordnung auch (zuordnung_in_gebrauch); eine Freigabe mit laufenden Buchungen wird nicht zurĂŒckgenommen (freigabe_in_gebrauch); Name und Art von Registrar-Produkten kommen nur aus dem Import (registrar_produkt), zuordnen und freigeben geht. - Kein neues Schema, keine Migration. Kopfzeile: Link âKatalogâ fĂŒr admin.
stammdaten.phpbleibt reine Lesesicht und verweist auf den Katalog. - Bekannt:
catalog_cli.php loadsetzt Name, Art, Achse und Preisart der EintrĂ€ge auscatalog_matrix.jsonauf den Stand der Datei zurĂŒck. Der Matrix-Abgleich findet Produkte ĂŒber den Namen, ein Umbenennen macht die Zeile also offen (â). tests/test_katalog.sh(97): jede Regel mit Code, Löschen samt Ablehnungen, die Seite ĂŒber HTTP (403 fĂŒr group_admin, CSRF, Formulare, Maskierung, kein Inline-JS), Durchstich bis Dashboard und Stammdaten. Sechs absichtlich kaputte Fassungen sind je rot. Regeln und Darstellung auch gegen MariaDB (tlsgate-my) geprĂŒft. VM (Image neu gebaut, 9.10.):run86,p348,p454,p531,p622,p750 (+6: Katalog-Klickpfad gegen MariaDB mit DML-Benutzer â anlegen, zuordnen, freigeben, Löschen abgelehnt und endgĂŒltig, Audit, leeres PHP-Fehlerlog). Klicktest-Instanz ozm03 frisch aufgebaut:mockup.sql+demo_matrix.sh(24 Produkte, 12 Kategorien, 18 aktive Buchungen, 12 Zeilen Beispielbestand an abgeschaltetem Zugang).
Instanzname auf der Anmeldeseite (Dev): login.php zeigt âInstanz: TLSGATE_INSTANCE_LABEL gesetzt ist (Config::instanceLabel, nur A-Za-z0-9._-, höchstens 32 Zeichen, sonst nichts). Ohne die Variable bleibt die Seite wie bisher: Im Betrieb verrĂ€t die Anmeldeseite keinen Namen. In der VM setzt instance.sh die Variable aus LOGIN_INSTANCE=1|0 in /root/.provision/<name>.env, Standard 1, eine fehlende Zeile wird ergĂ€nzt. Anlass: Ein alter Chrome-Tab traf ozm01 statt ozm03, die Fehlermeldung sagte nicht, welche Instanz antwortete. tests/test_login_instance.sh (5), VM run.sh 87 (+1), An/Aus in der VM von Hand geprĂŒft.
- Löschen-Knopf im Katalog rot hinterlegt (Kontrast 6,5:1).
- CSS und JS mit Versionsstempel (
View::asset:assets/tlsgate.css?v=<Ănderungszeit>). Anlass: Der rote Knopf blieb im Browser unsichtbar, obwohl der Server die neue CSS lieferte. nginx schickt keinCache-Control, der Browser nahm die alte Fassung aus dem Cache. Nach jeder Ănderung gibt es jetzt eine neue Adresse. - Dashboard: Hinweis oben in âMeine Buchungenâ (ân Buchung(en) warten auf deine Aktivierungâ), Knopf âaktivierenâ gefĂŒllt in der Akzentfarbe. Anlass: Branko fand den Knopf nicht, obwohl er ausgeliefert wurde (serverseitig gerendert und ĂŒber die SeitengröĂe geprĂŒft). Er stand klein und durchsichtig am Ende eines Baums mit neun aktiven EintrĂ€gen.
- Dashboard: âaktivierenâ fĂŒr den Inhaber. Eine freigeschaltete Buchung (
offered) trĂ€gt im eigenen Baum den Knopf âaktivierenâ (POST + CSRF,Bookings::activate, Auditbooking.activate). Nur der Inhaber (nur_inhaber) und nur ausoffered(zustand), Ablehnungen stehen im Audit. Sonst bleibt das Dashboard lesend. Anlass: Branko meldete die User als âaktiviertâ, das Audit zeigte aber nur Anmeldung und TOTP-Einrichtung. Einen Knopf zum Aktivieren gab es nicht, obwohl der Entwickler ihn angekĂŒndigt hatte.tests/test_katalog.sh111 (+8: Knopf, CSRF, Admin darf fremde Buchung nicht, Inhaber aktiviert, zweimal abgelehnt, Audit),test_dashboard_tilesprĂŒft âPOST nur fĂŒr aktivierenâ. - Katalog-Ablehnungen im Audit lesbar: Ein
denied-Eintrag trÀgt jetzt, was versucht wurde (nur die Felder, die sich Àndern sollten, als{"price_mode": {"von": "priced", "nach": "free"}}), und was im Weg stand (buchungen,produkte,unterkategorien,verbrauch,bestand). Die Meldung auf der Seite nennt die Anzahl. Anlass: Brankos Eintragcatalog.category_edit denied container {"reason":"kategorie_in_gebrauch"}sagte nicht, was er Àndern wollte.tests/test_katalog.sh103. - Storno-Grund im Audit:
order_cli.php order cancel <id> <grund>, der Grund ist jetzt Pflicht (vorher optional, Standard âabgebrochenâ).cli.order_canceltrĂ€gtgrund,von(vorheriger Zustand) undregistrar_ref, vorher stand der Grund nur inorder_events. Abfragbar aufauditlog.phpĂŒber die Suche (Aktion und Kontext), z. B. âorder_cancelâ oder âfalscher CSRâ.tests/test_orders.sh119. Ein Storno beim Registrar gibt es weiterhin nicht im Code.
v3.12.0 â 2026-10-08
inventory_cli.php load|status|eventsnehmen auch einen abgeschalteten SOAP-Zugang (dabei lĂ€uft kein SOAP-Aufruf; Hinweis auf stderr). OhneTLSGATE_CRED: Labeldefault, sonst der einzige Zugang der Gruppe.soap_cli.php syncund âAktualisierenâ im Web brauchen weiter einen aktiven Zugang.database/demo/bestand_beispiel.json: 12 synthetische Bestandszeilen (example.test,source mock) im Format vonsoap_cli.php list. FĂ€lle: drei Orders auf derselben Domain, eine Order mit zwei Domains, Ablauf in 5 und 15 Tagen, WildcardSSL-80,SSL-6ohne Katalogeintrag. Laden nur fĂŒr Demo:TLSGATE_GROUP=<gruppe> php bin/inventory_cli.php load database/demo/bestand_beispiel.json --allow-mock.tests/test_inventory.sh(70).- Seite âStammdatenâ (
stammdaten.php), ab Rollegroup_admin, nur lesen: PrĂŒfungen (Produkte ohne Kategorie, SSL ohne Registrar-ID, Bestand ohne Katalog, Personen ohne Buchung), Bestand â Katalog mit Zeilenzahl und Kandidat aus der Registrar-Produktliste (mit Fragezeichen: âSSL-4 = ID 4â ist vermutet), Produkte mit Kategorien und âeingerechnetâ je Rolle (Admin: Kategorie der Achseaccessohne Preis; User: Kategorie mit Preis), Freigabe und Buchungen in der eigenen Gruppe, Kategorien, Buchungen je Person als Baum mit Abrechnung je Eintrag (ohne Systemakte, nur âhinterlegtâ). Kein POST. Logiksrc/Catalog/MasterData.php. database/produktliste_registrar.json: die 131 EintrĂ€ge der Produkt-Auswahlliste der Registrar-Konsole (ID, Name, Laufzeit; Quelle Branko 8.10.). Referenz fĂŒr Kandidaten, legt nichts an.database/demo/stammdaten_beispiel.sql: Vorlage fĂŒr den manuellen Import, legt PositiveSSL 1 Jahr (SSL-4) und PositiveSSL Wildcard 1 Jahr (SSL-80) mit den Kategorienhostingundadminan. SQLite und MariaDB, mehrfach ladbar.- Matrix-Abgleich auf
stammdaten.php: jede Zeile von Brankos Produktmatrix (database/matrix_branko.txt, wörtlich, 17 Zeilen) mit â/â â Produkt im Katalog, Kategorie zugeordnet, aktive Buchung in der Gruppe (von wem), Abrechnung (eingerechnet/mit Preis).MasterData::matrixCheck. bin/demo_matrix.sh <gruppe> [--vm <instanz>]: Demo-Mockup, das die Matrix vollstĂ€ndig bucht: Katalog, Beispiel-Import, Freigaben aller Produkte, zwei Demo-User, 18 Buchungen ĂŒberbooking_cli.php(also durch alle Buchungsregeln), als Inhaber aktiviert, Beispielbestand an einem abgeschalteten Zugangdemo. Admin: #admin (eingerechnet, mit Systemakte); User 01: VM Small #proxmox mit allen ZusĂ€tzen und Server #baremetal; User 02: Hosting #managed mit Managed #hosting, Cluster #HA, RPI zero #container. Passwörter zufĂ€llig, in der VM unter/root/.provision/<instanz>.demo.tests/test_matrix_mockup.sh(20): 17 von 17 bestĂ€tigt, #admin genau die vier Admin-Zeilen, Gegenproben (andere Gruppe 0, falsche Zeilen offen, beendete Buchung zĂ€hlt nicht).- Tests:
tests/test_stammdaten.sh(32, zwei absichtlich kaputte Fassungen je rot),docker_vm/tests/p7.sh(44: Seite und Import gegen MariaDB).
v3.11.1 â 2026-10-08
Klick-Dummy: Seite âBestandâ (bestand.php) zeigt den Registrar-Spiegel der eigenen Gruppe aus der DB. VORLĂUFIG.
- Lesen ab
readonly, nur Gruppen mit SSL-Zugang (eigen). Sortierbar (Order, Domain, Status, gĂŒltig bis, gesehen seit), Filter nach Status, Suche in Order/Domain (%/_sind keine Platzhalter), âauch verschwundeneâ. Nach der Baseline aufgetauchte Orders sind als NEU markiert (es gibt einnew-Ereignis), je Zeile der Bash-Befehlsoap_cli.php info <order>zum Kopieren. Darunter die letzten 15 Ănderungen. Produktname aus dem Katalog, sonst â(nicht im Katalog)â. - âAktualisierenâ ab
user: POST + CSRF, holtsslListExtendedlive und gleicht mit denselben Schutzregeln wie die CLI ab (--forcegibt es nur in der CLI). Höchstens alle 5 Minuten je Zugang (sonst gilt der Spiegel), Auditssl.inventory_sync(ok/denied/rejected/failed). Nie Beispieldaten. - Kopfzeile: Link âBestandâ fĂŒr Gruppen
eigen. - VM: Spiegel
native_php/nachgezogen (vorher fehltenInventory,inventory_cli.php, Schema 0005:synclief in der VM nicht), Doktor kennt 19 Tabellen,instance-cli.shreichtTLSGATE_GROUP/TLSGATE_CREDdurch. Neue Suitedocker_vm/tests/p7.sh(37):synclive gegen den Fake-Registrar und MariaDB,load, Trigger, Schutzregeln, Klickpfad mit Anmeldung, Pause, Aktualisieren, CSRF. - Tests:
tests/test_inventory.sh(63) mit Modell und gerendertem HTML (Sortierung, Filter, Gruppentrennung, Maskierung, kein<script>). - Nicht geprĂŒft: echte SOAP-Antwort mit ĂŒber 600 Zeilen (Laufzeit eines Web-Abgleichs), Rolle
user/readonlyim Klickpfad der VM (nur das Modell), Browser-Optik.
v3.11.0 â 2026-10-08
Feature: Bestand beim Registrar in die DB spiegeln, danach nur noch Ănderungen erkennen. VORLĂUFIG, nicht von der Crew bestĂ€tigt. Nur Lesespiegel: das Bestandssystem bleibt fĂŒhrend, TLS-Gate bestellt hier nichts.
- Schema:
registrar_inventory(eine Zeile je Zugang, Order und Domain, denn eine Order kann mehrere Domains tragen) undregistrar_inventory_events(append-only:baseline,new,changedmit{feld:[alt,neu]},gone,back). Migration0005nur MariaDB (vermerkt die Version, die Tabellen kommen ausschema.mysql.sql); SQLite gleicht selbst an. Doktor/p5.sherwarten 19 Tabellen. - Abgleich:
php bin/soap_cli.php sync [--dry-run] [--force](live, brauchtTLSGATE_GROUP) oderphp bin/inventory_cli.php load <datei|-> âŠfĂŒr die gespeicherte Ausgabe vonsoap_cli.php list; dazustatusundevents [N]. Der erste Lauf je Zugang ist die Baseline (ein Ereignis mit Anzahl, keine 600 Einzelereignisse), danach steht nur noch, was sich Ă€ndert. Alles in einer Transaktion,--dry-runschreibt nichts, jeder Lauf im Audit (cli.inventory_sync). - Schutzregeln (fail closed): nur
source soap(mocknur mit--allow-mock,unavailablenie) · Liste ab 2000 Zeilen = vermutlich abgeschnitten, auch mit--forceabgelehnt · leere Liste oder mehr als die HĂ€lfte weg (ab 10 vorhandenen Zeilen) nur mit--force· eine ungĂŒltige oder doppelte Zeile bricht alles ab. - Abweichung statt Automatik: Ein
productId(z. B.SSL-4) ohne passendesproducts.registrar_refwird gemeldet (ABWEICHUNG: âŠ), nie angelegt. Sonderprodukte, Stammdaten und VerknĂŒpfungen pflegt der Mensch per SQL-Import. Zeit0(Zertifizierung lĂ€uft) wirdNULL. - Datenschutz: Der Bestand liegt nur in der DB.
tests/test_inventory.sh(44) arbeitet ausschlieĂlich mit synthetischen Daten (example.test); vier Schutzregeln und der SchlĂŒssel (Order+Domain) sind mutationsgetestet (je rot). Gegen MariaDB von Hand geprĂŒft (Migration 1â5, Baseline, Ănderung, Append-only-Trigger). - Nicht gebaut: Benachrichtigung der Crew bei Abweichung (âresonierenâ), VerknĂŒpfung der Bestandszeilen mit Stammdaten/Gruppen/Usern (Lese- und Schreibsicht), Dashboard-Anzeige, Paging ĂŒber 2000 Zeilen, regelmĂ€Ăiger Lauf (Cron). Offen: ob die echte Liste weitere Status/Felder kennt; die Statuswerte
zertifizierung_laeuftundssl_okstammen aus einem eingefĂŒgten Beispiel.
v3.10.1 â 2026-10-07
Fix: Admin-Dienste (Postgres, VPN, Gitea) standen als Angebot auch im Dashboard normaler User. Aufgefallen beim Durchsehen der Seiten von ozm03: user01 sah âPostgresâ unter âProdukte und Freigabenâ, obwohl er es nicht buchen kann (nur_admin). Tiles::products blendet jetzt Produkte aus, deren Kategorien alle der Achse access angehören, auĂer fĂŒr Konten mit Rolle admin. Produkte ohne Kategorie (Altbestand, Demo) bleiben fĂŒr alle sichtbar.
tests/test_bookings.sh(78): Admin sieht Postgres, User nicht, Storage und SSL sieht der User; ohne den Filter rot.
v3.10.0 â 2026-10-07
Feature: Export-Button fĂŒr die SOAP-Produktliste und Import in den Katalog. VORLĂUFIG, nicht von der Crew bestĂ€tigt.
- Export: Button âProduktliste exportieren (JSON)â im Reiter âSSL List & Statusâ (ab Rolle
user),public/products_export.php(nur POST, CSRF, Auditssl.exportProducts), dazusoap_cli.php products.SslHandler::exportProducts()liefert die Zeilen vonsslGetProductListroh (alle Felder, keine Zugangsdaten, keine Bestellungen).source:soaplive ·mocknur mitTLSGATE_MOCK=1·unavailableohne Ersatzliste (die Seite antwortet 502 statt einer Beispielliste). Dateiname trÀgt die Quelle. - Import:
php bin/catalog_cli.php import-products <datei|-> [--dry-run] [--allow-mock]. Nursource soap;mocknur mit--allow-mock(Demo),unavailablenie. Abgleich ĂŒberregistrar_ref(nie ĂŒber den Namen); Slugssl-<id>; ein vorhandener Slug mit anderemregistrar_refbricht ab; alles in einer Transaktion. Ein vorhandenes Produkt behĂ€ltactive; ein neues ist aktiv nur beistatus activated. Neue SSL-Produkte bekommen die Kategoriehosting(nichtadmin). - Schema:
products.registrar_refundproducts.registrar_meta(Rohzeile als JSON); Migration0004nur MariaDB. - âGroĂes Zertifikatâ (Branko: Wildcard, mehrere Domains oder DigiCert) wird aus den API-Feldern gelesen (
wildcard,type,maxDomain,CA), nicht als eigenes Flag:CatalogLoader::isLargeCert(), incatalog_cli.php listals Spalte GROSS. âMehrere Domainsâ ab 2 ist Annahme des Entwicklers. - Tests:
tests/test_products_import.sh(33; Fixturetests/fixtures/soap_products_doc.jsonist aus der Doku nachgebaut, nicht live), vier absichtlich kaputte Fassungen je rot.docker_vm/tests/run.sh+12 gegen MariaDB (Button, Export-Header, Import, Idempotenz, 405/403/302, Audit). - Nicht belegt: die echte Antwort von
sslGetProductList(Feldnamen laut Doku, nicht live gesehen); obSSL-4aussslListExtendeddemidder Produktliste entspricht.
v3.9.1 â 2026-10-07
Fix: Passwort zurĂŒcksetzen in users.php scheiterte unter MariaDB mit SQLSTATE 1370 ⊠routine 'unixepoch'. pw_changed_at = unixepoch() ist eine SQLite-Funktion; jetzt kommt die Zeit aus PHP (time()), wie schon in user_cli.php. Der Fehler bestand seit dem Web-Reset und fiel unter SQLite nicht auf.
tests/test_sql_portable.sherkenntunixepoch(),julianday()undsqlite_*im Anwendungscode (mit dem altenusers.phprot, 3/1).docker_vm/tests/run.sh(74) prĂŒft den Reset gegen MariaDB: Meldung âgesetztâ ohne SQLSTATE,pw_changed_atgesetzt, Auditadmin.user_passwd. Gegen den alten Stand in der VM nicht gelaufen.
v3.9.0 â 2026-10-07
Feature: Katalog nach Matrix, Kategorien, Buchungen je Person (docs/DESIGN_KATALOG_BUCHUNGEN.md). VORLĂUFIG, nicht von der Crew bestĂ€tigt.
- Schema:
categories,product_categories,user_products;products.kindumcontainer,addon,service,tokenerweitert (VARCHAR(16)). Migration0003nur fĂŒr MariaDB (liest den Namen der CHECK-Regel, idempotent); SQLite gleicht selbst an. Doktor undp5.sherwarten 17 Tabellen. - Matrix als Daten:
database/catalog_matrix.json(12 Kategorien, 16 Produkte, 21 Zuordnungen),bin/catalog_cli.php load [--dry-run]|list. Idempotent, löscht nichts, schaltet Abgeschaltetes nicht wieder an, stellt abweichende Namen zurĂŒck. - Buchungen:
src/Catalog/Bookings.php(Grundprodukt mit Anker, ZusÀtze darunter, Admin-Dienste ohne Preis mit Systemakte),bin/booking_cli.php offer|add|list|activate|end. 20 Regeln mit festen Fehlercodes, jede Ablehnung im Audit, die Systemakte nie. - Sicht: Die Systemakte (
sot_ref) sieht nur der Inhaber. Das Dashboard zeigt âMeine Buchungenâ (eigener Baum); die Systemakte der Freigabe (group_products.sot_ref) steht nicht mehr auf den Freigabe-Kacheln (Vorgabe: âder Admin sieht den NetBox-Link der User nichtâ). - Tests:
tests/test_bookings.sh(75, mit fĂŒnf absichtlich kaputten Fassungen je rot),test_dashboard_tiles.sh(29), VM-Suiten gegen MariaDB. - docker_vm:
instance-cli.sh <name> catalog_cli.php|booking_cli.php âŠstartet auch diese Werkzeuge. - Nicht gebaut: Domains/Orders, SOAP-JSON-Export (kommt als nĂ€chstes), Import der Systemakten, Preisberechnung.
v3.8.0 â 2026-10-06
Feature: Dashboard-Seite mit Kacheln (public/dashboard.php), nur lesend. Die SSL-Konsole index.php bleibt unverÀndert und weiter das Ziel nach dem Login; die Kopfzeile verlinkt das Dashboard.
src/Modules/Tiles.php(reine Auswertung, schreibt nichts, kein Audit):chain(Anzeige ausChain::status, sperrt nichts),modules(vorhandene Seiten mit Schnelllink; sichtbar nach Rolle, SSL-Konsole nur GruppentypeigenwieAuth::sslAllowed()),products(je aktive Freigabe aktiver Produkte der eigenen Gruppe:ready/pendingmit ânoch n min/Std/Tageâ, Verbrauch der jĂŒngsten Periode, Preisreferenz, SOT-Verweis).- VorlĂ€ufig, nicht von der Crew bestĂ€tigt: Ein Produkt hat noch kein Ziel-Feld. Verlinkt wird nur
sslâindex.php(und nur, wenn verfĂŒgbar); alle anderen Kacheln zeigen âkein Ziel hinterlegtâ, kein erfundener Link. Angezeigt wird die Preisreferenz, nie ein berechneter Preis. - Tests:
tests/test_dashboard_tiles.sh(27: Modell und HTML mit den Demo-Daten, Gruppentrennung, Escape-Probe<script>im Produktnamen, Redirect ohne Anmeldung), gegengeprobt mit drei kaputten Fassungen (inaktive Freigabe sichtbar, Link fĂŒr jedes Produkt, Name ohne Maskierung): je rot.docker_vm/tests/run.sh+6 PrĂŒfungen (58) gegen MariaDB: Freigabe einfĂŒgen, Kachel erscheint, aufrĂ€umen. - Demo:
database/demo/mockup_freigaben_fuer_gruppe.sqlgibt einer bestehenden Gruppe die Demo-Produkte frei (fĂŒr den Klicktest mit echtem Konto, z. B.ozm03). - Nicht gebaut:
Chainist weiter in keiner Seite verdrahtet, keine Sperre, keine Weiterleitung nach dem Login auf das Dashboard, keinproducts.target_url.
v3.7.0 â 2026-10-06
Feature: Env-Flag TLSGATE_MFA_DEFAULT=qr|code fĂŒr den MFA-Modus neu angelegter Gruppen. Standard ohne Angabe: qr (vorher fest code).
Config::mfaDefault(); genutzt vonuser_cli group addundpublic/users.php. UngĂŒltiger oder leerer Wert fĂ€llt aufqrzurĂŒck, GroĂ-/Kleinschreibung egal. Bestehende Gruppen behalten ihrgroups.mfa_policy; es gibt keinen stillen Wechsel, Umstellen weiter pergroup mfa <slug> code|qr. Das Schema (DEFAULT 'code') bleibt unverĂ€ndert, der Wert wird beim Anlegen gesetzt.- Achtung, Sicherheit:
qrheiĂt Passwort allein reicht zur Einrichtung, TOTP ist dann kein zweiter Faktor mehr (siehe v3.4.0). FĂŒr Gruppen mit Root/Managed/SSL (bare,vm,ha,ssl) nach dem Anlegen aufcodestellen oder die Variable aufcodesetzen. Die Festlegung âqr als Standardâ ist Brankos Vorgabe, die Wirkung auf Root-Gruppen ist nicht mit der Crew besprochen. tests/test_mfa_default.sh(8), gegengeprobt mit vertauschtem Standard: rot.test_mfa,test_auth,test_credentials_uisetzenTLSGATE_MFA_DEFAULT=code, weil sie den Code-Weg prĂŒfen.- docker_vm: Image mit neuem Docker (29.8.2) neu gebaut,
run52,p348,p454,p531,p622 grĂŒn. Die Suiten stellen den MFA-Modus der Instanz weiterhin selbst aufcode(lib.sh).
v3.6.1 â 2026-10-06
Fix: eine neue Order-ID wurde nach sslReplace/sslCreate verschluckt oder durch die alte ersetzt.
- Befund: Das Doku-Beispiel zu
sslReplace(S. 170) zeigt<orderNumber nil>, live kam am 10.9. eine neue Nummer.renewCert()gab bei fehlender Nummer still die angefragte zurĂŒck, die Rohantwort wurde nirgends festgehalten.
Am 6.10. (zwei Fehlversuche wegen alter CAA-EintrĂ€ge, Orders 3973250/3973252) erschien die neue ID erst nach Neuladen des SOAP-Index. Ob die Antwort sie trug, lĂ€sst sich fĂŒr diesen Fall nicht mehr belegen. SslHandler::writeInfo()(fĂŒrrenewCertundorderCert):orderNumbernur, wenn die Antwort eine enthĂ€lt, sonstnull; dazurequestedOrder,newIdGiven,newIdIsNewundraw(orderNumber,returnCode,returnSubCode,returnMessage,serverTRID).- Audit:
ssl.renewCert/ssl.orderCertschreiben die Rohantwort inaudit_log.ctx, Ergebnisaccepted,accepted_no_new_idoderfailed. dashboard.sh schreibtrenew_responsebzw.ok_no_new_idmitraw=insactions.logund macht bei fehlender ID nicht mit der alten Nummer weiter. - OberflĂ€che: Hinweis âkeine neue Order-ID geliefertâ in den Ergebnisseiten von Order und Renew.
tests/test_writeinfo.sh(12), gegengeprobt mit RĂŒckfall auf die angefragte Nummer: rot.- Nicht gelöst: Domains mit n Orders in der DB (Abrechnungszeitraum â SSL-Laufzeit) und der Kategorie-Layer (z. B. digicert
#abrechnung, letsencrypt#free). Entwurf steht aus, Form vorlÀufig.
v3.6.0 â 2026-10-05
Feature: Modulkette als Regel (src/Modules/Chain.php). In keiner Seite verdrahtet, kein Schema-Wechsel, Standardverhalten unverÀndert.
- Neu:
Chainwertet#login â #crew â #schluessel â #ca â #hosting â #sslje Gruppe aus den Stammdaten aus (status,access,next,check,requireModule). Zwei Regeln nach Vorgabe:
(1) keine Sackgasse: der Zugang zu einem Glied verlangt nur, dass die Glieder davor nicht fehlen, nicht dass es selbst erfĂŒllt ist, sodass das nĂ€chste Glied nach dem ersten Zugang immer anlegbar bleibt;
(2) Fehlversuch im Audit: wer ein Modul ohne Berechtigung aufruft, wird abgewiesen,module.denied/deniedmitreason(vorgaenger_fehlt,keine_freigabe,unbekanntes_modul) undblocked_by.
Glied âcaâ istopen(Platzhalter, blockiert nicht): die CA ist heute nur eine Test-CA der Node. Nicht eingebaut, weil die SSL-Konsole sonst fĂŒr alle Gruppen ohne Freigabe sofort gesperrt wĂ€re. - VorlĂ€ufige Festlegungen, nicht von der Crew bestĂ€tigt (Kopf von
Chain.php,docs/DESIGN_DASHBOARD.md): login = erste erfolgreiche Anmeldung in der Gruppe, crew = mindestens ein aktives Konto, schluessel = aktiver SOAP-Zugang,
hosting/ssl = aktive, verfĂŒgbare Freigabe der passenden Produktart. Der Test hĂ€lt sie fest, damit eine Ănderung auffĂ€llt. tests/test_chain.sh(43): Ende-zu-Ende ĂŒber alle sechs Glieder, von der leeren Gruppe bis zum freigegebenen SSL, mit der 30-Minuten-Verzögerung ĂŒberavailable_from, getrennten Gruppen und Audit-ZĂ€hlung (jede Ablehnung, keine Erlaubnis).
Gegengeprobt mit drei absichtlich kaputten Fassungen (ohne Audit, mit Sackgasse,openblockiert): je rot.
v3.5.0 â 2026-10-05
Schema: Stammdaten fĂŒr Produkte und Freigaben (docs/DESIGN_DASHBOARD.md, Schritt 1). Nur Tabellen, noch keine OberflĂ€che und kein Programmcode, der sie liest.
- Neu:
products(Katalog,kind=shared|bare|vm|ha|ssl),group_products(Freigabe je Gruppe:active,available_from,price_ref,sot_ref,created_by) undproduct_usage(Verbrauch je PeriodeJJJJ-MM
und Metrik). FremdschlĂŒssel ohne Löschweitergabe,UNIQUEje Gruppe/Produkt und je Freigabe/Periode/Metrik.available_from,price_ref,sot_refsind bewusst nullable: Startpunkt der 30-Minuten-Regel, Preis- und SOT-Quelle sind offen.
groups.typebleibt der Rechte-Schalter und ist nicht der Produkttyp.tests/test_products.sh48 (SQLite und MariaDB gleiche PrĂŒfungen,migrateauf dem Stand v3.4.1). - Fix (Migrator, MySQL/MariaDB):
migrate --statuskannte nur zwei feste Tabellen. Eine neue Tabelle ohne Migrationsdatei meldete "nichts zu tun", unddocker_vm/instance.shĂŒbersprangmigrate: die Instanz lief ohne die Tabellen
weiter (beiozm01gefunden, weilp5den Doktor mit 14 Tabellen erwartet). Jetzt liest der Status die Tabellennamen ausschema.mysql.sqlund meldet fehlende alscreate. Regressionstest mit altem Migrator rot, mit neuem grĂŒn. - Doktor: erwartet 14 Tabellen (
bin/TLS-doktor.bash). docker_vm:lib.shstellt den MFA-Modus vonozm01vor jeder Abnahme aufcode; ein Klicktest mitgroup mfa ⊠qrlieĂ sonst 3 bis 24 PrĂŒfungen je Suite scheitern.
v3.4.1 â 2026-10-05
Patch: veraltetes Schema meldet sich im Web als 503 mit dem Befehl. Kein Schema-Wechsel.
- Fix: Wer v3.4.0 einspielte und
migratevergaĂ, bekam bei der Anmeldung eine leere Seite (HTTP 200, Fatalno such column: g.mfa_policynur im Log).bootstrap.phpfĂ€ngt im Web
fehlende Spalten/Tabellen (SQLite-Text, MySQL42S22/42S02) ab und antwortet503 â ⊠Datenbank-Schema veraltet (php bin/user_cli.php migrate; vorher migrate --status). Andere Fehler laufen unverĂ€ndert weiter.
Neu:tests/test_schema_503.sh(12), alter Stand aus der Git-Historie, dazu die Gegenprobe, dass andere Fehler ein 500 mit Ursache im Log bleiben (ein erster Entwurf mit Rethrow lief in eine Endlosrekursion; docker_vmp3hat das gefunden). Merke: nach jedem Updatephp bin/user_cli.php migrate.
v3.4.0 â 2026-10-05
Feature: MFA-Modus je Gruppe (groups.mfa_policy). Schema-Wechsel (Migration 0002), Standardverhalten unverÀndert (code).
- Neu:
groups.mfa_policy=code|qr(Defaultcode). Beiqrerzeugt jeder Passwort-Login ein neues Secret + QR, also auch nach Abmelden und Session-Ablauf; beicodegilt nach der Einrichtung nur der Code der App.
Setzen:user_cli.php group mfa <slug> <code|qr>oder inusers.php(Spalte MFA).group listzeigt den Modus. Der Support-Weguser code/reset-mfaauf der Bash bleibt.
Sicherheit: wie beiTLSGATE_MFA_QR_EVERY_LOGIN: wer das Passwort kennt, sieht das QR. Gedacht fĂŒr Login-Produkte (Shared/Bare), nicht fĂŒr Gruppen mit Root/Managed/SSL.
Der Env-Schalter bleibt als globale Ăbersteuerung. Auslöser: nach dem Abmelden kam kein QR, nur der Hinweis auf die Bash. Ursache war der Default, kein Fehler im Code. - Migration
0002_group_mfa_policynur fĂŒr MySQL/MariaDB (idempotent ĂŒberinformation_schema); SQLite gleichtgroupsselbst an.tests/test_mfa.sh39 (+11), MariaDB-Migration grĂŒn.
v3.3.0 â 2026-10-03
Feature: optional neues TOTP-QR bei jedem Passwort-Login; login.doktor.sh. Kein Schema-Wechsel, Standardverhalten unverÀndert.
- Neu:
TLSGATE_MFA_QR_EVERY_LOGIN=1(src/Auth/Auth.php): jeder Passwort-Login erzeugt ein neues TOTP-Secret, legt es versiegelt in der DB ab (totp_enabled=0bis zur BestÀtigung)
und zeigt das QR; erst der Code des neuen Secrets meldet an. Das alte Secret ist sofort ungĂŒltig,totp_last_stepwird zurĂŒckgesetzt (ein eben benutzter Code ist nach Logout nicht mehr gesperrt).
Sicherheit: wer das Passwort kennt, sieht das QR â TOTP ist dann kein zweiter Faktor mehr. Nur Mockup/Demo; Standard aus.tests/test_mfa.sh28 (+4). - docker_vm:
instance.sh <name> [mock] [qrlogin]setzt die Variable im Pool (env[TLSGATE_MFA_QR_EVERY_LOGIN] = 1). OhneqrloginentfÀllt sie beim nÀchsten Lauf wieder. Bewusst nicht
fest im Pool-Template: die Abnahmen (run.sh,p3âp6) melden sich mituser codean und brauchen das bestehende Secret. Im Container von Hand belegt: zwei Logins, je neues Secret, 302, danachmfa.
Nicht automatisiert abgesichert: die Optionqrloginininstance.sh(kein eigener Test indocker_vm/tests/). - Neu:
bin/login.doktor.sh [URL] [Instanz] [E-Mail]â warum geht die Anmeldung nicht? Nur lesend: Port vergessen, falscher Dienst auf dem Port, Vhost/Host-Header,
__Host--Cookie ĂŒber http,login.php/CSRF, Pool-Schalter, Konto (gesperrt/deaktiviert/mfa-offen) und die letztenauth.*-Audit-EintrĂ€ge (bad_password,bad_code,blocked_ip).
v3.2.1 â 2026-10-02
Patch: Dubletten in der CLI melden einen Satz statt einer PDOException. Kein Schema-Wechsel.
-
Fix:
group addunduser addmit vorhandenem Slug bzw. vorhandener E-Mail (bin/user_cli.php): Vorab-PrĂŒfung, Exit 1, â⊠gibt es schon âŠâ; beiuser addvor der Passwortabfrage.
ZusĂ€tzlich wirdSQLSTATE 23000aus dem INSERT (Wettlauf) abgefangen. Die WeboberflĂ€che (users.php) prĂŒfte schon vorab.tests/test_auth.sh76 (+3, Gegenprobe: mit dem alten
user_cli.phpscheitern die zwei MeldungsprĂŒfungen),docker_vm/tests/p3.sh+2. Nicht einzeln belegt: die catch-Zweige fĂŒr den Wettlauf. -
Fix:
cred addmit vorhandenem Label zeigt einen Satz statt der PDOException.CredentialStore::add()prĂŒft vorab und fĂ€ngt zusĂ€tzlichSQLSTATE 23000aus dem INSERT (Wettlauf);
die Meldung nennt den Weg (replacebzw. âZugangsdaten ersetzenâ) und gilt fĂŒr CLI und Web (credentials.phpzeigte vorher den rohen SQL-Text).tests/test_credentials.sh62 (+3),
docker_vm/tests/p3.sh46 (+1, echter Aufruf im Container). Nicht einzeln belegt: der catch-Zweig fĂŒr den Wettlauf (die Gegenprobe zeigt nur, dass ohne VorprĂŒfung die DB-Meldung kĂ€me). Offen bleibtgroup addmit vorhandenem Slug.
Auch innative_php/.
v3.2.0 â 2026-10-02
Feature: Deploy-ZIP mit ssl_outbox; Fix: neuer aktiver SOAP-Zugang in der VM. Kein Schema-Wechsel.
- Fix (docker_vm): neuer aktiver SOAP-Zugang liess sich in der VM nicht anlegen.
instance.sh <name> fakeschriebenv[TLSGATE_SOAP_HOSTS] = soap.fake.testin den Pool;
CredentialStore::check()lehnte damit jede andere WSDL ab. Der Modusfakeist entfernt (instance.shkennt nur nochmock), ebenso die fest eingetragenen
Fake-Zugangsdaten in der.profileder Instanz.TLSGATE_SOAP_HOSTSbleibt eine bewusste, manuelle Betriebsoption. Der Fake-Registrar im Container bleibt als gewoehnliches
Test-Ziel;p3.shlegt seinen Zugang selbst an und belegt, dass ein Zugang mit fremdem WSDL-Host angelegt werden kann (45 Pruefungen).p4.shreicht die Fake-Zugangsdaten
nur fuer dendashboard.sh-Aufruf mit. Bestehende Instanzen verlieren die Einschraenkung beim naechsteninstance.sh <name>. - Neu: Deploy-ZIP. In der Detailansicht (getOrder) der Knopf âDeploy-ZIPâ:
public/outbox.phpholtsslInfomit dem Zugang der eigenen Gruppe und liefert
<domain>.zipmit<domain>/<domain>.certund<domain>/<domain>.chain.pem(dieselben Dateien wiedashboard.sh deploy, native_bash_cli). Nie ein Key: TLS-Gate besitzt keinen,
ein PEM mit âPRIVATE KEYâ bricht ab. Dieselben Bytes liegen in derssl_outbox(Config::outboxDir(), Standard<state>/ssl_outbox/<gruppe>/<domain>.zip, Ordner 0700, Datei 0640,
umlenkbar mitTLSGATE_OUTBOX_DIR) zum Einsammeln durch die Orchestrierung. Ab Rolleuser, CSRF, Auditssl.deployZip(requested/ok/fail, ok mit SHA-256). ZIP ohne PHP-Erweiterung
(src/Export/CertBundle.php, Methode âstoredâ).tests/test_outbox.sh(27 Pruefungen, offline) unddocker_vm/tests/p6.sh(22 Pruefungen gegen die Instanz). Auch innative_php/.
Nicht belegt: ein Zertifikat einer echten CA (nur Fake-PEM), die Weiterverteilung aus der Outbox. - Doku (docker_vm/HANDBUCH.md): §3a SOAP-Zugang anlegen, §3b Deploy-ZIP und Outbox, §3c der Weg vom Nullpunkt (
down -v). Fix: der ssh-Ablauf (§5) liessinstall -d /root/.provision/sshaus; nachdown -vscheitertedocker cpdes Schluessels mit âCould not find the fileâ, undinstance.sh ozm01 fakegibt es nicht mehr. - Referenz-Stand (frisch nach
down -v):run.sh52,p3.sh45,p4.sh54,p5.sh31,p6.sh22.
v3.1.0 â 2026-10-01
Feature: Zwei-Faktor erneuern, angemeldet. Kein Schema-Wechsel.
- Neu:
public/mfa.php(â2FAâ im Kopf der Konsole). Wer angemeldet ist, kann sein TOTP selbst ersetzen: aktuelles Passwort bestĂ€tigen, neuer QR-Code
(mit SchlĂŒssel undotpauth://-URI), erst der erste gĂŒltige Code des neuen Secrets tauscht es in der DB. Bis dahin gilt das alte (kein Aussperren bei
misslungenem Scan); âAbbrechenâ Ă€ndert nichts. Das neue Secret liegt bis zur BestĂ€tigung versiegelt in der Session (5 min), nicht in der DB; ein falsches Passwort
zĂ€hlt fĂŒr die Kontosperre; fĂŒnf falsche Codes verwerfen den Vorgang. Jede Rolle darf ihr eigenes TOTP erneuern. Audit:auth.mfa_rotate_begin,auth.mfa_rotate
(ok/fail), nie das Secret.Auth::mfaRotationBegin/Confirm/Cancel/Pending/Data. Vorher gab es nuruser reset-mfaper CLI oder die Einrichtung beim allerersten Login. tests/test_mfa.sh(24 PrĂŒfungen): Passwortzwang, QR, altes Secret gilt bis zur BestĂ€tigung, danach nur das neue, Abbruch, Audit ohne Secret, CSRF. LĂ€uft lokal (eigene SQLite-DB), nicht indocker_vm/.- Fix:
docker_vm/tests/p4.shwar locale-abhĂ€ngig. Das Muster^G.ltigerwartet, dass.dasĂŒtrifft; unterLANG=Csind es zwei Bytes. Jetzt^G.{1,2}ltig. Die Ausgabe vondashboard.shwar nie falsch. - Referenz-Stand (frisch gebaute VM nach
down -v):run.sh52,p3.sh43,p4.sh54,p5.sh31 PrĂŒfungen grĂŒn. .codebase-memory/graph.db.zstwird nicht mehr getrackt (abgeleiteter Index, in.gitignore).
v3.0.4 â 2026-10-01
Patch: Der 500 in der Detailansicht ist jetzt mit einem Fake-Registrar nachgestellt und an zwei weiteren Stellen behoben (Strukturen in Textfeldern, fehlendes php-mbstring).
Kein Schema-Wechsel.
- Fix: Struktur in einem Textfeld gab weiter einen 500 (
templates/ssl/*): 21 Stellen castelten mit(string)vor->e()und umgingen damit den Fix aus v3.0.2
(stdClassindomain,status,reseller, EigentĂŒmerdaten,comodostatus). Die Casts sind entfernt;e()liest Listen und Objekte selbst.tests/test_view.sh(22 PrĂŒfungen) verbietet->e((string)âŠ). - Fix: fehlendes
php-mbstringlieĂ die Detailansicht mit einem fatalen Fehler ausfallen (nicht nur den Fehlerpfad:SslControllerruftmb_substrim Normalfall).bootstrap.php
meldet fehlendesmbstring/sodiumjetzt sofort mit HTTP 503 und dem Paketnamen (auch im Log, CLI: Exit 1);composer.jsonverlangtext-mbstring;TLS-doktor.bashprĂŒft es. - Fix:
TLS-doktor.bashbrach unter GNU/Linux ab und kannte nur SQLite.perm()/owner()/fgrp()riefen zuerst BSD-stat -f, das unter GNU âgelingtâ (Dateisystem-Info statt Rechte; Abbruchunbound variable). Jetzt Weiche perstat --version. Neu: TreiberTLSGATE_DB_DRIVER=sqlite|mysql(bei mysqlpdo_mysqlPflicht, Schema und Version ĂŒberDb::pdo()/Migrator, keine DB-Datei-PrĂŒfungen; SQLite bleibt der Offline-Dev-Standard),WEB_USERfĂŒr den php-fpm-Pool-Benutzer, LesbarkeitsprĂŒfung als root perrunuser. Inbin/undnative_php/bin/.docker_vm/tests/p5.sh(31 PrĂŒfungen: Sollstand,root.key0644, falsches DB-Passwort, MariaDB gestoppt,state/-Besitz, Guard aus, Pool-Datei). Neu: Pool-Check â der Doktor liest perphp-fpm -ttden Pool des Webserver-Benutzers und prĂŒftdisable_functions(AUD-33),clear_env,open_basedir,display_errors. - Fix:
dashboard.shzeigte gegen Live-SOAP rohe Unix-Zeitstempel (ohne Resttage und Farbe):days_left()verstand nur ISO, die API liefert Unix-Sekunden (0= kein Datum). Neuto_epoch/fmt_ts(GNU und BSDdate), eingesetzt in Liste, Details und Cross-Check, inbin/dashboard.shundnative_bash_cli/.tests/test_dates.sh(16 PrĂŒfungen). docker_vm/P4:ozm02, ssh nur mit SchlĂŒssel (Port 2222, Instanz-Shell mitdashboard.shgegen den Fake-Registrar),tests/p4.sh(52 PrĂŒfungen: Isolation, Guard, getrennte Logs, Reseller-Sperre, ssh). Bekannt, nicht behoben:dashboard.shzeigt gegen Live-SOAP rohe Unix-Zeitstempel (siehedocker_vm/PLAN.md5c).docker_vm/: Fake-Registrar (fake-soap/: WSDL, Szenarien je Order-Nummer, Fake-CA, nur im Container) undtests/p3.sh(43 PrĂŒfungen). Reproduziert den 500 der Node mit dem altenView.php.
v3.0.3 â 2026-10-01
Patch fĂŒr Installationen mit MariaDB/MySQL (TLSGATE_DB_DRIVER=mysql): Fehlversuche beim Anmelden lieferten einen 500. Mit SQLite (Standard) war nichts betroffen.
Kein Schema-Wechsel.
- Fix: HTTP 500 bei jedem Fehlversuch unter MariaDB/MySQL (
Auth::accountFail()):UPDATE ⊠RETURNINGkennt nur SQLite. BeiTLSGATE_DB_DRIVER=mysql
warf jeder Fehlversuch (falsches Passwort, falscher TOTP-Code) bei einem bestehenden Konto einenPDOExceptionund damit einen 500; unbekannte Konten kamen durch.
JetztUPDATE, dannSELECT(portabel). Neutests/test_sql_portable.sh(statische PrĂŒfung auf SQLite-only-SQL). Gefunden mitdocker_vm/tests/run.sh. docker_vm/: P1 (provision/instance.sh, idempotent) und P2 (tests/run.sh, 52 PrĂŒfungen: Web-Login mit QR/MFA, Konsole mit Mockdaten, Seiten, Logs, Bash-Dashboard).
v3.0.2 â 2026-10-01
Patch nach dem RZ-Test von v3.0.1: Der 500er in der Detailansicht blieb, weil die Ursache ein anderer Wert war als in 3.0.1 behoben (SOAP-Objekte, nicht Listen).
Dazu die seit 3.0.1 aufgelaufenen Ănderungen. Kein Schema-Wechsel.
- Fix: 500 in der Detailansicht durch SOAP-Objekte (
Http\View::e(),copyDt()): v3.0.1 deckte Listen ab, aber SOAP liefert Strukturfelder
(z. B.validationDNS) auch alsstdClass.(string) $objektwirft in PHP 8Error: Object of class stdClass could not be converted to string,
undcopyDt()hatte das<dt>schon ausgegeben â die Seite brach mitten im Satz ab (RZ-Fund 1.10., Gegenprobe: die dreiechoincopyDtauskommentiert).
Jetzt werden Listen und Objekte gleich flach gelesen (Werte kommagetrennt,__toStringals Text, Tiefengrenze 8); ein Objekt in einer Liste ging vorher
stillschweigend verloren.copyDt()berechnet zuerst und gibt erst dann aus und ĂŒberspringt, was nach dem Abflachen leer ist.tests/test_view.sh21 PrĂŒfungen. bin/dashboard-ro.sh+bin/ro_cli.php: rein lesender Abruf (list,info,info --file,raw). Zugang ausTLSGATE_RO_WSDL/_USER/_PASS,
keine DB, kein Guard; nursslListExtendedundsslInfo(mitgetCSR=1).infowertet CSR (Selbstsignatur, Key, Hash),
Zertifikat (Restlaufzeit, CSRâZertifikat-Key) und Chain (Zertifikate, Root-Warnung,openssl verify) aus. Das CSR-Alter ist aus SOAP nicht
ableitbar und wird nur als Untergrenze (seitnotValidBefore) genannt. Bewusste Ausnahme zur Regel âZugang nie aus der Umgebungâ, Grenzen in
docs/SECURITY.md§1. Jeder Aufruf (auch abgelehnt) hÀngt eine Zeile an~/.local/state/tlsgate/ro.log
(0600,TLSGATE_RO_LOG=<datei>|off), ohne Zugangsdaten.tests/test_dashboard_ro.sh(41 PrĂŒfungen, offline mit selbst erzeugter CA).- Neu im Repo, ohne Einfluss auf den Lauf:
native_bash_cli/(unverÀnderter Stand3dec8a8vom 24.9., vor Login/DB) undnative_php/(nur, was das Frontend nutzt;
ohne Bestellschicht, Export und Node-CA) als Vorlagen fĂŒr eigene Repos,docker_vm/PLAN.md(RZ-Setup im Container, noch nicht gebaut). Nichts im SOT wurde gelöscht. docs/AUDIT.md: doppelteBWA-AUD-30bereinigt â der Guard-Besitzerweg ist jetztBWA-AUD-33, neuBWA-AUD-32(Ausnahme zu AUD-25 fĂŒrdashboard-ro.sh).- CLI-Guard: Besitzer der Instanz (
Guard::instanceOwner): Neben root/sudo darf diebin/-Werkzeuge ausfĂŒhren, wem alles Vorhandene aus
State-Verzeichnis, SQLite-DB und WurzelschlĂŒsseldatei gehört (mindestens eines muss existieren). FĂŒr den RZ-Betrieb, in dem jeder User eigene
Dienste hat und es kein root gibt. Gemischter Besitz, fehlende Dateien, Web-SAPI und der Dienst-User (nur Klasseservice) bleiben drauĂen.
Akteur im Audit ist der echte User,SUDO_USERzÀhlt ohne euid 0 weiterhin nie. Grenze steht indocs/DESIGN_TENANTS.md§5
(gehört dem Web-User alles, ist er auch CLI-Operator). Exit-Codes unverÀndert.tests/test_guard.shum die Besitzer-FÀlle erweitert.
v3.0.1 â 2026-09-30 · Tag auf ad07090; RZ-Test: 500er in der Detailansicht blieb, behoben in v3.0.2
Patch nach dem ersten Lauf auf einer Node mit Apache + PHP-FPM (RĂŒckmeldung vom 30.9.). Kein Schema-Wechsel, kein Guard-Umbau.
- Fix
View::e()/copyDt(): SOAP-Felder, die als Liste kommen (z. B. mehrere Validierungswerte), wurden als âArrayâ mit PHP-Warning
ausgegeben. Jetzt kommagetrennt, leere Listen entfallen. MutmaĂliche Ursache des 500 auf der Details-Seite im RZ (lokal nur die Warnung
reproduziert, der 500 selbst nicht bestÀtigt; bei Wiederholung die Zeile aus dem FPM-Log nachreichen). Neu:tests/test_view.sh. - Doktor: warnt, wenn
root.keyan mehreren ĂŒblichen Orten liegt (/etc/tlsgate,/var/lib/tlsgate,state/), und prĂŒft, ob der Web-User
ihn lesen kann (nur mit passwortlosemsudo). Der Behebungstext empfahlsudo -u <WEBUSER> ⊠migrate, das der Guard ablehnt: jetztsudo ⊠migrate, danachchown. - Doku:
deploy/apache-vhost.conf.examplesagt dasselbe wiedeploy/README.md(Befund #9 der RĂŒckmeldung). - Bewusst nicht in diesem Patch: Guard fĂŒr den Besitzer der Instanz (siehe Unreleased); der CLI-Weg im RZ war hier noch
sudo+chown. -
Nicht belegt: der Lauf im RZ selbst; der Zweig âmehrere root.keyâ und der Lesetest des Doktors sind nur als Syntax-/Lauftest auf einem Mac gesehen.
-
Demo-Setup
resources/demo/digdafunk.sh [domain] [--reset]: baut eine eigene Instanz unterstate-demo/(gitignored, fasststate/nicht an).
Mensch mit SchlĂŒssel + CSR, Node-CA, zwei Bestellungen, Tick-LĂ€ufe, CRL, Export mit echter AuĂensicht,sample.htmldaneben.
Die Validierung der ersten Bestellung kommt aus einer DNS-Fixture (Herkunftfixture), die zweite bleibt beim echten DNS stehen. order_tick.php --node-ca --dns-fixture <datei>: CNAMEs aus einer Datei statt echtem DNS, nur mitTLSGATE_MOCK=1, Herkunftfixture.
Getestet intests/test_export.sh(Ablehnung ohne Mock-Schalter und ohne--node-ca).
v3.0.0 â 2026-09-29 · Alle sieben Schritte des Mandanten-Entwurfs
Mit v3.0.0 ist docs/DESIGN_TENANTS.md §9 vollstĂ€ndig umgesetzt: WurzelschlĂŒssel, SOAP-ZugĂ€nge je Gruppe, CLI-Guard,
Bestellzustand mit Herkunft, order_tick, Export/Monitor und die Verwaltung der ZugÀnge im Browser. Dazu Node-CA mit CRL,
Sensor-IdentitÀt per mTLS und der Migrator.
GegenĂŒber v2.2.0 neu fĂŒr Betrieb und Crew:
- group_admin pflegt die SOAP-ZugÀnge der eigenen Gruppe selbst im Browser (SOAP-ZugÀnge), admin die aller Gruppen.
Die CLI (user_cli.php cred âŠ) bleibt gleichwertig.
- Kein Schema-Wechsel gegenĂŒber v2.2.0. Update: migrate --status sollte ânichts zu tunâ melden.
Weiterhin nicht belegt: ein Lauf gegen den echten Registrar (cred test / âLive testenâ), der Scan des TOTP-QR mit einer
Authenticator-App, OCSP und automatische CRL-Verteilung, automatischer Tabellenumbau unter MySQL.
- SOAP-ZugÀnge im Web (Schritt 7 aus
docs/DESIGN_TENANTS.md):public/credentials.php, Link in der SSL-Konsole fĂŒradminundgroup_admin. group_adminverwaltet nur die ZugĂ€nge der eigenen Gruppe,adminalle. GeprĂŒft je Aktion am Datensatz: Eine gefĂ€lschtecideiner fremden
Gruppe ergibt âUnbekannter Zugangâ (verrĂ€t die Existenz nicht) und steht alsdeniedim Audit. Eine gefĂ€lschtegroup_idbeim Anlegen landet
in der eigenen Gruppe. Gegenversuch ohne diese PrĂŒfung: 4 rote PrĂŒfungen.- Anlegen, Zugangsdaten ersetzen, an/aus, âLive testenâ:
sslListExtendedlesend, zeigt Anzahl und die sichtbaren Zertifikate (erste 50),
gekennzeichnet als âLive SOAPâ mit lokaler Zeit. Bei Störung âSOAP nicht verfĂŒgbar ⊠keine Beispieldatenâ. - Benutzer und Passwort werden nie zurĂŒck ins HTML, ins Audit oder in Meldungen geschrieben. Strikte CSP, kein Inline-JS, CSRF auf jeder Aktion.
- Tests:
tests/test_credentials_ui.sh(35) ĂŒber echte HTTP-Anfragen mit Login (Passwort + TOTP) je Rolle. Die Anzeige eines erfolgreichen
Live-Tests ist am Template mit einem synthetischen Ergebnis geprĂŒft (inkl. Escaping einer Domain aus der SOAP-Antwort). Gegen einen echten
Registrar ist die Seite nicht gelaufen.
v2.2.0 â 2026-09-29 · Mandanten-Kern: SchlĂŒssel, ZugĂ€nge, Bestellzustand, Node-CA, Monitor
Breaking gegenĂŒber v2.1.0:
- SOAP-ZugĂ€nge liegen je Gruppe verschlĂŒsselt in der DB (user_cli.php cred add). BWA_SOAP_* und Config::soap() sind entfernt.
- Kein stiller Mock mehr: ohne Zugang oder bei Störung âSOAP nicht verfĂŒgbarâ, Beispieldaten nur mit TLSGATE_MOCK=1.
- Die bin/-Werkzeuge verlangen root/sudo (Guard, Exit 3); Dev/Tests: TLSGATE_GUARD=off. Deploy: migrate als root, danach chown.
- TOTP-Secrets im neuen Format (tg1:), Umzug per user_cli.php key migrate.
Update von v2.1.0: php bin/user_cli.php migrate --dry-run, dann migrate (Sicherung liegt daneben), key migrate, cred add + cred test,
danach BWA_SOAP_* aus Pool/vhost entfernen und bin/TLS-doktor.bash grĂŒn ziehen.
Einzelheiten, in der Reihenfolge der Entstehung:
- MySQL/MariaDB-Connector neben SQLite (
TLSGATE_DB_DRIVER=sqlite|mysql,database/schema.mysql.sql). - TOTP per QR:
TlsGate\Auth\Qrist ein QR-Encoder ohne AbhÀngigkeit (Byte-Modus, Fehlerkorrektur M,
bis 213 Byte). Das Secret verlĂ€sst den Server nie, es gibt keinen externen QR-Dienst. php bin/user_cli.php user totp-setup <email> [--watch]: QR im Terminal (schwarz auf weiĂ, theme-unabhĂ€ngig),
Live-Countdown und Prompt. TOTP wird erst nach dem ersten gĂŒltigen Code aktiv, nach 5 Fehlversuchen
oder 5 Minuten bricht es ab. Gesperrte Konten bleiben gesperrt, das Secret landet nicht im Audit-Log.
--watchzeigt zusĂ€tzlich den erwarteten Code, nur fĂŒr Dev/Mockup.- Browser-Enrollment (
public/login.php) zeigt den QR als Inline-SVG ĂŒber dem manuellen SchlĂŒssel. -
Tests:
tests/test_qr.sh(28). GeprĂŒft wird gegen die Norm-Struktur (Finder, Timing, Formatinfo mit BCH)
und, falls python3 + OpenCV da sind, im Round-Trip mit einem unabhÀngigen Decoder (91 Texte von 1 bis 213 Byte).
Der Àltere OpenCV-Decoder liest einzelne Version-10-Codes nicht, der Aruco-Decoder liest alle. -
WurzelschlĂŒssel + KDF (
TlsGate\Security\Secrets, Schritt 1 ausdocs/DESIGN_TENANTS.md): ein SchlĂŒssel
(TLSGATE_ROOT_KEYoderTLSGATE_ROOT_KEY_FILE, Standardstate/root.key, 0400), getrennte UnterschlĂŒssel je Zweck,
XChaCha20-Poly1305 mit Bindung an den Datensatz. Blobtg1:+ base64(version | key_id | nonce | ct). - TOTP-Secrets nutzen ihn, gebunden an die User-ID.
Totp::encrypt/decryptbekommen dafĂŒr die User-ID. user_cli.php key status | rotate | migrate. Altformat-Blobs (secretbox,tlsgate.key) bleiben lesbar, biskey migratelief.migratelegtroot.keyan, aber keinetlsgate.keymehr. Die App verweigert eine fĂŒr alle lesbare Wurzeldatei.- Doktor prĂŒft WurzelschlĂŒssel und warnt, wenn er neben der DB liegt. Deploy-Vorlagen und Doku nutzen
root.key. - Tests:
tests/test_secrets.sh(51). -
Umstellung bestehender Installationen:
php bin/user_cli.php migrate(legtroot.keyan), dannkey migrate
mit noch gesetztemTLSGATE_SECRET_KEY/TLSGATE_KEY_FILE. Danach kann der alte SchlĂŒssel weg. -
SOAP-ZugÀnge je Gruppe in der DB (Schritt 2 aus
docs/DESIGN_TENANTS.md): Tabellesoap_credentials
(database/schema*.sql, idempotent permigrate),Security\CredentialStore, WertobjektSecurity\SoapCredential.
Blob mit dem WurzelschlĂŒssel versiegelt (Zwecksoap), gebunden an Gruppe + Label. Wird beikey rotate/key migratemitgenommen. user_cli.php cred add | list | replace | enable | disable | test. Benutzer/Passwort werden abgefragt, nie als Argument.
WSDL nurhttps://ohne eingebettete Zugangsdaten, optional eingeschrÀnkt durchTLSGATE_SOAP_HOSTS.- Breaking:
Config::soap()undBWA_SOAP_USER/PASS/WSDLsind entfernt. Web: Zugang der Gruppe des angemeldeten Users.
CLI/Bash:TLSGATE_GROUP=<slug>(optionalTLSGATE_CRED=<label>), z. B.TLSGATE_GROUP=bwa bin/dashboard.sh list. - Breaking: Kein stiller Mock mehr. Ohne Zugang oder bei gestörter Verbindung zeigt die UI âSOAP nicht verfĂŒgbarâ mit Grund
(source: unavailable),soap_cli.phpendet mit Exit 1. Beispieldaten nur mitTLSGATE_MOCK=1. Das gilt auch fĂŒr
AuthCode und Besitzerdaten (vorher erfundene Werte).SslHandlerbekommt den Zugang als Konstruktorargument. - Tests:
tests/test_credentials.sh(59), neuer Check intests/test_auth.sh;tests/test_smoke.shsetztTLSGATE_MOCK=1ausdrĂŒcklich. - Umstellung:
php bin/user_cli.php migrate(legt die Tabelle an),cred add,cred test. DanachBWA_SOAP_*aus Pool/vhost entfernen. -
Nicht belegt: Der Live-Aufruf gegen den echten Registrar. Getestet ist die Verdrahtung bis zum
SoapClient(gegen einen toten Host). -
CLI-Guard (
Security\Guard, Schritt 3 ausdocs/DESIGN_TENANTS.md):bin/user_cli.phpundbin/soap_cli.phpverlangen root
odersudo(Exit 3, es wird nichts geschrieben). BeisudomussSUDO_USERin einer Operator-Gruppe sein
(sudo,wheel,admin;TLSGATE_OPERATOR_GROUPS).SUDO_USERallein reicht nie. Der Web-Prozess kommt nie durch. - Breaking: Diese Werkzeuge (und damit
dashboard.sh) brauchensudo. Dev/Tests:TLSGATE_GUARD=off(nur exaktoff).
Das steht im Audit ("guard":"off"), der Doktor warnt, und am Terminal erscheint eine Warnzeile (nicht bei umgeleitetem stderr, weildashboard.shes mit dem JSON auswertet). - Jeder CLI-Audit-Eintrag trÀgt den Akteur (
"actor":"cli:alice");cred_addschreibt ihn nachcreated_by. - Deploy:
migrate,group add,user add,cred addlaufen jetzt als root/sudo, nicht mehr alssudo -u <WEBUSER>(das wird abgelehnt),
danachchown -R <WEBUSER> /var/lib/tlsgate(deploy/README.md,README.md). Nicht auf einem Linux-Host durchgespielt. - Klasse
service(Dienst-User,TLSGATE_SERVICE_USER) fĂŒr einen spĂ€terenorder tickvorbereitet, noch ohne Verbraucher. - GeĂ€ndert:
soap_cli.phpmeldet âSOAP nicht verfĂŒgbarâ mit Exit 1 (Schritt 2 hatte Exit 3). Exit 3 gehört jetzt dem Guard. - Doktor prĂŒft die PHP-Erweiterung
posixund warnt beiTLSGATE_GUARD=off. -
Tests:
tests/test_guard.sh(40: Entscheidungsmatrix, echte Skripte als normaler User). Nicht belegt: ein Lauf als root/sudo
(sudo bash tests/test_guard.shfĂŒhrt den root-Zweig aus). Alle anderen Tests setzenTLSGATE_GUARD=offausdrĂŒcklich. -
Inventar und Bestellzustand (Schritt 4 aus
docs/DESIGN_TENANTS.md): Tabellenassets,cert_orders,order_events
(database/schema*.sql, idempotent permigrate), KlassenTlsGate\Orders\{OrderStore, AssetStore, OrderState, EvidenceKind},
neue CLIbin/order_cli.php(asset add|list,order new|list|show|move|cancel,states). Nur root/sudo (Guard). - Zustandsautomat als Vertrag:
database/order_states.json(ZustĂ€nde, ĂbergĂ€nge, EndzustĂ€nde, Herkunftsregeln, Exit-Codes) wird von
OrderStategelesen und ist sprachneutral fĂŒr den Rust-Kern gedacht. Enum und Datei sind per Test deckungsgleich.
csr_submitted â awaiting_dns â validated â issued, dazufailed/cancelledaus jedem Nicht-Endzustand. Kein ZurĂŒck, kein Ăberspringen. - Herkunft je Ereignis (
self_reported,registrar_reported,dns_observed), nie umgerechnet. Regel ĂŒber den Entwurf hinaus:
validatedverlangtregistrar_reportedoderdns_observed, eine Selbstmeldung reicht nicht.issuedverlangt ein Zertifikat,
das zur CSR passt (gleicher öffentlicher SchlĂŒssel); Fingerabdruck und Laufzeit werden aus dem Zertifikat gelesen, nicht ĂŒbernommen. - Kein privater SchlĂŒssel, nirgends: Eingaben mit
PRIVATE KEYwerden verworfen, es gibt keine Spalte dafĂŒr, ein Test sucht in
database/*.sqlund in der angelegten DB nach Spaltennamen mitpriv/key_pem. RSA unter 2048 Bit wird abgelehnt (EC geht). - NebenlÀufigkeit: Jeder Wechsel lÀuft in einer Transaktion mit bedingtem
UPDATE ⊠WHERE state = <gelesen>. SQLite:BEGIN IMMEDIATE
(mit normalemBEGINbekam der Verlierer âdatabase is lockedâ mit Exit 1; per Gegenversuch belegt). MariaDB/MySQL:SELECT ⊠FOR UPDATE
auf der Bestellzeile, zusÀtzlich werden Fehler 1020 (Snapshot-Isolation, MariaDB ℠11.6) und 1213 (Deadlock) als Konflikt gemeldet.
Ergebnis in beiden Datenbanken: Bei gleichzeitigem, gleichem Wechsel gewinnt genau einer, der andere bekommt Exit 4. - Mandantengrenze:
OrderStorefragt immer mitgroup_id; eine fremde Bestellung ist âunbekanntâ, nicht âverbotenâ.null(alle Gruppen) nur fĂŒr die CLI. order_eventsist append-only (Trigger in SQLite und MariaDB, in beiden geprĂŒft).ts_localist die lokale Uhr, keine extern bestĂ€tigte Zeit.- Exit-Codes der CLI: 0 ok · 1 fachlicher Fehler · 2 Nutzung · 3 keine Berechtigung · 4 Zustand passt nicht.
- Tests:
tests/test_orders.sh(115). ZusÀtzlich von Hand gegen MariaDB 11 (tlsgate-my, Scratch-DB): Schema zweimal angewendet
(inkl.soap_credentialsaus Schritt 2, bisher nur geschrieben), kompletter Ablauf, 10 WettlÀufe, Trigger. -
Nicht dabei: kein SOAP-Aufruf (Bestellung beim Registrar, Statusabfrage: kommt mit
order tick), keine UI, keine DNS-Auflösung.
--evidence registrar_reportedper CLI ist die Angabe des Operators, nicht von TLS-Gate geprĂŒft (die CLI sagt das und das Ereignis nennt den Akteur).
Die MariaDB-LĂ€ufe stehen nicht in der Test-Suite (brauchen einen Container). -
order_tickfĂŒr den Cron (Schritt 5 ausdocs/DESIGN_TENANTS.md):bin/order_tick.php,TlsGate\Orders\Tick\*.
Je offener Bestellung höchstens ein Schritt, nur lesende Abfragen beim Registrar (sslInfo) und im DNS (CNAME). - Bestellt nie. Einreichen beim Registrar kostet Geld und bleibt ein Operator-Schritt. Im Portal bestellte AuftrÀge werden
gepollt, sobald die Referenz eingetragen ist: neuer Befehlorder_cli.php order ref <id> <registrar_ref>(ĂŒberschreibt nie). - Ports mit zwei Varianten:
SoapRegistrar/SystemDns(live) undFixtureRegistrar/FixtureDns(Dummy- oder aufgezeichnete
Daten). Die Abbildung einersslInfo-Antwort steht an einer Stelle (RegistrarStatus::fromSslInfo), Unbekanntes bleibt âpendingâ. - Neue Herkunft
fixture(database/order_states.jsonâ Version 2). Beim ersten Demo-Lauf trugen Fixture-Aussagen
registrar_reported/dns_observed: eine stille Aufwertung von Dummy-Daten. Jetzt nennt jeder Port seine Herkunft selbst, und
--fixturesverlangtTLSGATE_MOCK=1. Per Gegenversuch belegt, dass der Test rot wird, wenn ein Fixture-Port wiederregistrar_reportedmeldet. - Frische in neuer Tabelle
order_checks(letzte PrĂŒfung, Ergebnis, seit wann gleich). Ein Ereignis inorder_eventsgibt es nur bei
neuer Information oder einem Wechsel: 5 gleiche LĂ€ufe erzeugen kein Ereignis. Das ist eine eigene Speicherentscheidung, keine Folge von âWiederholung â BestĂ€tigungâ. annotate: neue Information ohne Zustandswechsel (Registrar nennt die Challenge) als Info-Ereignis mitfrom = to. Gespeicherte Werte werden nie ĂŒberschrieben.- Fehler einer Bestellung (
error) oder ein Konflikt (z. B. Zertifikat passt nicht zur CSR) stoppen den Lauf nicht und bewegen nichts.
flockgegen ĂŒberlappende LĂ€ufe auf einem Host, zwischen Nodes schĂŒtzt das bedingteUPDATEje Bestellung.--dry-runschreibt nichts. - Guard-Klasse
servicehat jetzt ihren ersten Verbraucher: Der Dienst-User (TLSGATE_SERVICE_USER) darforder_tick, aber nichtorder_cli. Scharf getestet. - Dummy-Fixtures unter
resources/fixtures/tick/(nurexample.org/example.net, kein privater SchlĂŒssel) mit README zum Austauschen.
Neuer Befehlsoap_cli.php record <orderNumber>gibt die rohesslInfo-Antwort in genau dieser Form aus. - Fix: Der Doktor erwartete genau die vier Tabellen von v2.1 und meldete deshalb seit
soap_credentialsfĂ€lschlich âSchema unvollstĂ€ndigâ.
Jetzt prĂŒft er, dass alle erwarteten Tabellen da sind, und warnt, wennorder_eventsnoch die alte CHECK-Regel ohnefixturehat. - Dev-DBs aus Schritt 4:
CREATE TABLE IF NOT EXISTSĂ€ndert die CHECK-Regel vonorder_eventsnicht.order_tick --fixturesbricht dort sichtbar ab
(Transaktion zurĂŒckgerollt, kein Wechsel). Abhilfe:migrate(Migrator, siehe oben). -
Tests:
tests/test_tick.sh(71). Nicht belegt: ein Lauf gegen den echten Registrar. Offen sind die echte Form vonvalidationDNS
und die Werte vonstatus. Der Live-Pfad ist mit einem Test-Port nachgestellt,SoapRegistrarnur gegen einen toten Host geprĂŒft. -
Export, Monitor und Node-CA (Schritt 6 aus
docs/DESIGN_TENANTS.md). Durchgespielt mitdigdafunk.de:docs/LAUF_NODE_CA.md. bin/export.php(TlsGate\Export\MonitorExport) schreibt den Vertrag, denresources/mock/sample.htmlliest, plus Herkunft je
Zertifikat (sourcelive | node_ca | fixture, Herkunftskette,public_trust, letzte PrĂŒfung). Ohnesoap_username,api_endpoint,
Zugangsdaten, CSR oder SchlĂŒssel. Atomar (Temp-Datei, fsync, rename, 0644).--groupoder ausdrĂŒcklich--all,--seal-toversiegelt
fĂŒr einen EmpfĂ€nger (sodium_crypto_box_seal). Guard-Klasseservice: lĂ€uft auch im Cron des Dienst-Users.--probe(TlsGate\Export\TlsProbe): AuĂensicht je Bestellung, von dieser Node aus:served_match,served_other,no_tls,unreachable.sample.htmlwird zum Monitor:?data=<datei>.jsonlĂ€dt einen Export. Ein Banner nennt Quelle, Zeit und Standort. Zwei neue
Spalten: Herkunft und AuĂensicht. Fix XSS: Die Tabelle wurde perinnerHTMLaus den Daten gebaut, ein Common Name wie
<img onerror=âŠ>aus einer fremden CSR wĂ€re als Skript gelaufen. Jetzt nurtextContent, im Browser belegt (headless Chrome).
Fix stiller Mock: Scheitertefetch, zeigte die Seite drei erfundene Zertifikate. Im Monitor-Modus steht jetzt âKEINE DATENâ,
nur die Sandbox ohne?data=zeigt Beispiele, laut als MOCK gekennzeichnet. Ebenso die Produktliste: ohne Produkte im Export stand dort festle-free-01/sec-dv-03, jetzt âkeine Produktliste in dieser Quelleâ...imdata-Pfad wird abgelehnt.- Node-CA (
bin/node_ca.php,TlsGate\Orders\Tick\NodeCa): eigene Test-CA der Node (EC P-256), nicht öffentlich vertrauenswĂŒrdig.
SchlĂŒssel versiegelt mit dem WurzelschlĂŒssel (neuer Zweckca), gebunden an den Fingerabdruck vonca.crt.submitnennt den CNAME,
order_tick --node-castellt erst aus, wenn TLS-Gate ihn selbst gesehen hat. Neue Herkunftnode_ca(Vertrag v3); sie reicht fĂŒr
validatednicht, die Node-CA validiert also nie sich selbst. Blatt: 90 Tage, SAN aus der Bestellung,CA:FALSE, serverAuth/clientAuth. - Ports haben jetzt
handles(): Ein Node-CA-Lauf fasst Registrar-Bestellungen nicht an und umgekehrt. - Beobachtung
digdafunk.de(von einem Mac, 29.09.)::443offen, TLS-Handshake mit Alert 40 abgelehnt, kein Zertifikat;:80Apache 200.
Der echte Lauf endet richtig beiawaiting_dns(CNAME nicht gesetzt). Auf einer Kopie mit DNS-Fixture bisissuedgespielt:
openssl verifygegen die Node-CA OK, AuĂensicht weiterno_tls. Ausgestellt â ausgeliefert. - Tests:
tests/test_export.sh(71, inkl. lokaler TLS-/TCP-Server fĂŒr die Probe und Browser-PrĂŒfung, falls Chrome da ist).
Gegenversuch: Mit der altensample.htmlwerden 7 PrĂŒfungen rot, darunter der XSS-Test im Browser. -
Nicht belegt: Probe gegen fremde Hosts in der Suite (nur die Handbeobachtung oben), Stromausfall zwischen fsync und rename,
~~Widerruf der Node-CA~~ (inzwischen CRL, siehe âWiderruf fĂŒr die Node-CAâ), kein OCSP. Node-CA-Bestellungen brauchen noch einen SOAP-Zugang der Gruppe (credential_idist Pflicht). -
Sensor-IdentitÀt per mTLS (Dev-Test):
bin/cert_whois.phpundTlsGate\Orders\CertIdentitybeantworten aus einem vorgelegten Zertifikat,
wohin es gehört: Aussteller (Node-CA?), Bestellung, Objekt, Gruppe, laut Inventar. Urteile:known,foreign_ca,unknown_cert,expired,
not_yet_valid,not_issued,invalid. Nimmt nie einen privaten SchlĂŒssel an; jedes Nachschlagen steht im Audit. tests/test_sensor.sh(28): Ein virtueller Sensor erzeugt seinen Dev-SchlĂŒssel selbst, bekommt ein Client-Zertifikat der Node-CA
(DNS per Fixture, Herkunftfixture) und meldet sich per mTLS an einem Test-Gateway. Erkannt alssensor/sensor-17in Gruppebwa,
Ort laut Inventarpv-container/batterie. Abgewiesen: ohne Zertifikat, selbstsigniert, fremde CA, echtes Zertifikat mit falschem SchlĂŒssel.
Gegenversuch mit gelockertem Gateway (verify_peeraus): 7 rote PrĂŒfungen.- Grenze, ausdrĂŒcklich getestet: Eine Kopie von Zertifikat und SchlĂŒssel ist fĂŒr das Gateway derselbe Sensor. Gegenmittel: Widerruf (siehe unten).
- Beobachtung: Unter TLS 1.3 ist der Handshake fĂŒr den Client fertig, bevor der Server ein fehlendes Client-Zertifikat ablehnt. Abgewiesen
heiĂt deshalb âkeine Antwort vom Gatewayâ, nicht âVerbindung scheitertâ. -
docs/SECURITY.md: neuer Abschnitt âWer darf was lesen, was wird protokolliert, was schĂŒtzt die Outboxâ. Lesen des Exports wird nicht protokolliert. -
Widerruf fĂŒr die Node-CA, âselbst ausgestelltâ gegen âgekauftâ (Vertrag v4: neuer Zustand
revoked,issued â revoked). node_ca.php revoke <id> <grund>: nur fĂŒr Zertifikate, die diese Node-CA signiert hat (PrĂŒfung per Signatur gegenca.crt, nicht per Namen).
Gekaufte Zertifikate lehnt TLS-Gate ab: die widerruft der Registrar. Neue Tabellecert_revocations(append-only, beide Datenbanken).
Reihenfolge: erst der Eintrag (Quelle der CRL), dann der Zustandswechsel. Scheitert der, bleibt der Widerruf: im Zweifel widerrufen.node_ca.php crl [--days N]: echte X.509-CRL (node-ca/crl.pem, 0644) mitnextUpdateund fortlaufender Nummer, fĂŒrnginx ssl_crl,
openssl verify -crl_checkoder einen Zero ohne Verbindung. PHP kann keine CRL signieren, deshalb dieopenssl-CLI. Den CA-SchlĂŒssel bekommt
sie ĂŒber eine Pipe (/dev/fd/3), nie als Datei.- âNix ist nullâ, belegt mit
openssl verify: vor der neuen CRL gilt der widerrufene Sensor noch als gĂŒltig (Widerruf wirkt erst mit
Verteilung); ohne CRL-Datei lehnt-crl_checkab; nachnextUpdatelehnt openssl auch ein gĂŒltiges Zertifikat ab (âCRL has expiredâ).
Ein unbekannter Widerrufsstand ist nie âgĂŒltigâ. - âSelbst ausgestelltâ gegen âgekauftâ:
issuer_kind(node_ca|extern|fixture) incert_whois, Export und Monitor.CertIdentitykennt
gekaufte Zertifikate aus dem Inventar jetzt alsknown(vorherforeign_ca), mit dem Hinweis, dass die Kette zur öffentlichen CA hier nicht geprĂŒft wird.
Neues Urteilrevoked, auch incert_whois(Exit 1). - Wildcard
*.schule.xyz: Die Challenge liegt an der Basis-Domain (_tlsgate-dcv.schule.xyz). Vorher wĂ€re_tlsgate-dcv.*.schule.xyzentstanden, kein gĂŒltiger DNS-Name. - Monitor:
REVOKED (Grund)rot, Spalte Herkunft nennt âselbst ausgestelltâ oder âgekauft/externâ. - Tests:
tests/test_revoke.sh(41), Sensor-Test erweitert (32: nach Widerruf meldet das Gateway Kopie und Original alsrevoked).
Gegenversuche: ohne Aussteller-PrĂŒfung und mit leerer CRL werden jeweils die passenden PrĂŒfungen rot. Von Hand auf MariaDB 11. - Dev-DBs aus Schritt 4 bis 6: Die CHECK-Regel von
cert_orderskenntrevokednicht,revokebricht dort ab. Behoben durch den Migrator (siehe oben). -
Nicht dabei: OCSP, automatisches Verteilen der CRL, Widerruf gekaufter Zertifikate ĂŒber SOAP.
-
Migrator (
src/Storage/Migrator.php,user_cli.php migrate [--dry-run|--status]). Aufgebaut âvon nullâ mit dem jetzigen Stand
als Basis: Version 1 =basis-v2.2, vermerkt in der neuen Tabelleschema_migrations(append-only).schema*.sqlbeschreibt immer den
neuesten Stand; spĂ€tere Ănderungen kommen alsdatabase/migrations/NNNN_name.{sqlite,mysql}.sql(Regeln:database/migrations/README.md). - SQLite gleicht selbst an: Das Soll-Schema entsteht in einer In-Memory-DB aus
schema.sql. Abweichende Tabellen (die alten CHECK-Regeln
ohnefixture,node_ca,revoked) werden nach dem SQLite-Verfahren umgebaut, Indizes und Trigger neu angelegt. Alles in einer Transaktion,
FremdschlĂŒssel geprĂŒft, am Ende Kontrolle âkein Unterschied mehr zum Sollâ, sonst Rollback. Vorher Sicherung perVACUUM INTO(0600, eindeutiger Name). - Reihenfolge: Migrationsdateien laufen vor dem Angleichen (sehen den alten Stand, dĂŒrfen Daten umformen, mĂŒssen idempotent sein).
Auf einer frischen DB laufen sie nicht, sie werden als âin schema.sql enthaltenâ vermerkt. - Abbruch statt Raten: Spalten, die im Soll fehlen (Daten gingen verloren), verletzte FremdschlĂŒssel, fehlerhafte Dateien â nichts geĂ€ndert.
Gegenversuch: ohne diesen Schutz hÀtte der Umbau eine fremde Spalteprivate_key_pemstill verworfen. - MySQL/MariaDB: fehlende Tabellen, Dateien, Version. Veraltete CHECK-Regeln werden erkannt (
information_schema) und fĂŒhren zum Abbruch mit Hinweis. - Doktor: meldet âSchema-Version N (aktuell)â oder âohne Versionsangabeâ statt der Einzelwarnungen zu CHECK-Regeln und legt dabei nichts an.
- Fix unterwegs: Zwei Migrationen in derselben Sekunde scheiterten, weil der Name der Sicherung nur sekundengenau war.
- Tests:
tests/test_migrate.sh(94). Jeder Schema-Stand aus der Git-Historie (28f2211, 13bf68b, 4e298ca, 6950951, 766274d, f18d59e) mit Daten:
Daten erhalten (PrĂŒfsumme je Tabelle), Schema gleich frisch (Tabellen, Indizes, Trigger), Append-only greift wieder, Sicherung = alter Stand.
Dazu AbbrĂŒche, Migrationsdateien genau einmal, MariaDB (fallstlsgate-mylĂ€uft). Gegenversuch âUmbau ohne Datenkopieâ: 5 rote PrĂŒfungen. - Damit erledigt: Die Hinweise âDev-DB neu anlegenâ weiter oben gelten nicht mehr.
php bin/user_cli.php migratebringt jede DB aus diesen StÀnden hoch.
v2.1.0 â 2026-09-28 · Phase 2: SSL-Konsole entflochten
Keine neue Funktion. Die Seite rendert bis auf eingebundenes CSS/JS identisch; geprĂŒft wurde das per
HTML-Vergleich alt gegen neu fĂŒr GET und alle Aktionen.
public/index.phpist von 1054 auf 37 Zeilen geschrumpft und dient nur noch als Einstieg: Zugang, Controller, Rendern.TlsGate\Http\SslController: RollenprĂŒfung, Audit, Pflicht-Grund, Aufruf der Aktion und Laden der
Anzeigedaten (Liste mit Restlaufzeit, Produkte, Software). Heraus kommt ein reines Array.TlsGate\Http\View+templates/(auĂerhalb des Webroots):layout.php,ssl/page.php, je eine
Ergebnis-Karte und ein Reiter als eigenes Template. Templates rufen nichts selbst auf.public/assets/tlsgate.cssundpublic/assets/ssl.js: Die 18 Inline-onclick/oninputsind durch
data-*-Attribute mit Event-Delegation ersetzt.- Strikte CSP auch fĂŒr die SSL-Konsole:
script-src 'self', keine Inline-Skripte mehr. Damit ist ein
offener Punkt ausdocs/SECURITY.md1c erledigt. - Tests:
tests/test_auth.sh71 (neu: CSP, kein Inline-JS, Assets erreichbar,src/Httpundtemplates/nicht im Web). - Fix:
.gitignorebindetstate/undSSL/jetzt an den Repo-Root (/state/,/SSL/). Auf macOS
(case-insensitive) hatSSL/sonst auchtemplates/ssl/still ausgeblendet. - Fix: flackernder Replay-Check in
tests/test_auth.sh. Er rechnete den âbenutztenâ TOTP-Code neu aus, statt
ihn wiederzuverwenden, und wurde deshalb rot, wenn der Lauf ĂŒber eine 30-s-Grenze fiel (Folgefehler: IP-Sperre).
Der Fehler steckte schon in v2.0.0 und ist jetzt per Start an der Fenstergrenze nachgewiesen und behoben.
v2.0.0 â 2026-09-28 · âCore mit Login, sauber verpackt"
Die erste v2. Aus dem SSL-Werkzeug wird der Anfang des Mandanten-Cores (docs/ARCHITEKTUR.md):
Login-Pflicht, Audit und Web-Verwaltung, und das Ganze in einer Struktur, die sich sauber auf einen
Produktionshost bringen lÀsst.
Nicht rĂŒckwĂ€rtskompatibel zu v1:
- Web-UI nur noch mit Anmeldung (Passwort + TOTP). Ohne DB antwortet sie mit 503 (fail closed).
- DocumentRoot ist public/ statt Repo-Root. Der vhost muss umgestellt werden (deploy/).
- Befehle liegen unter bin/: php bin/user_cli.php, php bin/soap_cli.php, bin/dashboard.sh.
- sample.json/api.php sind nicht mehr per Web erreichbar (resources/mock/).
Login & Audit (M1)
- Login mit Passwort (Argon2id, â„ 15 Zeichen) und TOTP-Pflicht (RFC 6238, mit Testvektoren geprĂŒft),
Gruppen (eigen/reseller) und Rollen (readonly<user<group_admin<admin). - Sperren pro Konto und pro IP, Replay-Schutz fĂŒr TOTP, CSRF auf jedem Formular, Sicherheits-Header.
Session-Cookie__Host-,SameSite=Strict. audit_logper Trigger nur anhÀngbar. Jede SSL-Aktion und jede VerwaltungsÀnderung wird protokolliert.- SQLite in
TLS_GATE_STATE_DIR, TOTP-Secrets mit sodium verschlĂŒsselt. bin/user_cli.php:migrate, Gruppen, Benutzer,enroll,code,audit.
Web-Verwaltung
users.php: Benutzer und Gruppen verwalten (nur admin). Man kann sich nicht selbst aussperren,
und der letzte Admin ist geschĂŒtzt.auditlog.php: Audit-Ansicht mit Filtern (nur admin).- Pflichtfeld âGrund/Zweck" bei SSL-Bestellungen. Der Grund landet im
audit_logund wird nicht an die CA geschickt.
Struktur & Betrieb
- PSR-4:
TlsGate\âsrc/(Auth, Audit, Storage, Ssl, Config), Autoloader inbootstrap.php
ohne Composer, dazucomposer.jsonmit derselben Zuordnung. public/als einziger Webroot.router.phpist jetzt eine Whitelist statt einer Deny-Liste.TlsGate\Config: Pfade und Umgebungsvariablen an einer Stelle (ersetzt die$GLOBALS-Credentials).- Vorlagen fĂŒr Apache und nginx sowie Hinweise pro Betriebssystem in
deploy/. bin/TLS-doktor.bash: Umgebungs-Diagnose je PHP-Binary und Web-User.bin/serve.sh: Dev-Server, der vom Terminal losgelöst lÀuft.- Tests:
tests/test_auth.sh(66),tests/test_smoke.sh(44),tests/test_totp_rfc6238.sh(11).
SOAP / Terminal
soap_cli.php probe(M0): Live-PrĂŒfung vonlevel/forReseller, nur lesend.
v1 â 2026-07-17 ⊠2026-09-24 · SSL-Werkzeug ohne Login
Stand bis Commit 3dec8a8 (vor 6d75485, âLogin fĂŒr das TLS-Frontend"). LĂ€uft als v1 in der
Test-VM. Flaches Layout: Repo-Root ist gleichzeitig Webroot, kein Login.
index.php: SOAP-Portal (Liste mit Expire, Suche, CSR/Key, Bestellen, VerlĂ€ngern ĂŒbersslReplace,
Authcode, Besitzerdaten, DCV per DNS/HTTP, Cross-Check), Mock-Fallback aufsample.json.SslHandler.php: gemeinsamer SOAP-Kern fĂŒr Web und CLI.soap_cli.php: JSON-BrĂŒcke.dashboard.sh: Terminal-Dashboard, OpenSSL-Toolbox,deploymit Match- und Chain-PrĂŒfung,
Stammdaten-Layerstate/state.json+state/actions.log.getRenewability: Renewal-Fenster aus dem Produktparameter.sample.html: Offline-Visualizer mit Anzeige der rohen SOAP-Pakete.- Audit-Findings BWA-AUD-01 ⊠15 (docs/AUDIT.md).