TLS-Gate — OZM Admin Audit (Betrieb, Sicherheit, PrĂŒfung)

Gilt fĂŒr: RC1.0 (App-Version siehe VERSION, Code-Stand siehe BUILD bzw. Kopfzeile von bin/tg-dash) · Stand 2026-10-16
FĂŒr wen: alle, die eine TLS-Gate-Instanz aufsetzen, betreiben, prĂŒfen oder wiederherstellen — ohne das Projekt vorher zu kennen.

Dieses Dokument ersetzt docs/SECURITY.md und docs/IDENTITAET.md. Jede Aussage hier ist am Code geprĂŒft; was nicht geprĂŒft ist,
steht als UNKNOWN da. UNKNOWN ist ein gĂŒltiger Zustand, kein Fehler und kein Platz fĂŒr eine Vermutung.


0. In fĂŒnf SĂ€tzen

  1. TLS-Gate ist eine PHP-Anwendung (Apache 2.4 + PHP-FPM 8.4 + MySQL 8), die SSL-Bestand, Katalog, Buchungen, SOAP- und
    NetBox-ZugĂ€nge je Gruppe verwaltet — mit Login (Passwort + TOTP) und einem Audit, das sich nicht Ă€ndern lĂ€sst.
  2. Die Umgebung (Webserver, PHP, MySQL, Zertifikat, root) gehört dem Rechenzentrum. TLS-Gate installiert nichts, es wird mit
    deploy/node/bend.sh auf die vorhandene Umgebung gebogen.
  3. Jeder Fehler ist rot und hÀlt an. Nichts geht still weiter.
  4. Ein Backup zÀhlt erst, wenn es eingespielt wurde: jeder Snapshot wird vor dem Verpacken probeweise eingespielt.
  5. Der WurzelschlĂŒssel ist das eine Geheimnis, das nie verloren gehen darf und nie rotiert wird, wenn man einen Dump zurĂŒckspielt.

1. Die vier Schichten: SOT → UI → bash → cron

