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 migrate vergaß, bekam bei der Anmeldung eine leere Seite (HTTP 200, Fatal no such column: g.mfa_policy nur im Log). bootstrap.php fĂ€ngt im Web
    fehlende Spalten/Tabellen (SQLite-Text, MySQL 42S22/42S02) ab und antwortet 503 — 
 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_vm p3 hat das gefunden). Merke: nach jedem Update php 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 (Default code). Bei qr erzeugt jeder Passwort-Login ein neues Secret + QR, also auch nach Abmelden und Session-Ablauf; bei code gilt nach der Einrichtung nur der Code der App.
    Setzen: user_cli.php group mfa <slug> <code|qr> oder in users.php (Spalte MFA). group list zeigt den Modus. Der Support-Weg user code / reset-mfa auf der Bash bleibt.
    Sicherheit: wie bei TLSGATE_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_policy nur fĂŒr MySQL/MariaDB (idempotent ĂŒber information_schema); SQLite gleicht groups selbst an. tests/test_mfa.sh 39 (+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=0 bis zur BestĂ€tigung)
    und zeigt das QR; erst der Code des neuen Secrets meldet an. Das alte Secret ist sofort ungĂŒltig, totp_last_step wird 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.sh 28 (+4).
  • docker_vm: instance.sh <name> [mock] [qrlogin] setzt die Variable im Pool (env[TLSGATE_MFA_QR_EVERY_LOGIN] = 1). Ohne qrlogin entfĂ€llt sie beim nĂ€chsten Lauf wieder. Bewusst nicht
    fest im Pool-Template: die Abnahmen (run.sh, p3–p6) melden sich mit user code an und brauchen das bestehende Secret. Im Container von Hand belegt: zwei Logins, je neues Secret, 302, danach mfa.
    Nicht automatisiert abgesichert: die Option qrlogin in instance.sh (kein eigener Test in docker_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 letzten auth.*-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 add und user add mit vorhandenem Slug bzw. vorhandener E-Mail (bin/user_cli.php): Vorab-PrĂŒfung, Exit 1, „
 gibt es schon 
“; bei user add vor der Passwortabfrage.
    ZusĂ€tzlich wird SQLSTATE 23000 aus dem INSERT (Wettlauf) abgefangen. Die WeboberflĂ€che (users.php) prĂŒfte schon vorab. tests/test_auth.sh 76 (+3, Gegenprobe: mit dem alten
    user_cli.php scheitern die zwei MeldungsprĂŒfungen), docker_vm/tests/p3.sh +2. Nicht einzeln belegt: die catch-Zweige fĂŒr den Wettlauf.

  • Fix: cred add mit vorhandenem Label zeigt einen Satz statt der PDOException. CredentialStore::add() prĂŒft vorab und fĂ€ngt zusĂ€tzlich SQLSTATE 23000 aus dem INSERT (Wettlauf);
    die Meldung nennt den Weg (replace bzw. „Zugangsdaten ersetzen“) und gilt fĂŒr CLI und Web (credentials.php zeigte vorher den rohen SQL-Text). tests/test_credentials.sh 62 (+3),
    docker_vm/tests/p3.sh 46 (+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 bleibt group add mit vorhandenem Slug.
    Auch in native_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> fake schrieb env[TLSGATE_SOAP_HOSTS] = soap.fake.test in den Pool;
    CredentialStore::check() lehnte damit jede andere WSDL ab. Der Modus fake ist entfernt (instance.sh kennt nur noch mock), ebenso die fest eingetragenen
    Fake-Zugangsdaten in der .profile der Instanz. TLSGATE_SOAP_HOSTS bleibt eine bewusste, manuelle Betriebsoption. Der Fake-Registrar im Container bleibt als gewoehnliches
    Test-Ziel; p3.sh legt seinen Zugang selbst an und belegt, dass ein Zugang mit fremdem WSDL-Host angelegt werden kann (45 Pruefungen). p4.sh reicht die Fake-Zugangsdaten
    nur fuer den dashboard.sh-Aufruf mit. Bestehende Instanzen verlieren die Einschraenkung beim naechsten instance.sh <name>.
  • Neu: Deploy-ZIP. In der Detailansicht (getOrder) der Knopf „Deploy-ZIP“: public/outbox.php holt sslInfo mit dem Zugang der eigenen Gruppe und liefert
    <domain>.zip mit <domain>/<domain>.cert und <domain>/<domain>.chain.pem (dieselben Dateien wie dashboard.sh deploy, native_bash_cli). Nie ein Key: TLS-Gate besitzt keinen,
    ein PEM mit „PRIVATE KEY“ bricht ab. Dieselben Bytes liegen in der ssl_outbox (Config::outboxDir(), Standard <state>/ssl_outbox/<gruppe>/<domain>.zip, Ordner 0700, Datei 0640,
    umlenkbar mit TLSGATE_OUTBOX_DIR) zum Einsammeln durch die Orchestrierung. Ab Rolle user, CSRF, Audit ssl.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) und docker_vm/tests/p6.sh (22 Pruefungen gegen die Instanz). Auch in native_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) liess install -d /root/.provision/ssh aus; nach down -v scheiterte docker cp des Schluessels mit „Could not find the file“, und instance.sh ozm01 fake gibt es nicht mehr.
  • Referenz-Stand (frisch nach down -v): run.sh 52, p3.sh 45, p4.sh 54, p5.sh 31, p6.sh 22.

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 und otpauth://-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 nur user reset-mfa per 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 in docker_vm/.
  • Fix: docker_vm/tests/p4.sh war locale-abhĂ€ngig. Das Muster ^G.ltig erwartet, dass . das ĂŒ trifft; unter LANG=C sind es zwei Bytes. Jetzt ^G.{1,2}ltig. Die Ausgabe von dashboard.sh war nie falsch.
  • Referenz-Stand (frisch gebaute VM nach down -v): run.sh 52, p3.sh 43, p4.sh 54, p5.sh 31 PrĂŒfungen grĂŒn.
  • .codebase-memory/graph.db.zst wird 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
    (stdClass in domain, 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-mbstring ließ die Detailansicht mit einem fatalen Fehler ausfallen (nicht nur den Fehlerpfad: SslController ruft mb_substr im Normalfall). bootstrap.php
    meldet fehlendes mbstring/sodium jetzt sofort mit HTTP 503 und dem Paketnamen (auch im Log, CLI: Exit 1); composer.json verlangt ext-mbstring; TLS-doktor.bash prĂŒft es.
  • Fix: TLS-doktor.bash brach unter GNU/Linux ab und kannte nur SQLite. perm()/owner()/fgrp() riefen zuerst BSD-stat -f, das unter GNU „gelingt“ (Dateisystem-Info statt Rechte; Abbruch unbound variable). Jetzt Weiche per stat --version. Neu: Treiber TLSGATE_DB_DRIVER=sqlite|mysql (bei mysql pdo_mysql Pflicht, Schema und Version ĂŒber Db::pdo()/Migrator, keine DB-Datei-PrĂŒfungen; SQLite bleibt der Offline-Dev-Standard), WEB_USER fĂŒr den php-fpm-Pool-Benutzer, LesbarkeitsprĂŒfung als root per runuser. In bin/ und native_php/bin/. docker_vm/tests/p5.sh (31 PrĂŒfungen: Sollstand, root.key 0644, falsches DB-Passwort, MariaDB gestoppt, state/-Besitz, Guard aus, Pool-Datei). Neu: Pool-Check — der Doktor liest per php-fpm -tt den Pool des Webserver-Benutzers und prĂŒft disable_functions (AUD-33), clear_env, open_basedir, display_errors.
  • Fix: dashboard.sh zeigte gegen Live-SOAP rohe Unix-Zeitstempel (ohne Resttage und Farbe): days_left() verstand nur ISO, die API liefert Unix-Sekunden (0 = kein Datum). Neu to_epoch/fmt_ts (GNU und BSD date), eingesetzt in Liste, Details und Cross-Check, in bin/dashboard.sh und native_bash_cli/. tests/test_dates.sh (16 PrĂŒfungen).
  • docker_vm/ P4: ozm02, ssh nur mit SchlĂŒssel (Port 2222, Instanz-Shell mit dashboard.sh gegen den Fake-Registrar), tests/p4.sh (52 PrĂŒfungen: Isolation, Guard, getrennte Logs, Reseller-Sperre, ssh). Bekannt, nicht behoben: dashboard.sh zeigt gegen Live-SOAP rohe Unix-Zeitstempel (siehe docker_vm/PLAN.md 5c).
  • docker_vm/: Fake-Registrar (fake-soap/: WSDL, Szenarien je Order-Nummer, Fake-CA, nur im Container) und tests/p3.sh (43 PrĂŒfungen). Reproduziert den 500 der Node mit dem alten View.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 
 RETURNING kennt nur SQLite. Bei TLSGATE_DB_DRIVER=mysql
    warf jeder Fehlversuch (falsches Passwort, falscher TOTP-Code) bei einem bestehenden Konto einen PDOException und damit einen 500; unbekannte Konten kamen durch.
    Jetzt UPDATE, dann SELECT (portabel). Neu tests/test_sql_portable.sh (statische PrĂŒfung auf SQLite-only-SQL). Gefunden mit docker_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 als stdClass. (string) $objekt wirft in PHP 8 Error: Object of class stdClass could not be converted to string,
    und copyDt() hatte das <dt> schon ausgegeben — die Seite brach mitten im Satz ab (RZ-Fund 1.10., Gegenprobe: die drei echo in copyDt auskommentiert).
    Jetzt werden Listen und Objekte gleich flach gelesen (Werte kommagetrennt, __toString als 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.sh 21 PrĂŒfungen.
  • bin/dashboard-ro.sh + bin/ro_cli.php: rein lesender Abruf (list, info, info --file, raw). Zugang aus TLSGATE_RO_WSDL/_USER/_PASS,
    keine DB, kein Guard; nur sslListExtended und sslInfo (mit getCSR=1). info wertet 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 (seit notValidBefore) 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 Stand 3dec8a8 vom 24.9., vor Login/DB) und native_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: doppelte BWA-AUD-30 bereinigt — der Guard-Besitzerweg ist jetzt BWA-AUD-33, neu BWA-AUD-32 (Ausnahme zu AUD-25 fĂŒr dashboard-ro.sh).
  • CLI-Guard: Besitzer der Instanz (Guard::instanceOwner): Neben root/sudo darf die bin/-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 Klasse service) bleiben draußen.
    Akteur im Audit ist der echte User, SUDO_USER zÀhlt ohne euid 0 weiterhin nie. Grenze steht in docs/DESIGN_TENANTS.md §5
    (gehört dem Web-User alles, ist er auch CLI-Operator). Exit-Codes unverÀndert. tests/test_guard.sh um 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.key an mehreren ĂŒblichen Orten liegt (/etc/tlsgate, /var/lib/tlsgate, state/), und prĂŒft, ob der Web-User
    ihn lesen kann (nur mit passwortlosem sudo). Der Behebungstext empfahl sudo -u <WEBUSER> 
 migrate, das der Guard ablehnt: jetzt sudo 
 migrate, danach chown.
  • Doku: deploy/apache-vhost.conf.example sagt dasselbe wie deploy/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 unter state-demo/ (gitignored, fasst state/ nicht an).
    Mensch mit SchlĂŒssel + CSR, Node-CA, zwei Bestellungen, Tick-LĂ€ufe, CRL, Export mit echter Außensicht, sample.html daneben.
    Die Validierung der ersten Bestellung kommt aus einer DNS-Fixture (Herkunft fixture), die zweite bleibt beim echten DNS stehen.

  • order_tick.php --node-ca --dns-fixture <datei>: CNAMEs aus einer Datei statt echtem DNS, nur mit TLSGATE_MOCK=1, Herkunft fixture.
    Getestet in tests/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ĂŒr admin und group_admin.
  • group_admin verwaltet nur die ZugĂ€nge der eigenen Gruppe, admin alle. GeprĂŒft je Aktion am Datensatz: Eine gefĂ€lschte cid einer fremden
    Gruppe ergibt „Unbekannter Zugang“ (verrĂ€t die Existenz nicht) und steht als denied im Audit. Eine gefĂ€lschte group_id beim Anlegen landet
    in der eigenen Gruppe. Gegenversuch ohne diese PrĂŒfung: 4 rote PrĂŒfungen.
  • Anlegen, Zugangsdaten ersetzen, an/aus, „Live testen“: sslListExtended lesend, 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\Qr ist 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.
    --watch zeigt 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 aus docs/DESIGN_TENANTS.md): ein SchlĂŒssel
    (TLSGATE_ROOT_KEY oder TLSGATE_ROOT_KEY_FILE, Standard state/root.key, 0400), getrennte UnterschlĂŒssel je Zweck,
    XChaCha20-Poly1305 mit Bindung an den Datensatz. Blob tg1: + base64(version | key_id | nonce | ct).

  • TOTP-Secrets nutzen ihn, gebunden an die User-ID. Totp::encrypt/decrypt bekommen dafĂŒr die User-ID.
  • user_cli.php key status | rotate | migrate. Altformat-Blobs (secretbox, tlsgate.key) bleiben lesbar, bis key migrate lief.
  • migrate legt root.key an, aber keine tlsgate.key mehr. 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 (legt root.key an), dann key migrate
    mit noch gesetztem TLSGATE_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): Tabelle soap_credentials
    (database/schema*.sql, idempotent per migrate), Security\CredentialStore, Wertobjekt Security\SoapCredential.
    Blob mit dem WurzelschlĂŒssel versiegelt (Zweck soap), gebunden an Gruppe + Label. Wird bei key rotate/key migrate mitgenommen.

  • user_cli.php cred add | list | replace | enable | disable | test. Benutzer/Passwort werden abgefragt, nie als Argument.
    WSDL nur https:// ohne eingebettete Zugangsdaten, optional eingeschrÀnkt durch TLSGATE_SOAP_HOSTS.
  • Breaking: Config::soap() und BWA_SOAP_USER/PASS/WSDL sind entfernt. Web: Zugang der Gruppe des angemeldeten Users.
    CLI/Bash: TLSGATE_GROUP=<slug> (optional TLSGATE_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.php endet mit Exit 1. Beispieldaten nur mit TLSGATE_MOCK=1. Das gilt auch fĂŒr
    AuthCode und Besitzerdaten (vorher erfundene Werte). SslHandler bekommt den Zugang als Konstruktorargument.
  • Tests: tests/test_credentials.sh (59), neuer Check in tests/test_auth.sh; tests/test_smoke.sh setzt TLSGATE_MOCK=1 ausdrĂŒcklich.
  • Umstellung: php bin/user_cli.php migrate (legt die Tabelle an), cred add, cred test. Danach BWA_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 aus docs/DESIGN_TENANTS.md): bin/user_cli.php und bin/soap_cli.php verlangen root
    oder sudo (Exit 3, es wird nichts geschrieben). Bei sudo muss SUDO_USER in einer Operator-Gruppe sein
    (sudo, wheel, admin; TLSGATE_OPERATOR_GROUPS). SUDO_USER allein reicht nie. Der Web-Prozess kommt nie durch.

  • Breaking: Diese Werkzeuge (und damit dashboard.sh) brauchen sudo. Dev/Tests: TLSGATE_GUARD=off (nur exakt off).
    Das steht im Audit ("guard":"off"), der Doktor warnt, und am Terminal erscheint eine Warnzeile (nicht bei umgeleitetem stderr, weil dashboard.sh es mit dem JSON auswertet).
  • Jeder CLI-Audit-Eintrag trĂ€gt den Akteur ("actor":"cli:alice"); cred_add schreibt ihn nach created_by.
  • Deploy: migrate, group add, user add, cred add laufen jetzt als root/sudo, nicht mehr als sudo -u <WEBUSER> (das wird abgelehnt),
    danach chown -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Ă€teren order tick vorbereitet, noch ohne Verbraucher.
  • GeĂ€ndert: soap_cli.php meldet „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 posix und warnt bei TLSGATE_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.sh fĂŒhrt den root-Zweig aus). Alle anderen Tests setzen TLSGATE_GUARD=off ausdrĂŒcklich.

  • Inventar und Bestellzustand (Schritt 4 aus docs/DESIGN_TENANTS.md): Tabellen assets, cert_orders, order_events
    (database/schema*.sql, idempotent per migrate), Klassen TlsGate\Orders\{OrderStore, AssetStore, OrderState, EvidenceKind},
    neue CLI bin/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
    OrderState gelesen und ist sprachneutral fĂŒr den Rust-Kern gedacht. Enum und Datei sind per Test deckungsgleich.
    csr_submitted → awaiting_dns → validated → issued, dazu failed/cancelled aus jedem Nicht-Endzustand. Kein ZurĂŒck, kein Überspringen.
  • Herkunft je Ereignis (self_reported, registrar_reported, dns_observed), nie umgerechnet. Regel ĂŒber den Entwurf hinaus:
    validated verlangt registrar_reported oder dns_observed, eine Selbstmeldung reicht nicht. issued verlangt 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 KEY werden verworfen, es gibt keine Spalte dafĂŒr, ein Test sucht in
    database/*.sql und in der angelegten DB nach Spaltennamen mit priv/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 normalem BEGIN bekam 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: OrderStore fragt immer mit group_id; eine fremde Bestellung ist „unbekannt“, nicht „verboten“. null (alle Gruppen) nur fĂŒr die CLI.
  • order_events ist append-only (Trigger in SQLite und MariaDB, in beiden geprĂŒft). ts_local ist 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_credentials aus 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_reported per 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_tick fĂŒr den Cron (Schritt 5 aus docs/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 Befehl order_cli.php order ref <id> <registrar_ref> (ĂŒberschreibt nie).
  • Ports mit zwei Varianten: SoapRegistrar/SystemDns (live) und FixtureRegistrar/FixtureDns (Dummy- oder aufgezeichnete
    Daten). Die Abbildung einer sslInfo-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
    --fixtures verlangt TLSGATE_MOCK=1. Per Gegenversuch belegt, dass der Test rot wird, wenn ein Fixture-Port wieder registrar_reported meldet.
  • Frische in neuer Tabelle order_checks (letzte PrĂŒfung, Ergebnis, seit wann gleich). Ein Ereignis in order_events gibt 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 mit from = 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.
    flock gegen ĂŒberlappende LĂ€ufe auf einem Host, zwischen Nodes schĂŒtzt das bedingte UPDATE je Bestellung. --dry-run schreibt nichts.
  • Guard-Klasse service hat jetzt ihren ersten Verbraucher: Der Dienst-User (TLSGATE_SERVICE_USER) darf order_tick, aber nicht order_cli. Scharf getestet.
  • Dummy-Fixtures unter resources/fixtures/tick/ (nur example.org/example.net, kein privater SchlĂŒssel) mit README zum Austauschen.
    Neuer Befehl soap_cli.php record <orderNumber> gibt die rohe sslInfo-Antwort in genau dieser Form aus.
  • Fix: Der Doktor erwartete genau die vier Tabellen von v2.1 und meldete deshalb seit soap_credentials fĂ€lschlich „Schema unvollstĂ€ndig“.
    Jetzt prĂŒft er, dass alle erwarteten Tabellen da sind, und warnt, wenn order_events noch die alte CHECK-Regel ohne fixture hat.
  • Dev-DBs aus Schritt 4: CREATE TABLE IF NOT EXISTS Ă€ndert die CHECK-Regel von order_events nicht. order_tick --fixtures bricht 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 von validationDNS
    und die Werte von status. Der Live-Pfad ist mit einem Test-Port nachgestellt, SoapRegistrar nur gegen einen toten Host geprĂŒft.

  • Export, Monitor und Node-CA (Schritt 6 aus docs/DESIGN_TENANTS.md). Durchgespielt mit digdafunk.de: docs/LAUF_NODE_CA.md.

  • bin/export.php (TlsGate\Export\MonitorExport) schreibt den Vertrag, den resources/mock/sample.html liest, plus Herkunft je
    Zertifikat (source live | node_ca | fixture, Herkunftskette, public_trust, letzte PrĂŒfung). Ohne soap_username, api_endpoint,
    Zugangsdaten, CSR oder SchlĂŒssel. Atomar (Temp-Datei, fsync, rename, 0644). --group oder ausdrĂŒcklich --all, --seal-to versiegelt
    fĂŒr einen EmpfĂ€nger (sodium_crypto_box_seal). Guard-Klasse service: 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.html wird zum Monitor: ?data=<datei>.json lĂ€dt einen Export. Ein Banner nennt Quelle, Zeit und Standort. Zwei neue
    Spalten: Herkunft und Außensicht. Fix XSS: Die Tabelle wurde per innerHTML aus den Daten gebaut, ein Common Name wie
    <img onerror=
> aus einer fremden CSR wÀre als Skript gelaufen. Jetzt nur textContent, im Browser belegt (headless Chrome).
    Fix stiller Mock: Scheiterte fetch, 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 fest le-free-01/sec-dv-03, jetzt „keine Produktliste in dieser Quelle“. .. im data-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 Zweck ca), gebunden an den Fingerabdruck von ca.crt. submit nennt den CNAME,
    order_tick --node-ca stellt erst aus, wenn TLS-Gate ihn selbst gesehen hat. Neue Herkunft node_ca (Vertrag v3); sie reicht fĂŒr
    validated nicht, 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.): :443 offen, TLS-Handshake mit Alert 40 abgelehnt, kein Zertifikat; :80 Apache 200.
    Der echte Lauf endet richtig bei awaiting_dns (CNAME nicht gesetzt). Auf einer Kopie mit DNS-Fixture bis issued gespielt:
    openssl verify gegen die Node-CA OK, Außensicht weiter no_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 alten sample.html werden 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_id ist Pflicht).

  • Sensor-IdentitĂ€t per mTLS (Dev-Test): bin/cert_whois.php und TlsGate\Orders\CertIdentity beantworten 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, Herkunft fixture) und meldet sich per mTLS an einem Test-Gateway. Erkannt als sensor/sensor-17 in Gruppe bwa,
    Ort laut Inventar pv-container/batterie. Abgewiesen: ohne Zertifikat, selbstsigniert, fremde CA, echtes Zertifikat mit falschem SchlĂŒssel.
    Gegenversuch mit gelockertem Gateway (verify_peer aus): 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 gegen ca.crt, nicht per Namen).
    Gekaufte Zertifikate lehnt TLS-Gate ab: die widerruft der Registrar. Neue Tabelle cert_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) mit nextUpdate und fortlaufender Nummer, fĂŒr nginx ssl_crl,
    openssl verify -crl_check oder einen Zero ohne Verbindung. PHP kann keine CRL signieren, deshalb die openssl-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_check ab; nach nextUpdate lehnt 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) in cert_whois, Export und Monitor. CertIdentity kennt
    gekaufte Zertifikate aus dem Inventar jetzt als known (vorher foreign_ca), mit dem Hinweis, dass die Kette zur öffentlichen CA hier nicht geprĂŒft wird.
    Neues Urteil revoked, auch in cert_whois (Exit 1).
  • Wildcard *.schule.xyz: Die Challenge liegt an der Basis-Domain (_tlsgate-dcv.schule.xyz). Vorher wĂ€re _tlsgate-dcv.*.schule.xyz entstanden, 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 als revoked).
    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_orders kennt revoked nicht, revoke bricht 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 Tabelle schema_migrations (append-only). schema*.sql beschreibt immer den
    neuesten Stand; spĂ€tere Änderungen kommen als database/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
    ohne fixture, 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 per VACUUM 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 Spalte private_key_pem still 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 (falls tlsgate-my lĂ€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 migrate bringt 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.php ist 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.css und public/assets/ssl.js: Die 18 Inline-onclick/oninput sind 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 aus docs/SECURITY.md 1c erledigt.
  • Tests: tests/test_auth.sh 71 (neu: CSP, kein Inline-JS, Assets erreichbar, src/Http und templates/ nicht im Web).
  • Fix: .gitignore bindet state/ und SSL/ jetzt an den Repo-Root (/state/, /SSL/). Auf macOS
    (case-insensitive) hat SSL/ sonst auch templates/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_log per 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_log und wird nicht an die CA geschickt.

Struktur & Betrieb

  • PSR-4: TlsGate\ → src/ (Auth, Audit, Storage, Ssl, Config), Autoloader in bootstrap.php
    ohne Composer, dazu composer.json mit derselben Zuordnung.
  • public/ als einziger Webroot. router.php ist 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 von level/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 ĂŒber sslReplace,
    Authcode, Besitzerdaten, DCV per DNS/HTTP, Cross-Check), Mock-Fallback auf sample.json.
  • SslHandler.php: gemeinsamer SOAP-Kern fĂŒr Web und CLI. soap_cli.php: JSON-BrĂŒcke.
  • dashboard.sh: Terminal-Dashboard, OpenSSL-Toolbox, deploy mit Match- und Chain-PrĂŒfung,
    Stammdaten-Layer state/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).