Externes Architektur-, Security- & Code-Audit: TLS-Gate (NativePHP RC1.0)

Datum: 18.10.2026
Geprüftes Repository: TLS-NativePHP_v1 (TLS-NativePHP_v1_gemini_rmx)
Geprüfter Stand: RC1.0 (Produktivbetrieb auf Docker-VM / Node, Vorbereitung für Rust-Rewrite)
Prüfumfang: Vollständiger Quellcode (src/, public/, bin/, database/, deploy/, tests/)


1. Executive Summary & Gesamturteil

Fragestellung: Ist das solider Code, der unter realen Bedingungen standhält, und taugt er als Fundament für den geplanten Rewrite in Rust?

Gesamturteil: JA — Außergewöhnlich solide (0 Critical / 0 High / 1 Medium / 5 Low-Info Befunde).

Diese Codebasis hat nichts mit typischem Legacy-PHP oder überladenen Framework-Projekten gemein. Sie ist als Zero-Dependency, Fail-Closed, Local-First Systemprogramm in PHP 8 geschrieben und setzt die Prinzipien aus 42_OZM.md (Epistemologische Hygiene, kein Raten, keine stillen Fallbacks) kompromisslos um.

  1. Belastbarkeit im Realbetrieb: Der Code hält im produktiven Einsatz problemlos stand. Kryptographie (Libsodium KDF + XChaCha20-Poly1305 mit Associated Data), Authentifizierung (Argon2id + Constant-Time Dummy-Hash + TOTP mit Replay-Sperre), Datenbank-Isolation (tlsgate_ddl vs. tlsgate_app, 100 % Prepared Statements, Append-Only-Trigger) und Web-Härtung (strikte CSP ohne Inline-JS, CSRF-Schutz, __Host--Cookies) bewegen sich auf Referenzniveau.
  2. Eignung für den Rust-Rewrite: Die Architektur ist bereits heute wie ein Rust-Programm geschnitten (final class ohne Vererbung, Pure Functions für Guard/Normalisierung, deklarative JSON-Verträge für State-Machines und Katalog, binärkompatibles tg1:-Verschlüsselungsformat). Ein späterer Rust-Service kann direkt gegen dieselbe MariaDB und dieselben verschlüsselten Secrets arbeiten.

Die im Audit identifizierten Härtungspunkte (SOAP-Stream-Timeouts/TLS, atomares TOTP-Step-Update gegen Race-Conditions, FQDN-Validierung beim Live-Cross-Check, $_POST-Entkopplung und fehlende Kategorie-Filter im Audit-Log-UI) wurden im Zuge dieses Audits direkt als Patch eingespielt.


2. Positiv-Audit: Architektur- & Security-Stärken

2.1 Kryptographie & Secret-Management (src/Security/Secrets.php)

  • Schlüsselableitung (KDF): Ein einziger 32-Byte Root-Key (TLSGATE_MASTER_KEY bzw. Keyfile) leitet über sodium_crypto_kdf_derive_from_key (Kontext TLSGATE1) strikt getrennte Subkeys pro Zweck (totp, soap, outbox, ca, netbox) und key_id ab.
  • AEAD mit Context-Binding: Jeder Chiffretext (tg1: + Version + key_id + 24-Byte Nonce + XChaCha20-Poly1305) bindet den logischen Eigentümer über die Associated Data (tg1|<purpose>|<bind>, z. B. user:<id> oder <group_id>|<label>). Selbst bei kompromittierter Datenbank kann ein Angreifer keinen verschlüsselten Blob in eine andere Zeile oder Gruppe kopieren.
  • Speicher- und Log-Hygiene: Abgeleitete Subkeys werden im finally-Block per sodium_memzero() aus dem RAM gelöscht. Die Value-Objekte SoapCredential und NetBoxCredential überschreiben __debugInfo(), __toString(), __serialize() und __clone(), sodass Zugangsdaten niemals in Stacktraces, Logs oder Sessions gelangen.
  • Key-Rotation im laufenden Betrieb: Transaktionales key rotate und key migrate über alle fünf Secret-Stores hinweg, abgesichert durch tests/test_secrets.sh.