Schicht Was Wo Wer Wahrheit ĂŒber
SOT MySQL-Datenbank der Instanz, WurzelschlĂŒssel, Umgebungsdatei DB tlsgate, /etc/tlsgate/root.key, /root/.tlsgate.env DB: nur ĂŒber die App und die CLIs; SchlĂŒssel/Umgebung: root Konten, Gruppen, Rollen, Katalog, Freigaben, Buchungen, versiegelte ZugĂ€nge, Audit
Code Git (git.crumbforest.org/branko/TLS-NativePHP_v1), auf der Node /home/apache/public_html + Manifest root (ĂŒber bend.sh) was lĂ€uft (BUILD: Commit + dirty)
Spiegel Tabellen registrar_inventory*, netbox_devices* nur lesend gespiegelt Registrar und NetBox bleiben fĂŒhrend; TLS-Gate zeigt, was es zuletzt gesehen hat
UI Web unter https://<host>/ public/*.php (nur dieses Verzeichnis ist DocumentRoot) angemeldete Konten, je Rolle (§4) Ansicht und Bedienung; jede Aktion im Audit
bash bin/tg-dash (MenĂŒ), bin/tg (eine Zeile), bin/*_cli.php App-Baum bin/ root bzw. Besitzer der Instanz (Guard, §8.5) dieselben Regeln wie die UI, jeder Aufruf mit Akteur im Audit
cron IntegritÀts-Scan bin/TLS-app-scan.bash -q /etc/cron.d/tlsgate-scan (Vorlage von bend.sh) root ob der App-Baum noch dem letzten Deploy entspricht

Faustregel: Was in der DB steht, gilt. Was die UI zeigt, kommt aus der DB. Was die CLI tut, tut auch die UI (oder umgekehrt), mit
denselben Regeln im selben Code. Der cron schaut nur zu und meldet.


2. Dateien, Pfade, Rechte

Pfad Inhalt Besitzer / Modus Gesichert durch
/home/apache/public_html (APP_DIR) App-Baum (Code), DocumentRoot = public/ root:root, fĂŒr den Web-Benutzer nur lesbar Git + Manifest /var/lib/tlsgate/app.manifest
/home/apache/state (TLS_GATE_STATE_DIR) Sessions, tmp, ssl_outbox Pool-Benutzer, 0700 — (flĂŒchtig)
/etc/tlsgate/root.key WurzelschlĂŒssel (<id>:<base64 32 Byte>, mehrere Zeilen nach key rotate) root:<pool-benutzer> 0640, Verzeichnis 0750 Snapshot secrets/root.key
/root/.tlsgate.env Umgebung der CLI: DB-Namen und -Passwörter (app, ddl), Pfade root 0600 Snapshot secrets/tlsgate.env
/etc/php/8.4/fpm/pool.d/tlsgate.conf FPM-Pool (gehört der Umgebung, Vorlage von bend.sh) root:root 0640 Umgebung
/root/tlsgate-bend/<zeit>/ Protokoll, Schritte, Vorlagen, Snapshot jedes bend.sh-Laufs root 0700 —
/var/lib/tlsgate/app.manifest sha256, Modus, Besitzer jeder Datei des App-Baums root 0600 wird bei jedem bend.sh neu geschrieben

Nichts davon liegt im Repo. Die Umgebung der App kommt ausschließlich aus Variablen:

Variable Bedeutung
TLS_GATE_STATE_DIR Pflicht. Ohne sie bricht jede Seite und jede CLI mit Meldung ab (Exit 2)
TLSGATE_DB_HOST, _PORT, _NAME, _USER, _PASS MySQL ĂŒber TCP mit Passwort (Standard)
TLSGATE_DB_SOCKET MySQL ĂŒber Unix-Socket; dann wird nie ein Passwort geschickt (Weg „IdentitĂ€t“, §8.7)
TLSGATE_DDL_USER, TLSGATE_DDL_PASS nur in der Umgebungsdatei, nur fĂŒr migrate (bin/tg migrate)
TLSGATE_ROOT_KEY_FILE oder TLSGATE_ROOT_KEY WurzelschlĂŒssel
TLSGATE_SOAP_HOSTS, TLSGATE_NETBOX_HOSTS optionale Allowlist fĂŒr ausgehende Verbindungen
TLSGATE_GUARD=off nur Dev/Tests: schaltet den CLI-Guard ab; steht dann im Audit, der Doktor meldet es
TLSGATE_COOKIE_SECURE=0 nur Test ohne TLS; im Pool ist es ein Fehler (Doktor)

3. Aufsetzen: deploy/node/bend.sh

bend.sh installiert nichts. Es fasst nur an, was der App gehört (App-Baum, state/, SchlĂŒssel, Umgebungsdatei, Inhalt der
eigenen DB). Pool, vhost und cron legt es als Vorlage in den Protokollordner; einsetzen tut sie der Mensch der Umgebung.

MODE=prod SITE=gate.example.org ADMIN_EMAIL=admin@example.org ADMIN_GROUP=ozm deploy/node/bend.sh
Modus WofĂŒr Benutzer
review Review-/Wegwerf-Instanz Review-Dummy (§3.3), danach Gegenprobe
prod echte Instanz erster Admin (ADMIN_EMAIL, ADMIN_GROUP); Zugang in /root/.tlsgate-admin.tsv (0600)
restore Wiederherstellen aus Snapshot (§7) aus dem Dump

3.1 Die Schritte (jeder rot = Abbruch, Protokoll nennt den Schritt)

# Schritt GrĂŒn heißt
0 Vorbedingungen (nur lesend) PHP ≄ 8.1 mit pdo_mysql mbstring openssl posix soap curl, Werkzeuge da, Web-Benutzer existiert
1 Umgebungsdatei vorhanden (Passwörter bleiben) oder neu mit Zufallspasswörtern; Pfade stimmen mit Aufruf ĂŒberein
2 Datenbank DB + zwei Benutzer (tlsgate_ddl alle Rechte auf die DB, tlsgate_app nur DML) angelegt bzw. vom DBA (DB_SETUP=skip, SQL in dba.sql); beide melden sich an
3 App-Baum Code root-eigen, state/ 0700, BUILD geschrieben, Manifest geschrieben
3b (nur restore) Dump in die leere DB eingespielt, PrĂŒfsummen stimmen
4 migrate Schema aktuell (mit dem DDL-Benutzer; SET_USER_ID nur wÀhrend der Migration)
5 Katalog Katalog-Basis geladen (idempotent; nicht bei restore)
6 Benutzer Review-Dummy bzw. erster Admin
7 SOAP/NetBox aktive ZugÀnge getestet; ohne Token = UNKNOWN, kein Fehler
8 Doktor bin/TLS-node-doktor.bash ohne FAIL (Warnungen zÀhlen nicht)
9 Gegenprobe review: deploy/node/review-check.sh grĂŒn; sonst gilt der Doktor
10 Snapshot Dump + Geheimnisse, Restore-Probe bestanden, verschlĂŒsselt

Erster Lauf auf einer RZ-VM endet bei Schritt 8 rot, solange Pool und vhost fehlen. Das ist gewollt: Vorlagen aus
/root/tlsgate-bend/<zeit>/vorlage-pool.conf, vorlage-vhost.conf, vorlage-cron einsetzen (Passwort aus /root/.tlsgate.env
in den Pool), FPM neu starten, denselben Befehl wiederholen. bend.sh ist idempotent.

3.2 Variablen (Auswahl; vollstÀndig im Kopf von bend.sh)

Variable Standard
APP_DIR, STATE_DIR, KEY_FILE, ENV_FILE siehe §2
WEB_USER www-data Gentoo: apache
PHP php8.4, sonst php Gentoo: php
DB_SETUP auto skip, wenn der DBA anlegt (dba.sql im Protokoll)
DB_AUTH password socket = Weg „IdentitĂ€t“ (§8.7)
SNAPSHOT_PASS_FILE — ohne: Abfrage am Terminal; ohne Terminal: rot (unverschlĂŒsselt nur mit SNAPSHOT_PLAIN=1)

Was im RZ beim Menschen bleibt und bend.sh nie anfasst: die Apache-Kette (wo TLS terminiert, wer weiterleitet — ĂŒbernommen werden
nur DocumentRoot <app>/public und die SetHandler-Zeile aus vorlage-vhost.conf), das produktive Zertifikat (nur das vorhandene,
nach Freigabe durch Review und Crew), eine externe MySQL-Instanz (DB_SETUP=skip, dba.sql an den DBA), DNS, Firewall, Monitoring.

deploy/node/build.sh ist kein Weg fĂŒr das RZ: es installiert Pakete und schreibt Webserver-Konfiguration. Es bleibt nur, um
eine Wegwerf-Node (Container) fĂŒr Tests zu bauen.

3.3 Review-Dummy (nur MODE=review)

deploy/node/review-dummy.sh legt zwei Gruppen an — bwa (MFA-Modus code) und ozm (MFA-Modus qr) — je sechs Konten
(admin, gadmin, user1, user2, ro, neu ohne MFA) als <kĂŒrzel>@<gruppe>.review.test, je Gruppe eigenen Beispielbestand und
eine Buchung aus der Matrix. Klartext-Passwörter und TOTP-Secrets stehen in /root/.tlsgate-review-users.tsv (0600). Nie auf
einer Produktions-Instanz
; das Skript verweigert ohne REVIEW_DUMMY=1.

deploy/node/review-check.sh meldet jeden an (Passwort + TOTP ĂŒber Apache → Pool → DB) und prĂŒft, dass jede Gruppe ihren Bestand
sieht und den der anderen nicht. Wiederholbar: Erst-Login-Konten werden vorher zurĂŒckgesetzt, ein im selben 30-s-Fenster schon
benutzter Code wird abgewartet (Replay-Schutz, §8.1).


4. Rollen und Gruppen

Zwei Achsen entscheiden ĂŒber jeden Zugriff: die Rolle des Kontos und die Art seiner Gruppe — nie der Name der Gruppe.

Rollen in Rangfolge (src/Auth/Auth.php, ROLES): readonly < user < group_admin < admin. Jede Seite verlangt eine
Mindestrolle (Auth::requireLogin); darunter 403, ohne Anmeldung Umleitung auf login.php.

Seite Mindestrolle Methode nur Gruppenart eigen Zweck
login.php – GET/POST Anmeldung: Passwort, dann TOTP (Erst-Login: Einrichtung)
logout.php – POST nur per Formular mit CSRF-Token
index.php readonly GET Inhalt SSL-Konsole (ohne SSL-Zugang der Gruppe nur Hinweis)
bestand.php readonly GET ja Registrar-Bestand der eigenen Gruppe; „Aktualisieren“ ab user
dashboard.php readonly GET Kacheln aus den Stammdaten der eigenen Gruppe
mfa.php readonly GET eigenes TOTP erneuern (altes gilt, bis das neue bestÀtigt ist)
outbox.php user POST ja Deploy-ZIP einer Bestellung (Zertifikat + Kette, nie ein SchlĂŒssel)
products_export.php user POST ja Produktliste des Registrars als JSON
credentials.php group_admin GET SOAP-ZugÀnge (group_admin: eigene Gruppe, admin: alle)
netbox.php group_admin GET NetBox-ZugÀnge und Lesespiegel
stammdaten.php group_admin GET Stammdaten der eigenen Gruppe, nur lesen
auditlog.php admin GET Audit lesen
katalog.php admin GET Katalog, Kategorien, Freigaben, Buchen
users.php admin GET Konten und Gruppen verwalten

Diese Tabelle ist tests/test_roles.sh: der Test prĂŒft zuerst, dass jede Seite unter public/ darin steht und Mindestrolle,
Methode und SSL-Sperre genau dem Code entsprechen, dann je Rolle in zufÀllig benannten Gruppen beider Arten jeden Zugriff. Eine
neue Seite ohne Eintrag oder ein geÀnderter requireLogin ohne Test ist rot.

Regeln, die der Code zusÀtzlich erzwingt (alle im Test belegt):

  • Gruppenart: eigen (eigene Organisation) oder reseller. SSL-Seiten nur fĂŒr eigen (Auth::sslAllowed).
  • admin ist global und gehört nur in eine Gruppe vom Typ eigen; user add 
 admin in einer Reseller-Gruppe wird abgelehnt.
  • Rolle, Gruppe und Aktiv-Status werden bei jedem Request aus der DB gelesen: ein Rollenwechsel, lock oder disable wirkt
    sofort, auch auf laufende Sitzungen.
  • MFA-Modus je Gruppe: code (Secret einmal einrichten, dann Codes) oder qr (nach jedem Passwort-Login ein neues Secret).
    Neue Gruppen bekommen TLSGATE_MFA_DEFAULT (Standard qr).
  • Buchungen (src/Catalog/Bookings.php): readonly bucht nichts; ein user nur fĂŒr sich; der Admin der Gruppe fĂŒr Personen
    seiner Gruppe; Kategorien der Achse „access“ nur fĂŒr Konten mit Rolle admin. Jede Ablehnung steht mit Grund im Audit.

5. TĂ€glicher Betrieb

Einstieg: bin/tg-dash (als root). Oben steht der Zustand der Instanz — Umgebung, DB, Schema, IntegritĂ€t, letzter bend.sh-Lauf
— darunter ein MenĂŒ. Vor jeder Aktion zeigt das MenĂŒ den gleichwertigen Befehl; wer ihn kennt, tippt direkt bin/tg 
.
bin/tg-dash status gibt nur den Zustand aus (Exit 1 bei Rot) und taugt fĂŒr einen schnellen Blick per ssh.

Aufgabe Befehl
Konten anzeigen bin/tg user list
Konto anlegen bin/tg user add <email> <gruppe> <rolle> (Passwort wird ohne Echo abgefragt, ≄ 15 Zeichen)
TOTP sofort einrichten (Secret ausgeben) bin/tg user enroll <email>
aktuellen Code (Test/Support) bin/tg user code <email>
entsperren nach Fehlversuchen bin/tg user unlock <email>
Handy verloren bin/tg user reset-mfa <email> (beim nÀchsten Login neu einrichten)
Passwort setzen / Rolle Àndern bin/tg user passwd <email> · bin/tg user role <email> <rolle>
sperren / abschalten bin/tg user lock <email> · bin/tg user disable <email>
Gruppe anlegen / MFA-Modus bin/tg group add <slug> <name> eigen\|reseller · bin/tg group mfa <slug> code\|qr
SOAP-Zugang einer Gruppe bin/tg cred add <gruppe> <label> <wsdl> (Benutzer/Passwort werden abgefragt) · bin/tg cred test <gruppe>
NetBox-Zugang printf '%s\n' "$TOKEN" \| bin/tg netbox zugang add <gruppe> <label> <url> <tenant> (Token nur ĂŒber stdin)
NetBox-Spiegel einer Gruppe TLSGATE_GROUP=<g> bin/tg netbox status \| geraete \| events 20 \| anker
NetBox abgleichen (live / aus Datei) erst TLSGATE_GROUP=<g> bin/tg netbox sync --dry-run, dann ohne --dry-run; Datei: bin/tg netbox fetch > nb.json, dann netbox load nb.json --dry-run
Registrar-Bestand abgleichen erst TLSGATE_GROUP=<g> bin/tg soap sync --dry-run, dann ohne; Ereignisse: bin/tg inventory events 20
Audit lesen bin/tg audit 50 oder auditlog.php
Schema / SchlĂŒssel bin/tg migrate --status · bin/tg key status

Im MenĂŒ (tg-dash 23–30) lĂ€uft jeder Abgleich zuerst als Probe (--dry-run); geschrieben wird erst nach „j“. --force (leere oder
stark geschrumpfte Liste trotzdem ĂŒbernehmen) gibt es dort bewusst nicht — das bleibt eine Entscheidung von Hand. Mehrere ZugĂ€nge in
einer Gruppe: TLSGATE_CRED=<label> dazu.

Code Ă€ndern heißt immer: Repo aktualisieren, bend.sh laufen lassen. Nur so passen Manifest, BUILD und Schema zusammen.
Eine Datei von Hand in den App-Baum zu legen ist ein Befund des Scans (§6.2) — auch wenn root es war.

ZugÀnge ohne Token sind normal. SOAP- und NetBox-ZugÀnge kommen von Menschen, nicht aus einem Skript. Bis dahin stehen sie
in bend.sh und im Dashboard als UNKNOWN. Der Seed legt je Gruppe einen abgeschalteten Platzhalter sample an; er zÀhlt nicht.


6. PrĂŒfen

6.1 Doktor — bin/TLS-node-doktor.bash (nur lesend)

PrĂŒft Voraussetzungen (PHP CLI und FPM), DB (Verbindung, Zeichensatz, Schema-Stand, Rechte des App-Benutzers, Trigger), Dateien
und Rechte (Code nicht schreibbar, state/ und SchlĂŒssel), IntegritĂ€t gegen das Manifest, Apache (vhost, Module), Pool
(disable_functions inkl. phpinfo, open_basedir, clear_env, Umgebung) und von außen ĂŒber HTTPS (Header, Cookie, keine
Pfade außerhalb von public/). Exit = Anzahl FAIL. bend.sh ruft ihn in Schritt 8.

6.2 IntegritĂ€t — bin/TLS-app-scan.bash

Vergleicht den App-Baum mit dem Manifest des letzten bend.sh-Laufs: NEU, GEAENDERT, RECHTE, FEHLT. Abweichung → Ausgabe
(cron schickt sie an MAILTO), Zeile in syslog (logger -t tlsgate-scan, auth.warning), Exit 1, und der Doktor ist rot.
cron-Vorlage: */10 * * * * root APP_DIR=
 
