Handbuch â die Node im Container (docker_vm)
Zusammenfassung des VM-Setups, Stand 1.10.2026 (v3.0.4 + P4). Ziel: Wer dieses Handbuch von oben nach unten ausfĂŒhrt, hat eine Node mit Web-Login und ssh,
und kann belegen, dass jeder Zugang an genau einem Zeichen hĂ€ngt. Daraus lassen sich die ĂŒbrigen Repos ableiten (Abschnitt 8).
Hintergrund und Entscheidungen: PLAN.md, Kurzreferenz: README.md.
Grundlagen (Nullfeld): https://git.crumbforest.org/branko/Bashpanda-Admin-Tools-v.0.0
Alles hier lĂ€uft gegen einen Fake-Registrar im Container. Kein echter Registrar, keine CA, nur Wegwerf-SchlĂŒssel und Testpasswörter.
1. Voraussetzungen
- Docker Desktop (oder Docker mit cgroup-v2-Host). systemd lÀuft als PID 1 ohne
privileged, braucht abercgroup: hostmit rw-cgroup (steht incompose.yml). - Freie Host-Ports 18080 (Web) und 2222 (ssh). Auf 8080 liegt oft
serve.shund ĂŒberdeckt das Mapping. ssh,ssh-keygen,curl,dockerauf dem Host. Alle Befehle vom Repo-Root.
2. Compose: Node starten
docker compose -f docker_vm/compose.yml up -d --build
docker exec tlsgate-vm systemctl is-system-running # -> running
Ein Container, drei Dienste (nginx, MariaDB 11.8, php-fpm 8.4), drei benannte Volumes (mysql, home, provision).
compose down behÀlt die Daten, down -v löscht alles. Nach down/up die Instanz neu angleichen (Abschnitt 3); Daten und Secrets bleiben.
3. Instanz anlegen
docker exec tlsgate-vm /opt/provision/instance.sh ozm01 # idempotent; keine Host-Einschraenkung: SOAP-Zugaenge legt man danach in credentials.php oder per cred add an
Ein zweiter Lauf meldet keine Aenderung (Sollstand erreicht). Je Instanz: eigener Benutzer (ozm01 = UID 2001, ozm02 = 2002),
/home/<name>/{app,bin,state,logs}, eigenes MariaDB-Schema mit <name>_ddl (nur migrate) und <name>_app (nur DML), eigener php-fpm-Pool, Vhost <name>.localhost.
3a. SOAP-Zugang anlegen
Die Instanz kennt keinen Registrar, bis ein Zugang hinterlegt ist (Web: Kopfzeile âSOAP-ZugĂ€ngeâ = credentials.php, nur admin/group_admin; oder CLI). Jede https-WSDL ist erlaubt.
Gespeichert wird nur versiegelt, gebunden an Gruppe + Label; Benutzer und Passwort erscheinen nie wieder.
# CLI (Gruppe = Instanzname, Label frei): Benutzer und Passwort kommen von stdin
printf 'USER\nPASS\n' | docker exec -i tlsgate-vm /opt/provision/instance-cli.sh ozm01 cred add ozm01 registrar-a https://<registrar>/<wsdl>
docker exec tlsgate-vm /opt/provision/instance-cli.sh ozm01 cred test ozm01 registrar-a # lesender Aufruf, "OK: ... Zertifikat(e) sichtbar"
# nur zum Ueben gegen den Fake-Registrar im Container (fakeuser / fakepass):
printf 'fakeuser\nfakepass\n' | docker exec -i tlsgate-vm /opt/provision/instance-cli.sh ozm01 cred add ozm01 fake https://soap.fake.test/wsdl
Ein zweites cred add mit demselben Label meldet âZugang '
3b. Deploy-ZIP und ssl_outbox
In der Detailansicht einer Bestellung (SSL-Konsole, âFetch Order Infoâ) gibt es âDeploy-ZIPâ. Es liefert <domain>.zip mit <domain>/<domain>.cert und <domain>/<domain>.chain.pem
(dieselben Dateien wie dashboard.sh deploy in native_bash_cli), nie einen Key. Dieselben Bytes liegen in der Outbox:
/home/<name>/state/ssl_outbox/<gruppen-id>/<domain>.zip # Ordner 0700, Datei 0640, Besitzer = Instanz; Wildcard "*." wird zu "_."
Umlenkbar mit TLSGATE_OUTBOX_DIR (z. B. ein Mount, den die Orchestrierung einsammelt). Ab Rolle user, CSRF, Audit ssl.deployZip (ok mit SHA-256). Abnahme: docker_vm/tests/p6.sh.
3c. Vom Nullpunkt: down -v und alles neu
docker compose -f docker_vm/compose.yml down -v && docker compose -f docker_vm/compose.yml up -d --build
for n in ozm01 ozm02; do docker exec tlsgate-vm /opt/provision/instance.sh $n; done # Benutzer, DB, Pool, Vhost, Admin
# SSL-Zugang je Instanz neu (3a), ssh-Schluessel neu (Abschnitt 5: erst `install -d /root/.provision/ssh`)
for t in run p3 p4 p5 p6; do docker_vm/tests/$t.sh | tail -1; done # 52 / 48 / 54 / 31 / 22
Am 2.10.2026 so durchgelaufen, alle grĂŒn. Die Admin-Passwörter sind danach neu (nur in /root/.provision/<name>.env), die TOTP-Einrichtung beginnt wieder mit dem QR.
4. Web-UI-Login
docker exec tlsgate-vm cat /root/.provision/ozm01.env # ADMIN_PASS (nur root lesbar); Admin: admin@ozm01.localhost
Browser: http://ozm01.localhost:18080/ (*.localhost löst der Browser selbst auf). Per curl: curl -H 'Host: ozm01.localhost' localhost:18080/login.php.
Ablauf: Passwort â beim ersten Login Enrollment mit QR (SVG, Secret, otpauth-URI) â TOTP-Code â Konsole mit den Mock-Bestellungen.
Der QR kommt beim Login nur einmal (Absicht, Auth::enrollmentData()); der zweite Login zeigt nur den Code-Schritt.
TOTP im Alltag der VM (alle Befehle laufen im Container; php bin/user_cli.php auf dem Mac arbeitet gegen die lokale SQLite-DB, nicht gegen die VM: Antwort Unbekannt):
C="docker exec tlsgate-vm /opt/provision/instance-cli.sh ozm01"
$C user list # Spalte âmfaâ = TOTP aktiv, âmfa-offenâ = Einrichtung steht aus
# Code ohne App (Dev/Test, Àndert nichts). Er gilt 30 s: bei wenig Restzeit warten, dann sofort eingeben
s=$(( $(date +%s) % 30 )); [ $s -gt 15 ] && sleep $((31 - s)); $C user code admin@ozm01.localhost
# Neuer QR â Weg A, angemeldet (ab v3.1.0): im Kopf der Konsole â2FAâ (mfa.php) â Passwort â QR scannen â ersten Code eingeben.
# Das alte Secret gilt, bis der Code des neuen stimmt; âAbbrechenâ Ă€ndert nichts.
# Neuer QR â Weg B, Admin ausgesperrt oder App weg: zurĂŒcksetzen, der nĂ€chste Login zeigt den QR wieder
$C user reset-mfa admin@ozm01.localhost
# Kopfloses Setup: TOTP sofort aktiv, Secret + URI im Terminal (das Secret landet im Terminal-Verlauf)
$C user enroll admin@ozm01.localhost
# QR im Terminal (-it nötig), nur wenn MFA noch offen ist (sonst erst reset-mfa)
docker exec -it tlsgate-vm /opt/provision/instance-cli.sh ozm01 user totp-setup admin@ozm01.localhost --watch
Nach down -v oder neuem Provisionieren ist das Admin-Passwort neu (ADMIN_PASS oben) und das TOTP offen. Ein altes Passwort im Browser-Speicher
fĂŒhrt zu bad_password im Audit ($C audit 5), nicht zu einer Sperre; erst fĂŒnf Fehlversuche sperren das Konto 15 min (user unlock <email>).
Die Instanz hat kein TLS, deshalb TLSGATE_COOKIE_SECURE=0 im Pool â im Betrieb entfernen.
Anmeldung geht nicht? bin/login.doktor.sh [URL] [Instanz] [E-Mail] prĂŒft Port, Vhost, Cookie, Konto und die letzten Audit-EintrĂ€ge (die hĂ€ufigste Ursache: Port in URL bzw. hosts vergessen, http://ozm01.localhost:18080).
Neues QR bei jedem Login (Demo): instance.sh ozm01 mock qrlogin setzt TLSGATE_MFA_QR_EVERY_LOGIN=1 im Pool. Jeder Passwort-Login ersetzt das TOTP-Secret und zeigt das QR; wer das Passwort kennt, sieht es, TOTP ist dann kein zweiter Faktor. Mit instance.sh ozm01 mock wieder aus. Die Abnahmen laufen ohne diese Option.
Je Gruppe statt je Instanz (ab v3.4.0): instance-cli.sh ozm01 group mfa <slug> qr (zurĂŒck: code) setzt groups.mfa_policy; kein Pool-Neustart nötig. Gleiche Sicherheitsfolge.
Gesperrt wird eine IP nach 10 Fehlversuchen in 15 min. Ein Fehlversuch ist kein Fehler des Systems: falsches Passwort heiĂt âabgelehntâ, nicht Konto gesperrt.
5. SSH mit Wegwerf-SchlĂŒsseln
Port 2222. Nur Instanz-Benutzer mit hinterlegtem SchlĂŒssel; kein root, kein Passwort, keine Tunnel (AllowGroups ssh-instances).
authorized_keys gehört root, der Benutzer kann seine SchlĂŒssel nicht Ă€ndern. Ohne Datei /root/.provision/ssh/<name>.pub gibt es keinen Zugang.
ssh-keygen -q -t ed25519 -N '' -C tlsgate-testdummy -f docker_vm/.demo-key # gitignored, nie committen
docker exec tlsgate-vm install -d -m 700 /root/.provision/ssh # fehlt nach "down -v", sonst scheitert docker cp
docker cp docker_vm/.demo-key.pub tlsgate-vm:/root/.provision/ssh/ozm01.pub
docker exec tlsgate-vm /opt/provision/instance.sh ozm01 # setzt authorized_keys
ssh -p 2222 -i docker_vm/.demo-key -o IdentitiesOnly=yes -n ozm01@127.0.0.1 'bash -lc dashboard.sh'
bash -lc ist nötig: dashboard.sh liegt nur im PATH der Login-Shell. -n verhindert, dass dashboard.sh unter nicht-interaktivem ssh auf stdin hÀngt.
Nach einem Image-Neubau gibt es neue HostschlĂŒssel: ssh-keygen -R "[127.0.0.1]:2222".
6. Der Beweis: ein Zeichen geÀndert, es bricht
Am 1.10.2026 live gegen den laufenden Container ausgefĂŒhrt:
| Versuch | Ergebnis |
|---|---|
ozm01 mit dem richtigen SchlĂŒssel |
Login als ozm01, Dashboard kommt |
| derselbe SchlĂŒssel, ein Zeichen im Base64-Teil geĂ€ndert (Diff: 4 Zeilen = 1 Zeile) | Permission denied (publickey), Exit 255 |
richtiger SchlĂŒssel, aber als root |
Permission denied (publickey) |
SchlĂŒssel von ozm01 auf ozm02 |
Permission denied (publickey) |
Das Web-GegenstĂŒck liefern die Suiten: richtiges Passwort + richtiger TOTP-Code â Konsole 200; falsches Passwort oder falscher Code â abgelehnt und im Audit-Log.
Der Admin von ozm01 kommt am Vhost von ozm02 nicht hinein.
Gesamtabnahme (Container muss laufen, wiederholbar, jede Suite zÀhlt gruen: N rot: M):
docker_vm/tests/run.sh # P2: ozm01 komplett, Web + Bash 52 PrĂŒfungen
docker_vm/tests/p3.sh # P3: Fake-Registrar, 500er nachgestellt, mbstring, Mock an/aus, Zugang mit fremdem WSDL, Dubletten 48
docker_vm/tests/p4.sh # P4: ozm02, Isolation, Guard, Logs, Reseller-Sperre, ssh 54
docker_vm/tests/p6.sh # P7: Deploy-ZIP (outbox.php), ssl_outbox, Audit 22
tests/test_dates.sh # dashboard.sh: Unix-Zeitstempel 16
Alle am 2.10.2026 grĂŒn, auch frisch nach down -v (52 / 48 / 54 / 22 / 16; dazu p5.sh 31). Die Gegenprobe ist Teil der Tests: ozm01 liest ozm02 und scheitert wie vorgesehen; der alte View.php aus v3.0.1 liefert mit einer Struktur im Feld validationDNS HTTP 500 â der RZ-Fehler ist damit nachgestellt.
7. Danach: Doktor, dann Deploy
7a. TLS-Doktor (bin/TLS-doktor.bash)
Nur lesend. PrĂŒft pro PHP-Binary pdo_sqlite, sodium, mbstring, posix, PASSWORD_ARGON2ID, Schema und die Rechte/Besitzer von state/. Behebungen schlĂ€gt er vor, fĂŒhrt sie nicht aus.
docker exec tlsgate-vm bash -c '. /root/.provision/ozm01.env; env WEB_USER=ozm01 TLS_GATE_STATE_DIR=/home/ozm01/state TLSGATE_ROOT_KEY_FILE=/home/ozm01/state/root.key \
TLSGATE_DB_DRIVER=mysql TLSGATE_DB_NAME=ozm01 TLSGATE_DB_USER=ozm01_app TLSGATE_DB_PASS="$APP_PASS" /home/ozm01/app/bin/TLS-doktor.bash php8.4'
In der Container-Node (MariaDB, nginx, php-fpm) mit der Umgebung des Pools, als root (dann prĂŒft er per runuser, was der Pool-Benutzer lesen und schreiben kann):
docker_vm/tests/p5.sh # Sollstand + Drills + Pool-Check, 31 PrĂŒfungen
Zwei Treiber, ein Doktor (Stand 1.10.): TLSGATE_DB_DRIVER=sqlite (Standard, Offline-Dev: bin/TLS-doktor.bash auf dem Rechner) prĂŒft pdo_sqlite, DB-Datei, Rechte 0600.
TLSGATE_DB_DRIVER=mysql (VM, RZ-Node) macht pdo_mysql zur Pflicht, liest Schema und Version ĂŒber MariaDB und sucht keine SQLite-Datei; pdo_sqlite ist dort nur ein Hinweis.
WEB_USER=<pool-user> sagt ihm, wessen Rechte er prĂŒfen soll (bei php-fpm der Instanz-Benutzer, nicht www-data).
Drills (P5), je mit Klartext und Exit > 0, danach wieder grĂŒn: root.key 0644 · falsches DB-Passwort · MariaDB gestoppt · state/ gehört root statt der Instanz · Guard aus (Warnung).
Zuvor brach der Doktor unter GNU ab: stat -f ist dort Dateisystem-Info und âgelingtâ mit MĂŒll statt Rechten; jetzt wĂ€hlt stat --version die Syntax.
Pool-Check (P6): Gibt es php-fpm, liest der Doktor per php-fpm -tt die wirksame Konfiguration und nimmt den Pool, der als WEB_USER lĂ€uft (als root ausfĂŒhren). Er meldet
disable_functions fehlt oder lÀsst eines von exec passthru shell_exec system proc_open popen zu (FAIL, nennt die Funktion), open_basedir = / (FAIL) oder fehlend/ohne state/ (warn), clear_env nicht yes (warn), display_errors an (FAIL), Pool als root (FAIL).
Ohne php-fpm (macOS, php -S, mod_php) entfÀllt der Abschnitt. Der Drill p5.sh verÀndert die Pool-Datei je um eine Kleinigkeit und stellt sie wieder her. Gefunden dabei: php-fpm -tt schreibt Flags als php_admin_value[...] = 0/1, nicht als php_admin_flag.
7b. Deploy (deploy/README.md)
Der Container ist die Probe fĂŒr denselben Ablauf auf der RZ-Node. Zuordnung:
Schritt in deploy/README.md |
Im Container (instance.sh) |
|---|---|
1. Code, DocumentRoot = public/ |
/home/<name>/app, Webroot app/public, src/ bin/ state/ unerreichbar |
| 2. Laufzeitdaten nur fĂŒr den Web-User, 0700 | /home/<name>/state 0700 |
| 3. WurzelschlĂŒssel getrennt von der DB | state/root.key 0400, je Instanz verschieden |
4. migrate, Gruppe, Admin, cred add |
user_cli.php als Instanz-User; DDL-Passwort nur im Env des Aufrufs |
| 5. Vorlage anpassen, Webserver neu laden | php-fpm-Pool + nginx-Vhost je Instanz aus provision/templates/ |
| 6. Gegenprobe mit denselben Pfaden | Doktor (7a, p5.sh) + curl-PrĂŒfungen der Suiten (src/, bin/, state/, .git nicht ausgeliefert) |
Auf der Node bleibt zu tun, was der Container bewusst nicht kann: TLS, echter Registrar, TLSGATE_COOKIE_SECURE zurĂŒck auf 1, Zugangsdaten nicht im Klartext im Pool (LoadCredential/Secret-Mount).
Test grĂŒn heiĂt nicht, dass Apache/nginx lĂ€uft: Erweiterungen, Besitzer und derselbe State-Pfad mĂŒssen im Webserver-Prozess stimmen â dafĂŒr ist der Doktor da.
8. Repos ableiten
Alles, was ein neues Repo braucht, steht schon als Datei fest:
| Baustein | Quelle hier | Eigenes Repo |
|---|---|---|
| Node-Image (Debian, systemd, nginx, MariaDB, php-fpm, sshd) | Dockerfile, conf/, compose.yml |
Basis-Image/Compose |
| Instanz-Provisionierung (idempotent) | provision/instance.sh, templates/ |
Provisioning |
| Fake-Registrar | fake-soap/ |
Testdouble |
| Abnahme (Beweis) | tests/lib.sh, run.sh, p3.sh, p4.sh |
Abnahme-Suite |
| Anwendung | native_php/, native_bash_cli/ |
unverÀndert kopiert |
Prinzip aus dem Nullfeld: Was sich nicht per Test brechen lĂ€sst (ein Zeichen anders â Zugang verweigert, Fremdlesen scheitert), gilt nicht als belegt.
9. Bekannte Punkte
- ~~
group addmit vorhandenem Slug endet mit einemPDOException-Stacktrace~~ â seit v3.2.1 behoben (ebensouser addmit vorhandener E-Mail undcred addmit vorhandenem Label: ein Satz, Exit 1). - Ein Neubau des Images erzeugt neue ssh-HostschlĂŒssel.
- Volumes ĂŒberleben
down;instance.shdanach neu laufen lassen. calls.logdes Fake-Registrars musswww-datagehören, sonst schreibt der Dienst still nichts (im Image behoben).
10. Referenz: Doktor-Familie (Puzzle, Stand 1.10.)
Vorlagen aus dem Nullfeld/OZM-Umfeld, die spĂ€ter in ein gemeinsames MenĂŒ passen. Hier nur festgehalten, nichts davon ist eingebaut.
| Doktor | Ebene | Inhalt |
|---|---|---|
backbone_doktor.sh |
Host (Backbone) | Last, RAM, Disk, DNS, ufw, fail2ban, nginx, Zertifikate, Ping zu Nullfeld |
strato_doktor.sh |
Host (Nullfeld) | IPs, Ports 80/443, DNS A/AAAA, HTTP(S) auch ĂŒber IPv6, nginx-Sites, Let's-Encrypt-Ablauf |
debian-doktor.sh (OZM-Keks-Handbuch, Pfad 27) |
Dienste + Logs | MenĂŒ: Dienste/Ports/Health, Tail, Restart, Shaolin-Dojo (Zeitverteilung, P90, Top-URLs/Agents, Fehlersuche) |
bin/TLS-doktor.bash |
Anwendung | PHP-Erweiterungen, DB (sqlite oder mysql), Schema, root.key, state/, Guard; Drills in tests/p5.sh |
WeiterfĂŒhrend: https://www.branko.de/martial/ozm/ninja-dashboard.html (nicht gelesen, nur als Ziel des Puzzles vermerkt).
Zur Verwendung des Dojo auf den Instanzen: Es liest $(NF-1) als Request-Time. Das setzt ein nginx-log_format mit $request_time als vorletztem Feld voraus; die Instanz-Vhosts aus provision/templates/ loggen bisher nicht so (nicht geprĂŒft, ob sie das Standardformat nutzen). Offen, bis jemand es braucht.