Changelog
Versionen nach SemVer. Die aktuelle Version steht in VERSION.
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).