/bin/TLS-app-scan.bash -q. Das ist Erkennen, nicht Verhindern (§8.8).

6.3 Tests — tests/run_all.sh

Alle Suiten gegen einen echten MySQL-8-Server, je Suite eine eigene Wegwerf-DB (tests/README.md). FĂŒr RC1.0 maßgeblich:
test_roles (Rollen × Gruppenart), test_auth/test_mfa (Login), test_migrate (Migration = frisch), test_secrets,
test_guard, test_credentials, test_katalog.


7. Sichern und Wiederherstellen

Jeder grĂŒne bend.sh-Lauf endet mit /root/tlsgate-bend/<zeit>/tlsgate-snapshot-<host>-<zeit>.zip.enc (AES-256, PBKDF2 600 000).
Inhalt: tlsgate.sql.gz, secrets/root.key, secrets/tlsgate.env, Benutzer-ZugÀnge, GEHEIMNISSE.md, RESTORE.md, dba.sql,
Vorlagen, Protokoll, SHA256SUMS.

Restore-Probe: Vor dem Verpacken wird der Dump in eine Wegwerf-DB eingespielt und Tabelle fĂŒr Tabelle (COUNT(*)) plus Trigger
mit der Quelle verglichen. Weicht etwas ab, ist der Lauf rot. Ohne MySQL-Admin-Zugang: UNKNOWN im Snapshot vermerkt.

openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in <datei>.zip.enc -out snap.zip && unzip snap.zip
MODE=restore SITE=<host> SNAPSHOT_DIR=<entpackter Ordner> deploy/node/bend.sh
  • Die DB muss leer sein; eine gefĂŒllte DB wird nie ĂŒberschrieben.
  • Root-ZugĂ€nge der Umgebung und DB-Passwörter dĂŒrfen neu sein: neue Werte vorher in /root/.tlsgate.env (und in den Pool).
  • Der WurzelschlĂŒssel nicht. Er muss byte-gleich zurĂŒck, sonst sind alle TOTP-Secrets, SOAP- und NetBox-ZugĂ€nge im Dump
    unlesbar. Eine vorhandene, abweichende SchlĂŒsseldatei ĂŒberschreibt bend.sh nie.
  • Belegt im Container: Restore in eine frische Maschine mit neuen DB-Passwörtern, danach Anmeldung aller Konten mit ihren alten
    TOTP-Secrets (49 grĂŒn, 0 rot).

Dumps Ă€lterer Instanzen (vor ebdb1eb) tragen Trigger mit einem ; im Körper und ließen sich nicht einspielen (Fehler 1064);
bend.sh repariert das Muster beim Dumpen, die Restore-Probe belegt es.


8. Sicherheit

