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
- 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. - Die Umgebung (Webserver, PHP, MySQL, Zertifikat, root) gehört dem Rechenzentrum. TLS-Gate installiert nichts, es wird mit
deploy/node/bend.shauf die vorhandene Umgebung gebogen. - Jeder Fehler ist rot und hÀlt an. Nichts geht still weiter.
- Ein Backup zÀhlt erst, wenn es eingespielt wurde: jeder Snapshot wird vor dem Verpacken probeweise eingespielt.
- 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) oderreseller. SSL-Seiten nur fĂŒreigen(Auth::sslAllowed). administ global und gehört nur in eine Gruppe vom Typeigen;user add ⊠adminin einer Reseller-Gruppe wird abgelehnt.- Rolle, Gruppe und Aktiv-Status werden bei jedem Request aus der DB gelesen: ein Rollenwechsel,
lockoderdisablewirkt
sofort, auch auf laufende Sitzungen. - MFA-Modus je Gruppe:
code(Secret einmal einrichten, dann Codes) oderqr(nach jedem Passwort-Login ein neues Secret).
Neue Gruppen bekommenTLSGATE_MFA_DEFAULT(Standardqr). - Buchungen (
src/Catalog/Bookings.php):readonlybucht nichts; einusernur fĂŒr sich; der Admin der Gruppe fĂŒr Personen
seiner Gruppe; Kategorien der Achse âaccessâ nur fĂŒr Konten mit Rolleadmin. 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 ĂŒberschreibtbend.shnie. - 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ĂŒrtotp,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 beivar_dump/json_encode/serialize. - WSDL nur
https://;TLSGATE_SOAP_HOSTSschrĂ€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 rotatehÀngt eine neuekey_idan; alte bleiben lesbar, biskey migrateneu 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 vonpublic/von auĂen ab und erwartet, dass keiner 200 liefert. - Ein eigener FPM-Pool (
deploy/node/tlsgate-pool.conf.example):clear_env = yes,open_basedirauf 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.phpinfosteht dort, weil eineinfo.phpâ der erste Reflex
nach jedem Install â sonst$_SERVERmit dem DB-Passwort des Pools im Browser zeigt. - Hinter einem Proxy muss die echte Client-Adresse als
REMOTE_ADDRankommen (mod_remoteip);X-Forwarded-Forwird 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.examplegehö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), GetterSoapCredential::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ĂŒrgroup_admin(heute nuradmin), 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.