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 aber cgroup: host mit rw-cgroup (steht in compose.yml).
  • Freie Host-Ports 18080 (Web) und 2222 (ssh). Auf 8080 liegt oft serve.sh und ĂŒberdeckt das Mapping.
  • ssh, ssh-keygen, curl, docker auf 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 add mit vorhandenem Slug endet mit einem PDOException-Stacktrace~~ — seit v3.2.1 behoben (ebenso user add mit vorhandener E-Mail und cred add mit vorhandenem Label: ein Satz, Exit 1).
  • Ein Neubau des Images erzeugt neue ssh-HostschlĂŒssel.
  • Volumes ĂŒberleben down; instance.sh danach neu laufen lassen.
  • calls.log des Fake-Registrars muss www-data gehö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.