8.1 Anmeldung

  • Passwort (Argon2id, 15–128 Zeichen, keine Zeichenklassen-Regeln, keine Zwangsrotation — NIST SP 800-63B) und TOTP fĂŒr jede Rolle.
  • TOTP-Secrets versiegelt (§8.3). Replay-Schutz: jeder Code gilt nur einmal (totp_last_step).
  • Sperren: Konto nach 5 Fehlversuchen (Passwort oder TOTP) fĂŒr 15 min, IP nach 10 Fehlversuchen in 15 min. Alle Fehlpfade melden
    dasselbe; bei unbekanntem Konto wird trotzdem ein Argon2id-Hash gerechnet. Zwischen Passwort und TOTP höchstens 5 min.
  • Sitzung: __Host--Cookie, Secure, HttpOnly, SameSite=Strict, neue Session-ID nach jedem Schritt, 30 min InaktivitĂ€t / 12 h absolut.
  • CSRF-Token auf jedem Formular (auch Logout, nur POST). Header: strikte CSP (default-src 'none'; script-src 'self'),
    frame-ancestors 'none', X-Frame-Options: DENY, nosniff, no-referrer, no-store. Kein Inline-JS.

8.2 Audit

audit_log ist per Trigger append-only (UPDATE und DELETE werfen Fehler). Jede Web-Aktion und jeder CLI-Aufruf steht dort mit
Akteur (cli:root, Konto, Gruppe), Ziel und Ergebnis (ok, denied, failed) — ohne Passwörter, Tokens oder Secrets. Grenze:
wer DDL-Rechte hat, kann Trigger entfernen; der Schutz ist die Trennung tlsgate_ddl (nur migrate) / tlsgate_app (nur DML, im Pool).