2.2 Authentifizierung, MFA & Session-Führung (src/Auth/)

  • Schutz gegen Timing-Enumeration: Auth::attemptPassword() prüft bei nicht existierenden E-Mail-Adressen gegen Auth::dummyHash() (Argon2id mit identischen Kostenparametern).
  • Privilegiengrenzen & Session-Hardening: Cookie __Host-tlsgate (Secure, HttpOnly, SameSite=Strict), session_regenerate_id(true) bei jedem Phasenübergang (Passwort $\rightarrow$ MFA $\rightarrow$ Login) und Live-Abfrage von active und role aus der Datenbank bei jedem Request.
  • Verifizierte Rollen-Matrix: tests/test_roles.sh prüft alle vier Rollen (readonly, manager, security, admin) gegen jeden Endpunkt und schlägt fehl, sobald eine neue Datei in public/*.php auftaucht, die nicht in der Testmatrix deklariert ist.

2.3 Datenbank-Design & Revisionssicherheit (database/schema.mysql.sql, src/Storage/)

  • 100 % echte Prepared Statements: PDO::ATTR_EMULATE_PREPARES => false und PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION in Db.php.
  • Zwei-Konten-Modell: tlsgate_ddl wird ausschließlich für bin/tg migrate genutzt; der Web-/CLI-Pool läuft unter tlsgate_app (optional passwortlos per Unix-Socket DB_AUTH=socket) ohne DROP-, ALTER- oder TRIGGER-Rechte.
  • Unveränderliche Event-Tabellen: audit_log, booking_events, catalog_events und registrar_inventory_events blockieren UPDATE und DELETE auf Datenbankebene per Trigger (SIGNAL SQLSTATE '45000'). Jeder Audit-Eintrag trägt zudem den exakten Build-/Git-Stempel (Config::buildStamp()).

2.4 Fail-Closed Synchronisation & Betrieb (src/Orders/Inventory.php, src/Netbox/, deploy/)

  • Keine stillen Mock-Daten: Fällt SOAP oder NetBox aus, meldet das System explizit source => 'unavailable' statt Beispieldaten unterzuschieben. Sample-Seeds sind kryptographisch/logisch getrennt und nur explizit per CLI (--allow-sample) ladbar.
  • Mass-Deletion Circuit-Breaker: Sowohl Inventory::sync() als auch DeviceMirror::sync() verweigern den Abgleich automatisch, wenn ab einem Mindestbestand (MASS_GONE_MIN = 10) plötzlich mehr als 50 % der Einträge fehlen (Schutz vor kaputten Upstream-Responses).
  • Geprüfte Backups: deploy/node/bend.sh folgt dem Grundsatz „Ein Backup zählt erst, wenn es eingespielt wurde“ und verifiziert jeden Datenbank-Dump unmittelbar in einer temporären Prüfdatenbank inkl. Trigger-Test.

3. Detail-Befunde & Eingespielte Patches

ID Schweregrad Komponente Befund Status
AUD-01 Medium src/Ssl/SslHandler.php src/Security/CredentialStore.php SoapClient wurde ohne connection_timeout, ohne HTTP-Stream-Timeouts und ohne Laufzeit-Prüfung von TLSGATE_SOAP_HOSTS initialisiert. Bei hängendem Registrar drohte eine Blockade aller PHP-FPM-Worker (pm.max_children = 5). Behoben (Patch)
AUD-02 Low / Medium src/Auth/Auth.php TOCTOU-Race-Condition bei der TOTP-Replay-Prüfung: totp_last_step wurde per SELECT gelesen und danach ohne atomare WHERE-Bedingung per UPDATE geschrieben. Behoben (Patch)
AUD-03 Low / Medium src/Ssl/SslHandler.php crossCheck() öffnete ssl://$domain:443 direkt mit dem String aus der SOAP-Antwort ohne vorherige FQDN-/IP-Literal-Validierung und ohne expliziten SNI-Hostnamen. Behoben (Patch)
AUD-04 Low src/Ssl/SslHandler.php Direkte $_POST[...]-Zugriffe ohne ?? '' in genCsr(), sslCreate() und sslGetDCVList() konnten bei unvollständigen Requests PHP-8-Warnings (Undefined array key) erzeugen. Behoben (Patch)
AUD-05 Low public/auditlog.php Die Kategorie-Whitelist $allowedCat kannte nur auth., ssl., admin., cli. — neuere Audit-Präfixe (web., booking., catalog., module.) waren nicht als Filter wählbar. Behoben (Patch)
AUD-06 Info / Ops src/Orders/Inventory.php src/Ssl/SslHandler.php sslListExtended() fragt bis zu LIMIT = 2000 Zertifikate ab; ab 2.000 Einträgen pro Gruppe bricht Inventory::sync() sicherheitshalber (fail-closed) ab, bis Paging implementiert wird. Dokumentiert (Fail-Closed greift)
AUD-07 Info / Ops database/schema.mysql.sql Tabelle login_attempts wächst unbegrenzt (bereits in docs/ADMIN.md §10 vermerkt). Empfehlung: periodisches Bereinigen alter Einträge (> 30 Tage). Dokumentiert

Technische Details zum eingespielten Patch

  1. SOAP-Client-Härtung (AUD-01):
  2. CredentialStore::assertAllowedWsdl($wsdl) als öffentliche Prüfmethode extrahiert und zusätzlich direkt vor new SoapClient(...) in SslHandler::getSoapClient() aufgerufen.
  3. SoapClient erhält nun connection_timeout = 15 sowie einen stream_context mit http.timeout = 20, follow_location = 0, max_redirects = 0 und ssl.verify_peer = true / verify_peer_name = true.
  4. Atomarer TOTP-Replay-Schutz (AUD-02):
  5. In Auth::verifyMfa() prüft das UPDATE-Statement nun atomar WHERE id = ? AND (totp_last_step IS NULL OR totp_last_step < ?).
  6. Ist $upd->rowCount() !== 1, wurde derselbe Zeitschritt in einem parallelen Request bereits verbraucht; der Versuch wird als auth.mfa (reason: replay_race) geloggt und abgewiesen.
  7. FQDN-Validierung & SNI in crossCheck() (AUD-03):
  8. Vor stream_socket_client() wird ein eventuelles *.-Wildcard-Präfix für den Socket-Connect entfernt und streng geprüft, dass ein syntaktisch gültiger FQDN (mit Punkt, max. 253 Zeichen, kein IP-Literal) vorliegt. Zusätzlich wird SNI (peer_name) explizit gesetzt.
  9. Defensive $_POST-Zugriffe (AUD-04) & Audit-Filter (AUD-05):
  10. Alle $_POST-Zugriffe in SslHandler sind mit ?? '' abgesichert.
  11. public/auditlog.php unterstützt in Dropdown und Schnellfilter-Chips nun auch web., booking., catalog. und module..

4. Blueprint für den Rust-Rewrite (Migrations-Architektur)

Die bestehende PHP-Anwendung ist so strukturiert, dass der geplante Rewrite in Rust ohne Bruch an Datenbank, Krypto-Blobs oder CLI-/API-Verträgen erfolgen kann:

4.1 Binärkompatibilität der Datenbank-Secrets (tg1:)

Das Format in src/Security/Secrets.php ist sprachunabhängig spezifiziert:

"tg1:" || Base64( 0x01 || u32_le(key_id) || nonce[24] || XChaCha20Poly1305_Seal(plaintext, ad) )
  • KDF: Blake2b-basierte Libsodium-KDF (crypto_kdf_derive_from_key, Kontext "TLSGATE1", Subkey-ID = key_id * 100 + slot).
  • AEAD: crypto_aead_xchacha20poly1305_ietf_encrypt mit Associated Data "tg1|" + purpose + "|" + bind.
  • Vorteil für Rust: Ein Rust-Binary (über libsodium-sys oder chacha20poly1305 + blake2b_simd) kann alle bestehenden Secrets in MariaDB sofort lesen und schreiben, ohne dass TOTP-Token der Benutzer oder SOAP-/NetBox-Zugangsdaten neu eingegeben werden müssen.

4.2 Empfohlener Rust-Stack & Modul-Mapping

PHP-Komponente Rust-Äquivalent (Empfehlung) Migrationsvorteil
src/Storage/Db.php sqlx (MySQL/MariaDB, Compile-Time Query-Check) Identisches SQL-Schema (schema.mysql.sql), inkl. Unix-Socket-Auth
src/Security/Secrets.php libsodium-sys / chacha20poly1305 + zeroize zeroize::Zeroizing ersetzt sodium_memzero + SoapCredential-Wrapper
src/Auth/Auth.php & Totp.php argon2, hmac + sha1, tower-sessions Argon2id-Hashes ($argon2id$v=19$...) sind 1:1 kompatibel
database/order_states.json database/catalog_matrix.json serde + serde_json (enum OrderState) Deklarative JSON-Verträge werden direkt zu typensicheren Rust-Enums
public/*.php + SslController axum + askama (Templates) Gleiche Routen, gleiche CSP/Security-Header via tower-http
bin/tg + bin/*_cli.php Ein einziges statisches Rust-Binary (clap) Ersetzt den gesamten PHP-CLI-Baum durch ein einziges Binary

4.3 Inkrementelle Strangler-Fig-Migration

Da sämtliche Integrationstests (tests/test_secrets.sh, tests/test_roles.sh, tests/test_bookings.sh, tests/test_inventory.sh, tests/test_netbox.sh) über CLI-Aufrufe, curl und SQL-Prüfungen gegen die echte Datenbank arbeiten, kann der Rust-Rewrite schrittweise gegen exakt dieselbe Test-Suite verifiziert werden:
1. Stufe 1: Rust-Binary übernimmt die CLI-Kommandos (bin/tg) und Hintergrund-Syncs (inventory sync, netbox sync, key status).
2. Stufe 2: Rust-HTTP-Server (axum) übernimmt /api.php und die Web-Views hinter Apache/Reverse-Proxy.
3. Stufe 3: Abschalten von PHP-FPM auf der Node – Datenbank und Secrets bleiben unverändert bestehen.