8.3 Geheimnisse

  • Ein WurzelschlĂŒssel, getrennte UnterschlĂŒssel je Zweck (src/Security/Secrets.php): XChaCha20-Poly1305, UnterschlĂŒssel per KDF
    fĂŒr totp, soap, outbox, netbox. Jeder Blob ist an seinen Datensatz gebunden (Gruppe, Label, Konto): eine kopierte Zeile
    öffnet sich nirgendwo anders.
  • SOAP- und NetBox-ZugĂ€nge gehören der Gruppe und liegen nur versiegelt in der DB. Eingabe nur ĂŒber Prompt bzw. stdin, nie als
    Argument (Prozessliste, History); nie zurĂŒck ins HTML; die Wertobjekte verraten sich nicht bei var_dump/json_encode/serialize.
  • WSDL nur https://; TLSGATE_SOAP_HOSTS schrĂ€nkt die Ziele ein. Kein Mock: ohne Zugang zeigt die App „nicht verfĂŒgbar“ mit Grund.
  • Wer nur die DB oder nur den SchlĂŒssel hat, öffnet nichts. Wer beides hat, öffnet alles dieser Instanz.
  • key rotate hĂ€ngt eine neue key_id an; alte bleiben lesbar, bis key migrate neu versiegelt hat.

8.4 Auslieferung

  • DocumentRoot ist nur public/. src/, bin/, database/, state/ liegen nicht im Web — durch Struktur, nicht durch
    Sperrlisten. Der Doktor ruft Pfade außerhalb von public/ von außen ab und erwartet, dass keiner 200 liefert.
  • Ein eigener FPM-Pool (deploy/node/tlsgate-pool.conf.example): clear_env = yes, open_basedir auf App, State und SchlĂŒssel,
    display_errors off, expose_php off, disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec, pcntl_fork,dl,putenv,mail,phpinfo. Die App braucht keine davon. phpinfo steht dort, weil eine info.php — der erste Reflex
    nach jedem Install — sonst $_SERVER mit dem DB-Passwort des Pools im Browser zeigt.
  • Hinter einem Proxy muss die echte Client-Adresse als REMOTE_ADDR ankommen (mod_remoteip); X-Forwarded-For wird nicht
    ausgewertet. Sonst stehen Proxy-Adressen im Audit und die IP-Sperre trifft alle.

8.5 CLI-Guard

Die CLIs laufen nur als root, per sudo (Aufrufer in einer Operator-Gruppe: sudo, wheel, admin) oder als Besitzer der Instanz
(wem State und SchlĂŒssel gehören); sonst Exit 3, nichts wird geschrieben. Der Web-Prozess kommt nie durch. Es gibt kein CLI-Login:
das Betriebssystem ist die Authentisierung, TOTP gilt fĂŒrs Web. Grenze: der Guard ordnet zu („wer war es“) und schĂŒtzt vor
Fehlbedienung und dem Web-Prozess; die Zugriffsgrenze sind die Dateirechte.

8.6 Was ein Code im Pool kann

Jede PHP-Datei, die im Pool lĂ€uft, hat die Rechte des Pools: sie liest per getenv() das DB-Passwort (Weg „Passwort“) und
root.key (der Pool muss ihn lesen, sonst keine TOTP-PrĂŒfung). Eine .env-Datei statt Pool-Variablen Ă€ndert daran nichts — was
der Pool lesen muss, liest auch Code im Pool. Die Abwehr ist deshalb IntegritĂ€t, nicht Geheimhaltung: Code root-eigen und fĂŒr
den Pool nicht schreibbar, die App schreibt nur nach state/ (außerhalb von public/), disable_functions, Manifest-Scan.

8.7 Weg „IdentitĂ€t“ (optional, DB_AUTH=socket)

Statt eines Passworts ist der Pool jemand: eigener OS-Benutzer je Instanz, MySQL-Benutzer gleichen Namens
IDENTIFIED WITH auth_socket, Anmeldung ĂŒber den Unix-Socket. Im Pool steht kein Passwort mehr.

Passwort (Standard) IdentitÀt
Pool-Benutzer www-data eigener je Instanz (useradd --system 
 ozm01; legt bend.sh nicht an)
DB-Anmeldung TCP + tlsgate_app + Passwort Socket, ozm01@localhost per auth_socket
state/, root.key www-data / root:www-data 0640 ozm01 / root:ozm01 0640 in eigenem Verzeichnis
CLI (root) Passwort aus /root/.tlsgate.env unverÀndert Passwort aus der Umgebungsdatei der Instanz

Was es bringt: kein mitnehmbares DB-Geheimnis (nicht in Pool-Datei, Backups, phpinfo-Resten) und keine Nachbarn — im Container
belegt mit zwei Instanzen: Pool A liest weder SchlĂŒssel, state/, Umgebungsdatei noch Pool-Datei von B und kommt nicht in B's DB,
auch ohne open_basedir (das OS sperrt). Was es nicht bringt: gegen Code im eigenen Pool hilft es nicht (§8.6).
Nachbauen: tests/identitaet/instanz.sh, tests/identitaet/probe.sh (nur im Wegwerf-Container).

Status RC1.0: auf der Review-Instanz nicht getestet (dort nur ein Pool), fĂŒr die finale Konfiguration vermerkt, nicht
entschieden
. UNKNOWN: Umstellen einer laufenden Instanz, MODE=restore mit socket, MariaDB (unix_socket), CLI ohne Passwort.

8.8 IntegritÀt und Reaktion

Der Scan erkennt, er verhindert nicht: im Fenster bis zum nĂ€chsten Lauf (≀ 10 min) kann eine fremde Datei den SchlĂŒssel lesen.
Nach einem Befund gelten fĂŒr diese Instanz SchlĂŒssel, Sitzungen und alles, was der SchlĂŒssel öffnet, als bekannt. Mindestens:
Datei sichern (Beweis) und entfernen, Audit und auth.log lesen, DB-Passwörter neu (Umgebungsdatei, Pool, bend.sh), SOAP-/NetBox-
ZugĂ€nge beim Anbieter neu und in TLS-Gate ersetzen, alle Konten reset-mfa. Ob zusĂ€tzlich der WurzelschlĂŒssel rotiert wird
(key rotate + key migrate), entscheidet der Mensch im Raum — nicht festgelegt.

8.9 SouverÀnitÀt

Keine externen CDNs, Schriften oder Bibliotheken; keine Telemetrie; das einzige Cookie ist die Sitzung. Ausgehend nur zu den
Zielen, die eine Gruppe als Zugang hinterlegt (Registrar per SOAP, NetBox), optional eingeschrÀnkt per Allowlist.


9. Wenn etwas rot ist

Zeichen Ursache Tun
CLI: TLS_GATE_STATE_DIR fehlt, Exit 2 Umgebung nicht geladen bin/tg 
 bzw. bin/tg-dash statt php bin/

CLI: Exit 3 Guard: nicht root / nicht Besitzer als root bzw. per sudo
Web 503 App erreicht die DB nicht Pool-Umgebung (env[...]) gegen /root/.tlsgate.env; Doktor Abschnitt 5b
Web 500 PHP-Fehler im Pool Pool-Errorlog (/var/log/php8.4-fpm-tlsgate.log); fehlende Extension in der FPM-SAPI?
migrate: Fehler 1419 Binlog an, DDL-Benutzer ohne SET_USER_ID (MySQL 8.0) bzw. MySQL ≄ 8.2, das SET_USER_ID nicht mehr kennt (SET_ANY_DEFINER genĂŒgt nicht) bend.sh vergibt SET_USER_ID bzw. schaltet log_bin_trust_function_creators nur fĂŒr die Dauer von migrate ein und danach zurĂŒck; von Hand: DBA
Dump einspielen: Fehler 1064 bei TRIGGER Dump einer Alt-Instanz (§7) Dump mit bend.sh ziehen (repariert)
Scan: NEU public/
 Datei außerhalb eines Deploys §8.8
Login: Code „falsch“, obwohl richtig Code im selben 30-s-Fenster schon benutzt (Replay-Schutz) oder Uhr der Node falsch nĂ€chsten Code nehmen; timedatectl
bend.sh rot in Schritt 6 Benutzer-Schritt (Review-Dummy, erster Admin) Protokoll Abschnitt == 6; Schritte 7–10 sind dann nicht gelaufen
bend.sh rot in Schritt 8 beim ersten Lauf Pool/vhost fehlen Vorlagen einsetzen (§3.1)

10. Bekannte Reste und Offenes (RC1.0)

  • Ungenutzte Tabellen bleiben (Entscheidung RC1.0): Bestellschicht und Node-CA des Ursprungs-Repos (cert_orders, order_events,
    order_checks, cert_revocations, device_certs, device_cert_revocations, assets) stehen im Schema und werden migriert, damit
    Migrator und Bestands-DBs unverÀndert bleiben. Kein Code in diesem Repo schreibt sie.
  • database/order_states.json, deploy/nginx.conf.example gehören zum Ursprungs-Repo (nginx-Node, Bestellschicht); die RZ-Node ist Apache.
  • src/Modules/Chain.php (Modulkette) wird nur angezeigt (Dashboard), sperrt keine Seite.
  • Nicht aufgerufene Methoden (grep ĂŒber das ganze Repo, §11): CredentialStore::byId (Bestellschicht), Chain::requireModule
    (Modulkette nicht verdrahtet), Getter SoapCredential::groupId, NetBoxCredential::groupId, NetBoxCredential::kontoBindung.
    Bleiben bis zur Entscheidung ĂŒber die Altlasten; keine davon ist von außen erreichbar.
  • Review-TSV und Manifest gelten je Host, nicht je Instanz (mehrere Instanzen auf einem Host: §8.7).
  • Offen seit M1: WebAuthn/Passkeys, Abgleich gegen geleakte Passwörter, Aufbewahrungsfrist fĂŒr audit_log/login_attempts,
    Benutzerverwaltung fĂŒr group_admin (heute nur admin), eigenes Passwort im Web Ă€ndern.
  • ACME (acme.sh-Worker, im Ursprungs-Repo als M6 geplant): nach RC1.0, nicht Teil dieses Stands.

11. PrĂŒfstand RC1.0 (OZM Admin Audit, 2026-10-16)

Ein Pool, Weg „Passwort“, Wegwerf-Container wie die Review-Instanz: Ubuntu 22.04 mit systemd, PHP 8.4.26, MySQL 8.0.46, Apache 2.4.

PrĂŒfung Ergebnis
bend.sh MODE=review Schritte 0–10 grĂŒn; 7 SOAP/NetBox UNKNOWN (keine Tokens); Doktor 0 FAIL; Gegenprobe 49 grĂŒn / 0 rot
Snapshot verschlĂŒsselt, Restore-Probe: eingespielt und gleich, 16 Trigger
Restore in frische Maschine, neue DB-Passwörter grĂŒn, alle Konten mit alten TOTP-Secrets angemeldet (49 / 0) — 2026-10-15
bin/tg-dash status alles ok (Exit 0)
tests/run_all.sh 17 Suiten, 3 LĂ€ufe hintereinander vollstĂ€ndig grĂŒn; test_roles 142 / 0
eingeschleuste info.php / env.php phpinfo gesperrt (HTTP 500, kein Passwort im HTML); Scan meldet beide als NEU, syslog, Doktor rot
IdentitĂ€t (zwei Pools, auth_socket) im eigenen Container grĂŒn, „A liest B nicht“ in beide Richtungen (§8.7); auf der Review-Instanz nicht geprĂŒft
nicht aufgerufener Code 5 von 326 Methoden in src/ (§10); der Code-Graph schließt bin/ und public/ aus und taugt dafĂŒr allein nicht

Im Zuge des Audits behoben: Test-Hilfe perm() las unter GNU-stat Dateisystem-Infos statt Rechte (der SchlĂŒssel war immer 0400);
Testserver bekamen feste 5 s ohne PrĂŒfung und wackelten bei langsamem Kaltstart (jetzt tg_wait_web, bis 15 s, sonst klarer Fehler);
test_login_instance ließ seine Test-DB liegen.

Echte Hardware im VPN (2026-10-17, cruncher): Raspberry Pi 5, Debian 12 arm64, PHP 8.4.26 (sury), MySQL 8.4.11 (Oracle-Tarball,
signaturgeprĂŒft), Apache nur an Loopback und wg0: bend.sh MODE=review 0–10 grĂŒn (Doktor 0 FAIL, Gegenprobe 49/0, Restore-Probe
16 Trigger). Gefunden und behoben: MySQL 8.4 kennt SET_USER_ID nicht mehr (Fehler 1419 bei migrate). Aus dem VPN login.php 200,
http→https, Pfade außerhalb public/ abgelehnt; aus dem Koffer-LAN keine Verbindung auf 80/443/3306. Das Exponat auf derselben Kiste
lief durch. Umgebung: Ansible-Crumbforest:playbooks/cruncher-tlsgate-env.yml.

Live (omc33, von Branko): erste native Instanz lĂ€uft, Gruppe bwa mit Admin und User getestet, weitere Gruppe ĂŒbertragen, alles im
Audit; Passwörter der getesteten Konten gesperrt bzw. zurĂŒckgesetzt. Letzter bend.sh-Lauf dort (20261016T171017Z) rot in Schritt 6 —
Schritte 7–10 sind dort nicht gelaufen; vor der Freigabe einen vollstĂ€ndig grĂŒnen Lauf (oder MODE=prod) auf der Instanz.

12. Herkunft

Herausgelöst aus native_php/ von TLS-Gate-Mock-Pipeline (main 35451b0). Dort liegen weiter die Entwurfsdokumente, auf die
Kommentare im Code als TLS-Gate-Mock-Pipeline:docs/
 verweisen (Mandanten, Katalog/Buchungen, NetBox-Connector, Dashboard,
Architektur), sowie Bash-Dashboards, Docker-VM, Node-CA und Bestellschicht.