Database Gateway
Datenbanken von CMS- und Shopsystemen sicher und temporär per Tool abfragen.
Manchmal brauchst du Daten aus einer fremden Datenbank — der Live-Shop-DB des Kunden, dem Contao- oder WordPress-Backend, einem alten CRM. Direkter SQL-Zugriff von TaskMonkey aus geht nicht (Netzwerk, Firewall, Sicherheit). Lösung: das Database Gateway.
Config-Referenz (remote_databases)
Nur diese Schlüssel liest der Code (src/Tooling/DatabaseGatewayTools.php, SshKeyResolver, DatabaseConnector). Keine Schlüssel erfinden: writeTenantConfig lehnt unbekannte ab (z. B. web_url → gemeint ist base_url). Datei: database_connections.php, Key darin: remote_databases.
| Schlüssel | Bedeutung | Beispiel |
|---|---|---|
mode |
persistent = Connector (kein SSH, Datei liegt dauerhaft beim Kunden). Weglassen (oder ssh/temporary) = SSH-Gateway |
'persistent' |
hostname |
SSH-Server (Gateway). Ohne base_url auch die Domain der Gateway-URL |
'p611804.mittwaldserver.info' |
remote_user |
SSH-Benutzer | 'p611804' |
ssh_key |
privater Schlüssel, nur als Secret | '{{secret:SSH_KUNDE_KEY}}' |
key_passphrase |
Passphrase des Schlüssels, nur als Secret | '{{secret:SSH_KUNDE_PASSPHRASE}}' |
web_root |
Verzeichnis auf dem SSH-Server, in das das Gateway-Script per SCP kommt. Muss öffentlich per HTTP erreichbar sein (Contao: web/ bzw. public/) |
'/html/kunde/public' |
base_url |
öffentliche https-Domain, unter der web_root ausgeliefert wird. Fehlt sie, wird https://<hostname> genommen — bei Hostern wie Mittwald fast immer falsch (Ping 404) |
'https://www.kunde.de' |
profile |
System-Profil (auto, contao, wordpress, shopware6, gambio, …), für Produktivsysteme fest setzen |
'contao' |
allow_write |
Obergrenze Schreibrecht (Exposure-Flag, nur Literal true/false, Freigabe im Chat) |
true |
host_key |
Host-Key des SSH-Servers; dann strikte Prüfung statt „beim ersten Kontakt merken“ | 'ssh-ed25519 AAAA…' |
tables |
schränkt das Profil ein | ['read' => ['tl_news*'], 'write' => ['tl_news']] |
extra_tables |
gibt zusätzliche Tabellen frei (Denylist gilt weiter) | ['read' => ['tl_dlh_googlemaps']] |
deny |
zusätzliche Deny-Muster | ['tl_member*'] |
unsafe_allow_denied_tables |
hebt die Denylist für exakte Tabellen auf (nur mit Begründung) | ['tl_log'] |
prefix |
Tabellenpräfix (bei unbekannten Systemen) | 'wp_' |
media_dirs |
Upload nur in diese Unterverzeichnisse | ['files/taskmonkey'] |
upload |
Upload einschränken | ['max_mb' => 5, 'extensions' => ['jpg', 'png']] |
cache_min_interval |
Sekunden zwischen zwei Cache-Leerungen (Default 30) | 60 |
backup_dir |
Backup-Ablage beim Kunden, außerhalb des Web-Roots | '../tm-backups/kunde_web' |
auto_backup / auto_backup_hours |
Backup vor der ersten Änderung einer Sitzung / Sitzungsdauer | true / 12 |
backup_keep_days / backup_keep_max / backup_max_mb |
Rotation und Größengrenze der Backups | 7 / 10 / 1024 |
allowed_ips |
nur Connector: erlaubte Absender-IPs | ['203.0.113.10'] |
state_dir |
nur Connector: Statusdatei außerhalb des Web-Roots | '../tm_state' |
sftp |
nur Connector: Installation per SFTP (hostname, port, username, password, web_root, host_key) |
['hostname' => 'ssh.kunde.de', …] |
local |
nur Tests: Gateway auf dem TaskMonkey-Server | — |
Ältere Schreibweisen, die noch gelesen werden: host (= hostname), user (= remote_user), webRoot (= web_root), key (= ssh_key), passphrase (= key_passphrase). Neu nicht mehr verwenden.
Die Contao-Version nicht beim Kunden erfragen — ensureGateway/deployGateway erkennt System und Version selbst (cms_detected, version).
Hoster Mittwald
- SSH läuft in einer chroot. Für SSH/SCP ist das Home
/, die Seiten liegen unter/html/<ordner>/…— nicht unter/home/pXXXXXX/…oder/home/www/pXXXXXX/…(das sind die Pfade außerhalb der chroot, z. B. in Symlink-Zielen).web_rootalso z. B./html/icon-immobilien.de/web. - Der Ordnername sagt nichts über die Domain.
/html/dev-www.kunde.dekann die Live-Seite sein. Welche Domain auf welchen Ordner zeigt, steht im Mittwald-Kundencenter (Domains → Ziel), nicht im Dateisystem. base_urlimmer setzen = die öffentliche Domain, die denweb_rootausliefert (z. B.https://www.kunde.de). Ohnebase_urlbaut das Gatewayhttps://pXXXXXX.mittwaldserver.info/…— das liefert 404.- Contao 4 (bis 4.13 ohne Umstellung): Web-Root
…/web. Contao 5 (oder 4.13 mitpublic):…/public. Deployer-Setups:…/<ordner>/current/public(currentist ein Symlink aufreleases/<n>; das Gateway landet im Release-Ordner und ist nach dem nächsten Deploy des Kunden weg —ensureGatewaydeployt dann neu). - Pfade selbst prüfen statt den Kunden fragen: sobald SSH steht,
ensureGatewaymit dem wahrscheinlichsten Pfad aufrufen; der Fehler (SCP … No such file or directory) zeigt, ob der Pfad existiert.
Ausführliches Beispiel: Rezept Contao-Redakteur.
Konzept
Statt eine permanente DB-Verbindung zu öffnen, deployt das Gateway-Tool ein kleines PHP-Script über SSH auf den Webserver des Ziels. Das Script erkennt das System (Contao, WordPress, Shopware, …), liest die DB-Zugangsdaten aus dessen Konfiguration, nimmt SQL per POST entgegen, prüft es gegen das System-Profil und gibt JSON zurück. Nach Ablauf löscht es sich selbst.
Kein SSH-Zugang, nur FTP oder Hosting-Panel (typisch bei all-inkl, IONOS, Strato, vielen Gambio-/WooCommerce-Shops)? Dann nimmst du den Connector: dieselbe Datei als Dauerversion, einmal hochgeladen, jeder Aufruf signiert. Siehe Dauerbetrieb ohne SSH (Connector).
Vorteile:
- Kein VPN, kein Tunnel, keine direkte DB-Verbindung
- Token-basiert, zeitlich begrenzt (max. 72 h)
- Read-only oder Read/Write konfigurierbar
- Tabellen-Profil je System, im Gateway selbst erzwungen: Inhalte ja, Benutzer/Passwörter/Sessions/Tokens/Zahlungsdaten/Logs/Konfig nie
- Backups und Rollback beim Kunden (ab Protokoll 5): automatisches Backup vor der ersten Änderung einer Sitzung, jede Schreiboperation einzeln zurücknehmbar — die Daten verlassen den Server nie, siehe Backups und Rollback
- MySQL und MariaDB (was das Ziel-PHP per
pdo_mysqlerreicht, auch über Unix-Socket)
Voraussetzungen
Du brauchst SSH-Zugang zum Ziel-Webserver mit einem Key, dessen privater Teil hinterlegt ist. Pro Ziel hinterlegst du in config/tenants/<workspace>/database_connections.php eine Connection-Config unter dem Key remote_databases.<projekt>:
Datei vs. Key. Die Datei heißt
database_connections.php, der Config-Key darin heißtremote_databases— das ist der einzige Key, den die Tools lesen. Der Dateiname selbst ist kein Key (der Workspace-Loader nimmt die Keys aus demreturn-Array). Ein Top-Level-Keydatabase_connectionsist veraltet: der Loader faltet ihn beim Laden inremote_databasesum; steht dasselbe Projekt unter beiden Keys, gewinntremote_databaseskomplett und es gibt eine Log-Warnung. Neue Configs nur mitremote_databases.
| Feld | Bedeutung |
|---|---|
hostname |
Server-Adresse für SSH |
remote_user |
SSH-User |
web_root |
Pfad, in dem das Gateway-Script abgelegt wird (muss per HTTP erreichbar sein) |
ssh_key |
Privater Schlüssel nur als Workspace-Secret: '{{secret:SSH_KUNDE_KEY}}' (oder secret('SSH_KUNDE_KEY')). Dateipfade werden abgelehnt. Setzen/Ändern nur durch den TaskMonkey-Betreiber. |
key_passphrase |
falls der Key passwortgeschützt ist, ebenfalls als '{{secret:NAME}}' |
base_url |
öffentliche URL des web_root, nur https:// (der Gateway-Token geht im Header mit) |
host_key |
optional: Host-Key des Servers (ssh-ed25519 AAAA… oder known_hosts-Zeile). Dann wird strikt geprüft, sonst beim ersten Kontakt gemerkt |
allow_write |
optional, default false — Obergrenze für Schreib-Gateways dieser Verbindung. Nur mit true können deployGateway/ensureGateway mit allow_write deployen, uploadFile hochladen und executePreparedQuery mit mode: write schreiben. Das Tool-Arg allow_write wirkt nur innerhalb dieser Grenze. Auch Default für refreshDatabaseGateways. Exposure-Flag: per Builder (writeTenantConfig) nur neu setzbar nach Bestätigung im Chat (Tragweite in der Freigabe-Rückfrage, ein Ja), nur als Literal true/false |
profile |
optional, default auto — System-Profil, siehe unten |
tables |
optional — ['read' => [...], 'write' => [...]], schränkt das Profil ein (bei unbekannten Systemen: die Allowlist) |
extra_tables |
optional — ['read' => [...], 'write' => [...]], gibt zusätzliche Tabellen explizit frei (z. B. einer Erweiterung, Bestellungen fürs Reporting). Die Denylist gilt trotzdem |
deny |
optional — zusätzliche Deny-Muster |
media_dirs |
optional — Upload nur in diese Unterverzeichnisse der Profil-Medienverzeichnisse (bei unbekannten Systemen: explizit nötig) |
upload |
optional — ['max_mb' => 5, 'extensions' => ['jpg', 'png']], nur einschränkend (max. 25 MB) |
unsafe_allow_denied_tables |
optional, ausdrückliches Flag — exakte Tabellennamen, für die die Denylist aufgehoben wird. Zusätzlich muss die Tabelle über extra_tables freigegeben sein. Nur mit Begründung im Config-Kommentar |
backup_dir |
optional — Ablage für Backups und Rollback-Journal beim Kunden, relativ zum Gateway-Verzeichnis oder absolut. Default: eine Ebene über dem Web-Root, ../tm-backups/<projekt>/. Muss außerhalb des Web-Roots liegen (das Gateway prüft per realpath und lehnt sonst ab). Nur A-Z a-z 0-9 _ . - / |
auto_backup |
optional, default true — automatisches Backup vor der ersten Schreiboperation einer Sitzung. Nur false schaltet ab |
auto_backup_hours |
optional, default 12 — nach so vielen Stunden beginnt für denselben Chat eine neue Sitzung (neues Backup) |
backup_keep_days / backup_keep_max |
optional, default 7 / 10 — Rotation: Backups älter als N Tage bzw. über der Höchstzahl werden gelöscht (das neueste bleibt immer). Rollback-Journale folgen backup_keep_days |
backup_max_mb |
optional, default 1024 — Abbruch, wenn der SQL-Dump (unkomprimiert) größer wird |
Tabellennamen in tables/extra_tables/deny sind volle Namen (mit Präfix, z. B. tl_news, wp_posts), * ist als Wildcard erlaubt (mind. drei feste Zeichen).
<?php
// config/tenants/<workspace>/database_connections.php
return [
'remote_databases' => [
'kunde_web' => [
'hostname' => 'kunde.de',
'remote_user' => 'p123456',
'ssh_key' => secret('SSH_KUNDE_KEY'),
'base_url' => 'https://kunde.de',
'web_root' => '~/html/kunde.de/current/public',
'profile' => 'contao',
'allow_write' => true, // Obergrenze: ohne das nur lesend
// nur News pflegen:
'tables' => ['read' => ['tl_news*', 'tl_content', 'tl_files'], 'write' => ['tl_news', 'tl_content']],
],
],
];
Tools
| Tool | Zweck |
|---|---|
ensureGateway |
Standard-Einstieg. Nutzt ein laufendes Gateway weiter; neu deployt wird nur bei < 6 h Restlaufzeit, fehlendem Schreibrecht, veralteter Gateway-Version, geänderter Connection-Config oder wenn es nicht antwortet |
deployGateway |
Deployt immer ein neues Gateway (neuer Token, 24 h default, max. 72 h) und entfernt das alte Script |
executeQuery |
Genau ein SQL-Statement an das aktive Gateway, Ergebnis als JSON. Gesperrt über API-Keys, öffentliche Tools (public_api), eingehende Webhooks und im Read-only-Kontext |
executePreparedQuery |
Festes SQL mit ?-Platzhaltern + params als Prepared Statement. mode: read (nur SELECT, in READ ONLY-Transaktion) oder write. Baustein für Tools, die per API-Key erreichbar sind |
uploadFile |
Datei ins Medienverzeichnis des Systems hochladen (braucht allow_write in der Verbindung und am Gateway, nicht im Read-only-Kontext); bei Contao Registrierung im DBAFS |
clearCmsCache |
HTTP-Cache des Zielsystems nach einer Änderung leeren (derzeit nur Contao, ab Protokoll 4). Mit Tags (contao.db.tl_page.12) und Shell gezielt per contao-console fos:httpcache:invalidate:tag, sonst wird var/cache/prod/http_cache geleert. Braucht Schreibrecht |
readCmsTemplate |
Templates des Zielsystems lesen (derzeit nur Contao, Gateway-Actions template_list/template_read, auch ohne Schreibrecht): nur unter contao/templates/, templates/ (inkl. Theme-Ordner aus tl_theme.templates) und local/templates/ relativ zum Contao-Root, Endungen .html5/.twig/.php, max. 256 KB, als Text — nie ausgeführt, kein Schreibweg. Gateway prüft per realpath (keine Traversal, keine Symlinks hinaus). Ältere Gateways: ensureGateway deployt neu (Script-Hash steckt im Deploy-Hash), Connector neu herunterladen |
createContaoVersion |
Aktuellen Stand eines Contao-Datensatzes (tl_page, tl_article, tl_content, tl_news, tl_calendar_events, tl_faq) als Version in tl_version ablegen (Benutzer „TaskMonkey“, im Backend wiederherstellbar). Das Gateway liest den Datensatz selbst; tl_version bleibt für SQL gesperrt. Ab Protokoll 4, braucht Schreibrecht |
listTables / describeTable |
SHOW TABLES (zeigt nur erlaubte Tabellen) / DESCRIBE |
getGatewayStatus |
Existiert ein Gateway, ist es aktiv, wann läuft es ab, darf es schreiben, ist es veraltet? Bei Connectoren: letzter Kontakt, Fehlschläge in Folge, Key-Version. Ohne Token/Secret |
removeGateway |
Räumt das Script wieder ab (einen Connector nicht — der wird nur im Manage-Bereich deinstalliert) |
refreshDatabaseGateways |
Täglicher Redeploy (Token-Rotation), siehe unten. Bei Connectoren: Secret-Rotation |
pingDatabaseConnector |
Health-Check eines Connectors (signierter Ping), zählt Fehlschläge in Folge |
checkDatabaseConnectors |
Scheduled Health-Check aller Connectoren, Admin-Mail nach 2 Fehlschlägen in Folge |
createDatabaseConnector |
Builder: Connector-Datei erzeugen und als Download-Link liefern — mit Freigabe-Schritt (Vorschlag → Ja → confirm=true). Start-Secret nie im Result/Log |
installConnectorViaSftp |
Builder: Connector per SFTP hochladen und testen (braucht sftp mit host_key; ohne host_key nur Fingerprint-Anzeige). Mit Freigabe-Schritt |
getConnectorStatus |
Builder: installiert? Protokoll aktuell? Fehlschläge, letzter Fehler, nächster Schritt. test=true = „Verbindung testen“ (rotiert das Start-Secret beim ersten Kontakt) |
createDbBackup / listDbBackups |
Backup der Schreib-Allowlist jetzt anlegen / Backups auflisten (ab Protokoll 5, siehe Backups und Rollback) |
restoreDbBackup / deleteDbBackup |
Backup wiederherstellen / löschen — nur mit allow_write und nur nach ausdrücklichem Ja im Chat (zweistufig) |
listDbWrites |
Letzte Schreiboperationen aus dem Rollback-Journal (Default: dieser Chat) mit ID |
rollbackLastWrite / rollbackWrite |
Letzte Änderung dieses Chats bzw. eine Änderung per ID zurücknehmen — Vorschau + Ja im Chat, Konflikt-Schutz |
Alle Tools funktionieren für beide Betriebsarten gleich: ensureGateway → executeQuery/executePreparedQuery/uploadFile. Bei Connector-Projekten deployt ensureGateway nichts, sondern pingt und gleicht die Policy ab.
ensureGateway und deployGateway laufen pro Projekt unter einem Lock. Parallele Chats oder Scheduled Tasks, die ensureGateway nutzen, teilen sich damit ein Gateway, statt sich gegenseitig das Script wegzudeployen.
Schreibrecht hat zwei Stufen: allow_write in der Connection-Config (remote_databases.<projekt>.allow_write) ist die Obergrenze, das Tool-Arg allow_write fordert innerhalb dieser Grenze ein Schreib-Gateway an. Fehlt die Obergrenze, lehnen deployGateway/ensureGateway mit allow_write, uploadFile und executePreparedQuery (mode: write) ab — mit einem Nutzertext in error („Schreiben ist noch nicht freigeschaltet — bitte im Builder freischalten.“), error_code: write_not_enabled und dem Technischen in admin_detail (read-only-Kontext: error_code: read_only). Wird die Obergrenze nachträglich entzogen, verweigert executeQuery das laufende Schreib-Gateway und ensureGateway deployt es lesend neu. Im Read-only-Kontext (API-Key ohne external_api_write, Webhook ohne inbound_hook_write) gibt es kein Schreib-Gateway, auch wenn die Verbindung es erlaubt. min_hours_left ist auf 71 h gedeckelt (ein neues Gateway läuft höchstens 72 h).
System-Profile
Das Gateway erkennt das System anhand typischer Dateien (vom Gateway-Verzeichnis bis zu vier Ebenen aufwärts). Mit profile in der Connection-Config legst du es fest; das ist für Produktivsysteme empfohlen. Unbekannte Systeme bekommen das Profil generic: nur Tabellen aus tables/extra_tables, alles andere ist gesperrt.
Profil (profile) |
Erkennung | Zugangsdaten | Präfix | Lesen (Auszug) | Schreiben | Hart gesperrt (Auszug) | Medien (Upload) |
|---|---|---|---|---|---|---|---|
contao |
vendor/contao/*, system/modules/core |
.env/.env.local/.env.<env>/.env.<env>.local (DATABASE_URL), config/parameters.yml, Contao 3 localconfig.php |
tl_ |
page, article, content, news*, calendar*, faq*, files, layout, theme, module, form*, image_size* | page, article, content, news, calendar_events, faq, files | user*, member*, session, remember_me, opt_in, trusted_device, log, cron*, crawl_queue, undo, version, comments, newsletter_recipients, messenger_messages | files |
wordpress (Alias woocommerce) |
wp-includes/version.php |
wp-config.php (auch eine Ebene höher) |
$table_prefix |
posts, postmeta, terms*, term_, wc_product__lookup, wc_category_lookup | posts, postmeta, terms, term_*, wc-Lookups | users, usermeta, options, comments*, woocommerce_sessions/api_keys/payment_tokens, wc_customer_lookup, wc_webhooks, actionscheduler_*, Formular-Einträge | wp-content/uploads |
typo3 |
vendor/typo3/cms-core, typo3/sysext/core |
config/system/settings.php (+ additional.php), typo3conf/LocalConfiguration.php |
– | pages, tt_content, sys_file*, sys_category*, tx_news_* | pages, tt_content, sys_file_reference/metadata, tx_news* | be_, fe_, sys_log, sys_history, sys_registry, sys_file_storage, tx_scheduler_, cache_, tt_address, powermail | public/fileadmin, fileadmin |
shopware6 (Alias shopware) |
vendor/shopware/core |
.env-Reihenfolge wie Symfony (DATABASE_URL) |
– | product*, property_group*, category*, media*, cms_, landing_page, seo_url, tag, language … | product*, category*, cms_, landing_page, tag | user*, acl_, customer, integration*, app*, system_config, order_customer/_address/_transaction*, cart, sales_channel(_api_context), webhook*, log_entry, document* | public/media |
magento2 (Alias magento) |
bin/magento |
app/etc/env.php |
db.table_prefix |
catalog_product_entity*, catalog_category_, eav_, cms_page/_block*, url_rewrite, store* | catalog_product_entity*, catalog_category_, cms_ | admin_, authorization_, customer_, core_config_data, oauth_, integration, sales_order_address/payment, sales_grid, quote, vault_, cron_schedule, queue | pub/media |
prestashop |
classes/PrestaShopAutoload.php, config/settings.inc.php |
app/config/parameters.php, 1.6: config/settings.inc.php |
database_prefix / _DB_PREFIX_ |
product*, category*, image*, cms*, manufacturer*, feature*, attribute*, stock_available … | product*, category*, image*, cms*, feature_product, tag | employee*, customer*, address, configuration*, webservice_, cart, connections*, guest, log, mail, api_* | img |
oxid |
vendor/oxid-esales/oxideshop-ce, oxseo.php |
config.inc.php (auch source/) |
– | oxarticles, oxartextends, oxcategories, oxobject2category, oxcontents, oxseo … | oxarticles, oxartextends, oxcategories, oxcontents, oxseo … | oxuser, oxadminlog, oxlogs, oxconfig, oxshops (SMTP-Passwort), oxuserpayments, oxuserbaskets, oxvouchers, oxaddress | source/out/pictures, out/pictures |
jtl (Alias jtl-shop) |
includes/config.JTL-Shop.ini.php |
ebendort (DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_SOCKET) |
– | tartikel*, tkategorie*, tmerkmal*, tlink*, tnews*, tseo, thersteller* … | tnews*, tlink*, tartikelsprache, tkategoriesprache, tseo | tadmin*, tkunde*, tadresse, tzahlung, tsession, tsynclogin*, teinstellungen*, tplugineinstellungen, tjtllog, tcron, tjobqueue, tlicense*, tstore* | mediafiles |
gambio (Alias gambio-gx) |
GXMainComponents/, GXEngine/, GambioCore/Application/Application.php; Version aus Application::VERSION (GX4) bzw. $gx_version in release_info.php (GX2/GX3) |
includes/local/configure.php (gewinnt, wie im Shop), sonst includes/configure.php: DB_SERVER (auch host:port/host:/socket), DB_SERVER_USERNAME, DB_SERVER_PASSWORD, DB_DATABASE. Nur geparst, nie ausgeführt |
– (fest im Code, nicht konfigurierbar) | products*, products_description, products_to_categories, products_images, products_attributes/_options*, products_properties_combis*, properties*, feature*, categories*, manufacturers*, content_manager, url_rewrites, specials, languages … | products, products_description, products_to_categories, products_images, gm_prd_img_alt, products_attributes, products_properties_combis (Bestand), products_xsell, categories, categories_description, content_manager | customers*, address_book, admin_access*, gm_admin_, gm_login_history, sessions, whos_online, configuration, gm_configuration, gx_configurations, gx_lang_configurations, api_tokens, newsletter_recipients, module_newsletter, emails, email_contacts, coupon*, banktransfer, sepa, payone_, payment_, amzadvpay_, orders_paypal, orders_payment_instruction, orders_hermes, invoices, packing_slips, withdrawals, magnalister_* | images/product_images, images/categories, images/manufacturers, images/content |
joomla |
libraries/src/Version.php |
configuration.php (JConfig) |
$dbprefix |
content, categories, menu*, modules*, tags, fields* | content, categories, menu, modules, fields_values, tags | users, user_, usergroups, session, action_logs, privacy_, extensions (Parameter mit API-Keys), messages, webauthn_, scheduler_* | images |
drupal |
core/lib/Drupal.php, web/core/lib/Drupal.php |
sites/default/settings.php ($databases) |
prefix |
node*, taxonomy_, menu_link_content, path_alias*, file_managed/_usage, block_content*, paragraph*, media* | node*, taxonomy_, path_alias, menu_link_content_data, file_, paragraph | users*, user__, sessions, key_value, config*, cache_, watchdog, queue, flood, webform_submission, comment* | web/sites/default/files, sites/default/files |
laravel |
artisan |
.env (DB_*, DB_URL/DATABASE_URL) |
– | nur tables |
nur tables |
users, sessions, password_reset*, personal_access_tokens, oauth_, jobs, failed_jobs, cache, telescope_* | nur media_dirs |
cakephp |
bin/cake |
.env (DATABASE_URL), config/app_local.php |
– | nur tables |
nur tables |
users, sessions, queue*, phinxlog | nur media_dirs |
generic |
– (Fallback) | .env (DB_*, DATABASE_URL) |
– | nur tables |
nur tables |
nur generische Denylist | nur media_dirs |
Die vollständigen Listen stehen in resources/database_gateway/profiles.php.
Für alle Profile gilt zusätzlich eine generische Denylist (gegen den Namen mit und ohne Präfix): *user*, *admin*, *customer*, *member*, *employee*, *session*, *token*, *password*, *oauth*, *api_key*, *access_key*, *secret*, *credential*, *auth*, *2fa*, *mfa*, *webauthn*, *remember*, *payment*, *vault*, *creditcard*, *address*, *subscriber*, *config*, Log-Tabellen (log, *_log, *_logs, log_*, *logging*), *cron*, *queue*, *messenger*.
Bestellungen stehen bewusst in keiner Allowlist (sie enthalten Kundendaten), sind aber nicht hart gesperrt. Für Reporting per extra_tables freigeben; Adressen, Kundenkonten und Zahlungsdaten bleiben gesperrt.
Gambio: Die Tabelle orders enthält Kunden-, Liefer- und Rechnungsadresse (und alte cc_*-Spalten) direkt in der Zeile, das Gateway kann nicht spaltenweise filtern. Für Reporting reichen meist orders_products, orders_total und orders_status_history; orders selbst nur freigeben, wenn die Adressen wirklich gebraucht werden. Kundengruppen-Preise (personal_offers_by_customers_status_*) fallen unter die generische Sperre *customer*. Neue Produktbilder gehören nach images/product_images/original_images/; Thumbnails/Info-/Popup-Bilder erzeugt Gambio daraus über die Bildverarbeitung im Admin.
Präfix: Profil-Tabellen gelten nur mit dem Präfix des Systems. Eine Tabelle ohne dieses Präfix (z. B. posts statt wp_posts) ist nicht über das Profil erlaubt, nur über extra_tables.
Config-Regeln: tables kann das Profil nur einschränken. extra_tables gibt explizit zusätzliche Tabellen frei, die Denylist gewinnt aber immer. Die Denylist lässt sich nur mit unsafe_allow_denied_tables (exakte Namen, keine Wildcards) aufheben.
Sicherheitsmodell
Alles Folgende setzt das Gateway-Script selbst durch (serverseitig), nicht der Prompt und nicht TaskMonkey:
| Mechanismus | Schutz |
|---|---|
| Token | 256 bit, im Script liegt nur der SHA-256-Hash, Vergleich mit hash_equals. Falscher Token, falsche Methode (nur POST) oder abgelaufen → 404 |
| Laufzeit | Default 24 h, hart gedeckelt auf 72 h. Abgelaufene Scripts löschen sich beim nächsten Aufruf |
| Credentials | ausschließlich aus der Config des Zielsystems. Ein credentials-Override im Request gibt es nicht mehr |
| SQL-Analyse | SQL wird tokenisiert, jede Tabellen-Referenz (FROM, JOIN, Komma-Joins, INTO, UPDATE, DELETE, DESCRIBE, SHOW … FROM, Subqueries, UNION) wird gegen das Profil geprüft. Schreib-Statements brauchen die Tabelle in der Schreib-Allowlist |
| Ablehnen statt raten | Kommentare (auch /*! … */), WITH/CTEs, TABLE-Statement, Klammer-Joins, Index-Hints, PARTITION, @-Variablen, INFORMATION_SCHEMA, performance_schema, mysql.*, sys.*, fremde Datenbanken (andere_db.tabelle), INTO OUTFILE/DUMPFILE, LOAD_FILE, LOAD DATA, SLEEP, BENCHMARK, MariaDB-Sequenzen (NEXTVAL, SETVAL, LASTVAL), Locks, HANDLER, PREPARE, CALL, DDL (DROP, ALTER, CREATE, TRUNCATE, …), SET, SHOW außer TABLES/TABLE STATUS/COLUMNS/INDEX/CREATE TABLE |
| Ein Statement | Multi-Statements im Treiber aus, ; nur am Ende. sql_mode wird auf Standard-Quoting normalisiert (kein ANSI_QUOTES/NO_BACKSLASH_ESCAPES), damit die Analyse dasselbe sieht wie MySQL. Statement-Timeout 20 s |
| Lesen isoliert | Lese-Statements laufen in START TRANSACTION READ ONLY, max. 500 Zeilen. SHOW TABLES zeigt nur erlaubte Tabellen |
| Upload | nur mit allow_write; nur unter dem Medienverzeichnis des Profils; Pfad ohne .., ohne versteckte Dateien, nur [A-Za-z0-9._-]; realpath-Prüfung nach jedem Verzeichnis (keine Symlinks hinaus); Endungen jpg jpeg png webp gif pdf (SVG nie); keine Doppelendungen wie .php.png, nie .php/.phtml/.phar/.htaccess/.user.ini; Inhalt muss zur Endung passen und darf kein <?php enthalten; Default 10 MB, max. 25 MB; bestehende Dateien werden nie überschrieben (Suffix -1, -2 …) |
| Backups / Journal | Actions backup_create, backup_list, rollback_list (lesend) und backup_restore, backup_delete, rollback_apply (nur mit allow_write). Ablage nur aus der eingebetteten Konfiguration, nie per Request; realpath-Prüfung gegen Web-Root und Symlinks bei jeder Nutzung; nur Tabellen der Schreib-Allowlist; Wiederherstellung akzeptiert nur reine Literal-INSERTs in die jeweilige Tabelle und prüft den Inhalts-Hash. Antworten enthalten keine Daten |
| Contao-Actions | contao_version und cache_invalidate nur im Profil contao und nur mit allow_write. contao_version nimmt nur Tabelle + ID (Tabelle muss im Profil schreibbar sein), liest die Zeile selbst und schreibt sie serialisiert nach tl_version — kein Inhalt vom Aufrufer, deshalb keine gefälschten Versionen (z. B. für tl_user). cache_invalidate nimmt nur Tags der Form contao.db.<tabelle>[.<id>] (max. 50) für Tabellen der Schreib-Allowlist (plus deren Eltern tl_news_archive, tl_calendar, tl_faq_category), läuft höchstens einmal pro 30 s je Gateway (sonst RATE_LIMIT mit retry_after; clearCmsCache wartet dann nicht, sondern meldet „Cache wird in Kürze geleert“ und plant einen verzögerten Nachlauf als Async-Job — ein Job je Projekt, Tags zusammengefasst) und löscht beim Fallback nur Inhalte unter var/cache/prod/http_cache (Symlinks werden entfernt, nie verfolgt) |
| Fehler | nach außen generisch (Query failed: unknown column, error_code: 42S22), die Original-Meldung der DB steht nur im Tool-Log |
| SSH | StrictHostKeyChecking=accept-new mit eigener known_hosts-Datei pro Projekt (storage/database_gateways/known_hosts/<workspace>_<projekt>); mit host_key strikt. Ein geänderter Host-Key bricht den Deploy ab |
Das Gateway ist trotzdem ein Live-Zugriff auf eine fremde Produktiv-DB. Setze es nur ein, wenn du die Verantwortung hast oder explizit autorisiert wurdest, lass allow_write aus, wo es geht, und schränke mit tables auf das ein, was fachlich nötig ist.
Grenzen:
- Die Prüfung ist tabellenbasiert, nicht zeilenbasiert. Beispiel WooCommerce ohne HPOS: Bestellungen liegen als
shop_orderinwp_posts/wp_postmeta(inkl. Rechnungsadresse). Wer das nicht will, schränkt mittablesein oder aktiviert HPOS. - Vorhandene Stored Functions/Views der Ziel-DB werden nicht analysiert.
- Die Denylist greift pro Tabellenname. Eigene Tabellen mit sensiblen Daten und unauffälligem Namen gehören in
deny.
Parametrisierte Queries für Tools und APIs
Sobald Werte von außen kommen (API-Aufruf, Formular, Webhook), gehört das SQL fest ins Tool und die Werte in params:
$ctx['tool']('executePreparedQuery', [
'project' => 'kunde_shop',
'sql' => 'SELECT id, name, price FROM products WHERE sku = ? LIMIT 1',
'params' => [$args['sku']],
]);
Über die API-Keys, öffentliche Tools (public_api) und eingehende Webhooks ist executeQuery (freies SQL) grundsätzlich gesperrt und executePreparedQuery läuft nur lesend, solange das aufgerufene Tool nicht 'external_api_write' => true trägt. Vollständiges Beispiel: Rezept: API für eine Bestandsdatenbank.
Backups und Rollback
Ab Gateway-Protokoll 5 sichert das Gateway (und der Connector) die Daten, die TaskMonkey ändern darf, beim Kunden — kein Dump und kein Datensatz verlässt den Server. TaskMonkey bekommt nur Metadaten zurück: Dateiname, Größe, Tabellen- und Zeilenzahl.
Ablage. tm-backups/<projekt>/ eine Ebene über dem Web-Root (dem Verzeichnis des Gateways, das base_url zeigt), z. B. bei Contao …/kunde.de/current/tm-backups/kunde_web/ neben public/. Mit backup_dir in remote_databases.<projekt> frei wählbar (relativ zum Gateway-Verzeichnis oder absolut). Das Gateway löst den Pfad vor dem Anlegen per realpath auf und lehnt alles ab, was im Gateway-Verzeichnis, im DOCUMENT_ROOT des Webservers, in einem typischen Web-Verzeichnis des Projekts (public/, web/, pub/, htdocs/, html/, …) oder — wenn das Gateway im Projekt, aber in keinem dieser Verzeichnisse liegt (WordPress, Contao ohne public/) — im Projekt-Root selbst liegt, auch über Symlinks. Dann schlagen Backup und Schreiben fehl (fail-closed, kein Journal ohne sichere Ablage), die Meldung verlangt backup_dir. Verzeichnis 0700, Dateien 0600, dazu eine .htaccess mit „deny all“ als zweite Schicht. Dateinamen enthalten Zeitstempel, SHA-256 des Inhalts und einen Zufallsteil (tm_backup_20260925-143012_3f9c…_8a1e….sql.gz.php), sind also nicht ratbar. Dritte Schicht: Backups und Journale enden auf .php und beginnen mit einem PHP-Guard (<?php http_response_code(404); exit; ?> — im Journal als erste Zeile, im Backup im Kommentarfeld des gzip-Headers, die Datei bleibt normales gzip). Liefert ein falsch konfigurierter Webserver das Verzeichnis doch aus, führt er die Datei aus und sie endet sofort. Ältere Dateien (….sql.gz, journal_<datum>.jsonl) werden weiter gelesen, wiederhergestellt und rotiert.
Bei Systemen, deren Web-Root das Projektverzeichnis selbst ist (WordPress, Gambio, JTL, …), liegt das Default-Verzeichnis im Elternordner. Wird der vom Hoster ebenfalls ausgeliefert (z. B.
~/htmlals Root einer weiteren Domain),backup_dirauf ein sicher nicht ausgeliefertes Verzeichnis setzen, z. B./home/p123456/tm-backups/kunde_shop.
Umfang. Genau die Tabellen der Schreib-Allowlist des effektiven Profils — inkl. extra_tables.write, nie Denylist-Tabellen (auch nicht, wenn sie per extra_tables genannt sind), keine nur lesbaren Tabellen. Format: SQL (DROP TABLE IF EXISTS + CREATE TABLE + INSERT, utf8mb4, Binärdaten als Hex), gzip, in einer konsistenten Lese-Transaktion (InnoDB). Der Dump wird gestreamt (ungepufferte Abfrage, INSERT-Blöcke von max. 512 KB) und ist auch von Hand mit mysql einspielbar.
Automatisch. Vor der ersten Schreiboperation einer Sitzung (Projekt + Chat; ohne Chat: Projekt + Einstiegsart wie Scheduled Task) legt executePreparedQuery/executeQuery ein Backup an und meldet es im Tool-Result (auto_backup). Eine neue Sitzung beginnt nach auto_backup_hours (Default 12 h); ein Backup, das jünger als 10 Minuten ist (paralleler Chat), zählt mit. Das gilt für jeden Schreibpfad über das Gateway, also auch für die Contao-Redakteur-Tools. Schlägt das Backup fehl, wird nicht geschrieben (fail-closed) — die Meldung nennt den Grund. Abschalten nur bewusst mit 'auto_backup' => false. Gateways/Connectoren vor Protokoll 5 kennen weder Backup noch Journal: normale Tools schreiben dann weiter und sagen es im Result (auto_backup.unsupported); Tools, die Rückgängig-Machen versprechen (require_journal — alle Contao-Redakteur-Tools über contaoWriteRow), schreiben dann nicht und melden „Connector aktualisieren (Protokoll 5)“ — unabhängig von auto_backup.
Manuell. createDbBackup (höchstens eins pro 30 s), listDbBackups. Rotation nach jedem Backup: backup_keep_days (Default 7) und backup_keep_max (Default 10), das neueste bleibt immer.
Wiederherstellen. restoreDbBackup — nur mit allow_write (Obergrenze der Verbindung und Schreib-Gateway) und nur nach ausdrücklicher Bestätigung im Chat: der erste Aufruf zeigt, welches Backup und was verloren geht, und setzt die Buttons „Ja, wiederherstellen“ / „Nein, abbrechen“; ausgeführt wird erst, wenn danach eine neue Nachricht des Nutzers mit einem klaren Ja im Chat steht (gleicher Baustein confirmChatAction wie contaoSetPublished, Kern App\Tooling\ApprovalGate — das Ja muss die unmittelbar nächste Nachricht sein). Ohne Chat (Scheduled Task, API) nie. Das Gateway prüft zuerst, dass alle Tabellen des Backups heute existieren und in der Schreib-Allowlist sind, legt dann ein frisches Backup des aktuellen Stands an und spielt die Tabellen ein (pro Tabelle DELETE + INSERT, bei InnoDB alles in einer Transaktion, Fremdschlüssel-Prüfung währenddessen aus). Jede Zeile aus der Datei muss exakt ein INSERT INTO <tabelle> (…) VALUES (…) mit reinen Literalen sein — eingeschleuste Statements, Funktionen oder andere Tabellen lassen die Wiederherstellung abbrechen (und zurückrollen). Eine Datei, deren Inhalt nicht mehr zum Hash im Namen passt, wird abgelehnt. Mit tables lassen sich einzelne Tabellen wiederherstellen.
Rollback-Journal. Jede Schreiboperation über das Gateway (executePreparedQuery mit mode: write, executeQuery mit INSERT/UPDATE/DELETE, alle CMS-Tools, die darauf aufsetzen) schreibt im selben Request einen Eintrag nach tm-backups/<projekt>/rollback/journal_<datum>_<zufall>.jsonl.php (JSON-Lines, erste Zeile PHP-Guard):
| Statement | Eintrag |
|---|---|
UPDATE/DELETE einer Tabelle (mit WHERE/ORDER BY/LIMIT) |
Before-Image der betroffenen Zeilen per Primärschlüssel (im Gateway per SELECT … FOR UPDATE in derselben Transaktion gelesen), geänderte Spalten, Hash des Nachher-Stands |
INSERT … VALUES |
Primärschlüssel der neuen Zeilen (explizit oder AUTO_INCREMENT bei einer Zeile) + Hash |
mehrere Tabellen, REPLACE, INSERT … SELECT/SET, ON DUPLICATE KEY, Tabelle ohne Primärschlüssel, > 2000 Zeilen |
Schreiben läuft, Eintrag vermerkt „nicht einzeln rücknehmbar“ (rollback.available: false + Grund) — dafür gibt es das Backup |
Einträge sind pro Tool-Aufruf gruppiert (journal_group, z. B. alle Schritte von contaoSetPublished) und tragen einen Hash des Chats. Das Tool-Result nennt die ID (rollback.id). Kann das Journal nicht geschrieben werden, wird die Schreiboperation zurückgerollt (fail-closed). Zurücknehmen: rollbackLastWrite (letzte Änderung dieses Chats) bzw. rollbackWrite mit ID aus listDbWrites — beide mit Vorschau und Ja im Chat; die Contao-Redakteur-Tools nutzen denselben Mechanismus direkt (contaoUndoLastChange). Konflikt-Schutz: Hat sich eine betroffene Zeile seitdem geändert (Hash) oder existiert eine gelöschte Zeile wieder, wird abgebrochen und gemeldet; nur mit force wird trotzdem zurückgenommen (dann nur die Spalten der Änderung). Die Rücknahme selbst läuft in einer Transaktion und wird im Journal vermerkt (keine doppelte Rücknahme; das Journal wird dafür erst gesperrt, dann gelesen). Gelesen werden höchstens die neuesten 400 Journal-Dateien und 16 MB, von hinten; Änderungen am Schnitt (bis 1 h nach dem ältesten geladenen Eintrag) gelten dann als nicht einzeln rücknehmbar — dafür das Backup.
Fremdschlüssel. Wiederherstellen und Zurücknehmen laufen mit SET FOREIGN_KEY_CHECKS = 0 (nur für die eigene DB-Session, danach wieder 1) — sonst scheitern DELETE + INSERT einer Tabelle bzw. das Zurückschreiben gelöschter Zeilen an der Reihenfolge. Folge: MySQL prüft dabei keine Fremdschlüssel. Verweisen Tabellen außerhalb des Backups bzw. der zurückgenommenen Änderung auf wiederhergestellte Zeilen (oder umgekehrt), können danach verwaiste Verweise bleiben, und ON DELETE CASCADE/ON UPDATE lösen nicht aus. Für die CMS-Profile (Contao, WordPress, …) ohne echte Fremdschlüssel ist das ohne Wirkung; bei Shops mit InnoDB-Fremdschlüsseln (Shopware 6, Magento) nach einer Teil-Wiederherstellung (tables) die Verweise prüfen oder das ganze Backup einspielen.
Grenzen (Shared Hosting). Das Gateway setzt set_time_limit wo erlaubt und bricht vor dem Laufzeitlimit des Hosters sauber ab (keine halbe Datei, Meldung mit Tabelle), ebenso bei backup_max_mb oder wenn weniger als 64 MB frei bleiben würden — geprüft vor dem Start und während des Schreibens (spätestens jedes MB), ein Backup füllt also nie die Platte des Kunden. Das Rollback-Journal verlangt 16 MB Reserve; fehlt sie, wird die Schreiboperation zurückgerollt (fail-closed). TaskMonkey wartet bis zu 5 Minuten auf ein Backup. Große Shops: Tabellen per tables einschränken oder Backups beim Hoster einplanen. Eine Wiederherstellung ohne InnoDB (MyISAM) läuft ohne Transaktion — bricht sie ab, meldet das Ergebnis den Teilstand, das Vor-Backup ist dann der Rückweg.
'remote_databases' => [
'kunde_web' => [
// … wie oben …
'backup_dir' => '../tm-backups/kunde_web', // Default, außerhalb des Web-Roots
'auto_backup' => true, // Default
'auto_backup_hours' => 12,
'backup_keep_days' => 7,
'backup_keep_max' => 10,
],
],
Dauerbetrieb: täglicher Redeploy
Ein Gateway läuft ab. Wenn Tools (z. B. eine API) dauerhaft darauf zugreifen, erneuert ein Scheduled Task es täglich. Jeder Lauf erzeugt einen neuen Token, verlängert die Laufzeit und entfernt das alte Script vom Server. Schlägt ein Projekt fehl, bleibt das bisherige Gateway bis zu seinem Ablauf aktiv und die Admins des Workspaces bekommen eine Mail.
Aufräumen im Web-Root. Beim Redeploy wird das vorherige Script _tm_gateway_<hash>.php samt seiner Rate-Limit-Datei _tm_gateway_<hash>.cache-rate.php (von cache_invalidate) per SSH gelöscht. Danach räumt das neue Gateway sein eigenes Verzeichnis auf (Action sweep, ohne weitere SSH-Befehle): Es löscht nur _tm_gateway_<16 hex>.php, deren eingebetteter Ablauf ($EXPIRES_AT) vorbei ist, und .cache-rate.php-Dateien ohne zugehöriges Script. Nie das eigene Script, nie ein noch gültiges (andere Workspaces/Projekte können denselben Web-Root nutzen), nie Connectoren, keine Unterverzeichnisse oder Symlinks. So verschwinden auch Waisen aus Fehlschlägen oder parallelen Läufen, die kein Datensatz mehr kennt. Die Anzahl steht im Deploy-Ergebnis (orphans_removed, null wenn der Sweep fehlschlug) und im Log. Ein abgelaufenes Gateway, das noch aufgerufen wird, löscht sich zusätzlich selbst (samt Rate-Limit-Datei) und antwortet 404.
// config/tenants/<workspace>/scheduled.php
return [
'scheduled' => [
'refresh_db_gateways' => [
'enabled' => true,
'tool' => 'refreshDatabaseGateways',
'interval' => '03:30',
'description' => 'DB-Gateways täglich neu deployen (Token-Rotation)',
'args' => [
'projects' => ['bestand'], // leer = alle Projekte mit bestehendem Gateway
'expiry_hours' => 48, // überbrückt einen ausgefallenen Lauf (max. 72)
// 'notify' => ['it@firma.de'], // sonst admin_emails des Workspaces
],
],
],
];
Auch ein manueller deployGateway räumt das vorherige Script ab, sobald das neue antwortet. Antwortet das neue nicht, wird es wieder entfernt und das alte bleibt aktiv.
Dauerbetrieb ohne SSH (Connector)
Für Kunden, die nur FTP oder den Dateimanager ihres Hosters haben. Der Connector ist dasselbe Script wie das Gateway (gleicher SQL-Prüfer, gleiches System-Profil, eingebettete Policy), aber:
- er wird einmal hochgeladen und läuft dauerhaft (kein Ablauf, keine Selbstlöschung),
- jeder Aufruf ist mit einem Secret pro Projekt signiert (HMAC-SHA256) statt mit einem statischen Token,
- Secret-Rotation und Policy-Änderungen laufen über signierte Befehle, ohne neuen Upload.
Einrichten (Workspace)
In database_connections.php ein Projekt mit 'mode' => 'persistent' anlegen:
'remote_databases' => [
'kunde_shop' => [
'mode' => 'persistent',
'base_url' => 'https://www.kunde-shop.de', // Verzeichnis, in das der Connector kommt
'profile' => 'wordpress', // für Produktivsysteme festlegen
'allow_write' => false,
'tables' => ['read' => ['wp_posts', 'wp_postmeta']],
// optional:
// 'allowed_ips' => ['203.0.113.10'], // Ausgangs-IP des TaskMonkey-Servers
// 'state_dir' => '../tm_state', // Statusdatei außerhalb des Web-Roots
// 'sftp' => [ // statt FTP-Upload durch den Kunden
// 'hostname' => 'ssh.kunde.de', 'username' => 'ftp123',
// 'password' => secret('KUNDE_SFTP_PASS'), 'web_root' => '/html/shop',
// 'host_key' => 'SHA256:…', // Pflicht, vorab beim Hoster bestätigen
// ],
],
],
| Feld | Bedeutung |
|---|---|
mode |
persistent = Connector. Ohne mode (oder temporary) gilt das SSH-Gateway |
base_url |
URL des Verzeichnisses, in das der Kunde die Datei lädt. Muss https:// sein |
profile, tables, extra_tables, deny, unsafe_allow_denied_tables, media_dirs, upload |
wie beim Gateway, werden in den Connector eingebettet |
allow_write |
Obergrenze wie beim Gateway (nur true zählt). Wird beim Erzeugen in den Connector eingebettet; später per Config nur abschaltbar — update_policy hebt Schreiben nie darüber. Bis zum nächsten Abgleich (ensureGateway) verweigern executeQuery/Uploads einen Connector, der noch Schreibrecht hat, obwohl die Config es nicht mehr erlaubt |
allowed_ips |
optional: nur diese IPs/Netze (IPv4/IPv6, CIDR) dürfen den Connector ansprechen. Fest in der Datei |
state_dir |
optional: Verzeichnis für die Statusdatei (relativ zum Connector oder absolut), wenn der Web-Root für PHP nicht beschreibbar ist |
backup_dir |
wie beim Gateway, wird fest eingebettet — eine Änderung wirkt erst mit einem neuen Connector (ensureGateway meldet sie als warning). auto_backup, Rotation und backup_max_mb gelten sofort |
sftp |
optional: Upload durch TaskMonkey per SFTP mit Passwort (phpseclib). host_key (SHA256:…) ist Pflicht — ohne passenden Fingerprint wird das Passwort nicht gesendet; die Fehlermeldung nennt den gemeldeten Fingerprint zum Abgleich mit dem Hoster. Interne Adressen sind gesperrt |
Schritt für Schritt (Kunde)
Im Manage-Bereich unter Verwaltung → DB-Connectoren (/manage/connectors):
-
Connector herunterladen. Die Datei heißt
_tm_connector_<zufall>.php. Jeder Download erzeugt einen neuen Connector mit neuem Start-Secret; ein schon installierter alter entfernt sich dabei selbst. -
Die Datei unverändert per FTP-Programm (z. B. FileZilla) oder Dateimanager des Hosters (all-inkl KAS, IONOS, Strato …) in den Web-Root laden — das Verzeichnis, das
base_urlim Browser zeigt:System Web-Root Contao 4.13 / 5 public/(ältere Contao-4:web/)WordPress / WooCommerce Hauptverzeichnis mit wp-config.phpShopware 6 public/Magento 2 pub/TYPO3 public/(Composer) bzw. Hauptverzeichnis mitfileadmin/Gambio, JTL-Shop, PrestaShop Hauptverzeichnis des Shops OXID eShop source/(OXID 6/7)Joomla Hauptverzeichnis mit configuration.phpDrupal web/(Composer) bzw. Hauptverzeichnis mitcore/Der Connector sucht das System vom eigenen Verzeichnis bis vier Ebenen aufwärts.
-
Verbindung testen. Der Connector meldet System, Version, Profil und Datenbank. Beim ersten Kontakt wird das Start-Secret aus der Datei sofort ersetzt — die heruntergeladene Datei ist danach wertlos und kann lokal gelöscht werden.
-
Fertig. Tools, Chats und APIs nutzen den Connector über
ensureGateway/executeQuerywie ein Gateway.
Per Builder-Chat geht dasselbe ohne Manage-Bereich: createDatabaseConnector({project}) legt nach der Freigabe im Chat den Connector an und liefert einen Download-Link (die Datei heißt exakt _tm_connector_<id>.php; der Inhalt läuft nicht durch Tool-Args oder Logs). Nach dem Upload getConnectorStatus({project, test: true}) — beim ersten Kontakt wird das Start-Secret ersetzt und die Download-Kopie auf unserem Server gelöscht. Der Download-Link gilt nur 72 h (Boot-Fenster des Start-Secrets); wird der Connector nie installiert, löscht der tägliche prune_tool_cache-Lauf die Kopie danach trotzdem, ebenso ein neuer Download die Kopie des Vorgängers. Mit SFTP-Zugang: installConnectorViaSftp({project}). Muster im Builder: showPattern({name: 'contao-redakteur'}).
Secret rotieren (Button oder deployGateway/refreshDatabaseGateways) tauscht das Secret, das alte bleibt 15 Minuten gültig. Deinstallieren lässt den Connector sich selbst löschen; ist er nicht erreichbar, wird das Secret bei uns gelöscht (die Datei ist dann wirkungslos) und der Kunde entfernt sie per FTP.
Sicherheitsmodell (Connector)
| Mechanismus | Schutz |
|---|---|
| Signatur | Jeder Request trägt X-TM-Key-Version, X-TM-Timestamp, X-TM-Nonce, X-TM-Signature = HMAC-SHA256 über Methode \n Pfad \n Connector-ID \n Key-Version \n Timestamp \n Nonce \n SHA-256(Body). Vergleich mit hash_equals. Nur POST. Falsche/fehlende Signatur, falscher Pfad, fremde Connector-ID, manipulierter Body → 404 (keine Auskunft) |
| Zeitfenster | Timestamp ±5 min, sonst 401 (nur für korrekt Signiertes, mit Serverzeit zur Diagnose) |
| Replay | Jede Nonce wird nach erfolgreicher Prüfung gespeichert (TTL 11 min), Wiederholung → 409. Kann der Connector seine Statusdatei nicht schreiben, lehnt er alles ab (fail-closed) |
| Signierte Antworten | Antworten tragen eine HMAC über Nonce, Status, aktive Key-Version und Body. TaskMonkey verwirft unsignierte oder manipulierte Antworten |
| Secret-Ablage | Beim Kunden: separate Statusdatei _tm_connector_<id>.state.php (0600, erste Zeile <?php http_response_code(404); exit; ?>, atomar geschrieben) — nicht im Connector selbst. Bei uns: Tenant-Secret-Store (tenants_untracked/<workspace>/.env, Key TM_CONNECTOR_<PROJEKT>), nicht im Gateway-JSON unter storage/ |
| Start-Secret | Steht in der Download-Datei, wird beim ersten Kontakt rotiert. Jeder Download erzeugt einen neuen Connector. Es gilt nur bis zur ersten Rotation (danach liegt eine Marker-Datei _tm_connector_<id>.rotated.php neben der Statusdatei) und nur 72 h nach dem Download. Fehlt die Statusdatei danach, antwortet der Connector mit 503 statt auf das Start-Secret zurückzufallen — dann neu herunterladen. Rotiert wird beim ersten erfolgreichen Kontakt, auch wenn nur der Health-Check pingt. Wurde nach Ablauf der 72 h noch nie rotiert, lehnt der Connector mit dem Start-Secret alles ab, auch rotate (503, BOOT_EXPIRED) — eine liegen gebliebene Download-Datei kann sich so nicht mehr selbst freischalten. TaskMonkey meldet dann „Connector neu herunterladen“; die alte Datei kann sich nicht mehr selbst entfernen und wird per FTP gelöscht |
| Rotation | Signierte Action rotate nur mit dem aktiven Secret. Das neue Secret kommt XOR-verschlüsselt (Pad = HMAC aus aktuellem Secret und Nonce), nie im Klartext im Body. Altes Secret 15 min Übergang (1–1440 einstellbar), ältere verfallen sofort. Geht die Antwort verloren, gleicht der nächste Request über das vorgemerkte Secret (…_PENDING) ab |
| Policy-Update | Signierte Action update_policy, nur mit dem aktiven Secret, Version muss steigen. Innerhalb der eingebetteten Policy: Profil und Präfix fest, Denylist wird nur ergänzt, unsafe_allow_denied_tables nur Teilmenge, Schreibrecht nur abschaltbar (nie über die allow_write-Obergrenze, die beim Erzeugen eingebettet wurde), Medien/Upload nur einschränkend. Tabellen nur als Schnittmenge: erlaubt ist, was die eingebettete Policy und die neuen tables/extra_tables erlauben. Einschränken geht per Config, Erweitern (neue Tabelle, neue extra_tables, Einschränkung entfernen) nur per neuem Download — ensureGateway meldet solche Einträge als warning |
| IP-Allowlist | optional, fest in der Datei (nicht remote änderbar — sonst hebelt ein geleaktes Secret sie aus). Nicht erlaubte IP → 404 |
| Übergangs-Secret | darf nur lesen/schreiben/pingen, keine Verwaltungs-Actions (rotate, update_policy, uninstall) |
Alles aus dem Sicherheitsmodell des Gateways (SQL-Analyse, Tabellen-Profil, Upload-Prüfung, generische Fehler) gilt unverändert.
Grenzen:
- Wer Schreibzugriff auf das Dateisystem des Kunden hat, kann Statusdatei und Marker löschen — innerhalb der ersten 72 h nach dem Download gilt dann wieder das Start-Secret. Wer dort schreiben kann, kann aber ohnehin die DB-Zugangsdaten des Systems lesen.
- Profil, Präfix, Denylist-Ausnahmen, IP-Allowlist, Tabellen-Erweiterungen und Schreibrecht-Erweiterungen brauchen einen neuen Connector (neu herunterladen, hochladen, testen).
- Der Connector braucht HTTPS beim Kunden und Schreibrecht im eigenen Verzeichnis oder in
state_dir. TaskMonkey spricht ihn nur überhttps://an, über den EgressGuard (keine internen oder reservierten Adressen, DNS gepinnt, keine Redirects). - PHP ab 7.3 mit
pdo_mysql. - Web Application Firewalls bei Shared-Hostern (z. B. mod_security) können Requests mit SQL im Body blocken — das gilt auch für das Gateway. Symptom:
403oder eine HTML-Fehlerseite statt JSON (bzw. „nicht korrekt signiert“). Dann den Hoster bitten, die Regel für die Connector-URL auszunehmen. Eine Base64-Kapselung des Bodys ist vorgesehen, wird aber erst umgesetzt, wenn das bei einem Kunden tatsächlich auftritt.
Health-Check
// config/tenants/<workspace>/scheduled.php
'check_db_connectors' => [
'enabled' => true,
'tool' => 'checkDatabaseConnectors',
'interval' => '30min',
'description' => 'DB-Connectoren pingen, Admin-Mail nach 2 Fehlschlägen',
'args' => ['failure_threshold' => 2], // Mails gehen nur an admin_emails des Workspaces
],
Nach zwei Fehlschlägen in Folge geht eine Mail an die Admins, bei Wiederkehr eine Entwarnung. Für die tägliche Secret-Rotation zusätzlich refreshDatabaseGateways einplanen (siehe oben); er rotiert bei Connector-Projekten das Secret.
Update auf Gateway-Protokoll 5
Protokoll 5 bringt Backups und Rollback. SSH-Gateways ersetzt ensureGateway automatisch. Connectoren bis zum nächsten Download: die Backup-Tools melden „kennt Backups und Rollback noch nicht — Connector neu herunterladen“, Schreiben über executePreparedQuery/executeQuery geht weiter, aber ohne automatisches Backup und ohne Rollback-Eintrag (das Tool-Result sagt es, auto_backup.unsupported). Die Contao-Redakteur-Tools schreiben dann nicht („Connector aktualisieren (Protokoll 5)“), weil sie Rückgängig-Machen versprechen. Für den vollen Umfang den Connector im Manage-Bereich neu herunterladen und hochladen. Vorher prüfen, dass PHP eine Ebene über dem Web-Root schreiben darf — sonst backup_dir setzen (bei Protokoll 5 gilt: ohne Journal-Ablage kein Schreiben). Der Ping (deployGateway) zeigt unter backups das Verzeichnis und ob es passt.
Die frühere Contao-Undo-Liste im Chat-State (contao_undo) gibt es nicht mehr; Änderungen, die vor dem Update gemacht wurden, lassen sich nicht mehr per Undo zurücknehmen (Versionen in tl_version bleiben).
Update auf Gateway-Protokoll 4
Protokoll 4 bringt die Contao-Actions contao_version (Versionen in tl_version) und cache_invalidate (HTTP-Cache leeren) für die Contao-Redakteur-Tools. SSH-Gateways ersetzt ensureGateway automatisch (Protokoll veraltet). Connectoren tragen ihr Script fest beim Kunden: bis zum nächsten Download melden clearCmsCache/createContaoVersion „kennt die Action noch nicht“ — die Contao-Tools arbeiten trotzdem (Undo in TaskMonkey bleibt, Version und Cache-Leeren fehlen dann). Für den vollen Umfang den Connector im Manage-Bereich neu herunterladen und hochladen.
Update auf Gateway-Protokoll 3
Gateways, die vor den System-Profilen deployt wurden (Protokoll ≤ 2), laufen ohne Tabellen-Prüfung weiter, bis sie ablaufen. getGatewayStatus meldet sie als outdated, ensureGateway ersetzt sie beim nächsten Aufruf automatisch. Laufende Gateways nach dem Update einmal neu deployen (deployGateway oder refreshDatabaseGateways).
Tools, die auf Tabellen außerhalb des Profils zugreifen (eigene Erweiterungen, generische Anwendungen), brauchen danach tables/extra_tables in der Connection-Config.
Beispiel-Flow im Chat
User: "Welche News sind im Kundenweb diese Woche erschienen?"
Modell:
1. ensureGateway(project: 'kunde_web') → Gateway läuft (oder wird deployt)
2. executeQuery(sql: "SELECT headline, FROM_UNIXTIME(date) FROM tl_news WHERE date > UNIX_TIMESTAMP() - 604800")
Antwort: "Drei News: …"
Einmalige Recherchen können am Ende removeGateway rufen. Für wiederkehrende Tasks ensureGateway nehmen und das Gateway laufen lassen.
Use-Cases
- Content-Pflege: News, Seiten, Produkttexte anlegen oder korrigieren
- Migrations-Checks: „Sind alle Datensätze aus der alten DB rüber?"
- Live-Reporting: „Zeig mir den heutigen Umsatz aus dem Shop" (Bestellungen per
extra_tables) - Konsistenz-Checks: „Gibt es Produkte ohne Kategorie?"
Was sich nicht dafür eignet:
- Massendaten-Migrationen (Tool-Calls haben ein Token-Budget)
- Zugriff auf Benutzer-, Kunden- oder Zahlungsdaten — dafür ist das Gateway absichtlich gesperrt
- Daten, die du dauerhaft brauchst — pull sie einmal, leg sie in deine eigene DB (z. B. Supabase)
Verwandt
- Supabase — wenn du eine eigene DB brauchst, nicht eine fremde anzapfen willst
- Eigene REST-API anbinden — wenn das Zielsystem eine API hat, nimm die statt SQL
Debug
tm test-tool ensureGateway project=kunde_shop
tm test-tool executeQuery project=kunde_shop sql='SHOW TABLES'
tm logs
- Deploy schlägt fehl: meistens SSH-Key falsch,
web_rootnicht beschreibbar, oderbase_urlzeigt nicht auf den Web-Root. Reines SSH-Login zum Ziel manuell testen. Host key verification failed: der Server hat einen anderen Host-Key als beim ersten Kontakt. Erst klären, warum (Umzug? Angriff?), dannstorage/database_gateways/known_hosts/<workspace>_<projekt>löschen oderhost_keysetzen.Access to table '…' is not allowed (profile …): die Tabelle steht nicht im Profil oder auf der Denylist.deployGateway/ensureGatewayzeigen, welches Profil erkannt wurde; ggf.profile,tablesoderextra_tablessetzen.Query failed: …miterror_code: die Details (Original-Meldung der DB) stehen im Tool-Log.- Connector antwortet mit
404: Datei nicht (mehr) im Web-Root,base_urlzeigt woanders hin, IP nicht inallowed_ipsoder Secret passt nicht (z. B. Connector neu heruntergeladen, aber alte Datei hochgeladen). Neu herunterladen, hochladen, testen. Uhrzeit weicht ab: Server-Uhr des Kunden liegt mehr als 5 min daneben — Hoster ansprechen.Connector state file not writable: PHP darf im Connector-Verzeichnis nicht schreiben. Verzeichnis beschreibbar machen oderstate_dirsetzen und neuen Connector erzeugen.Backup directory must be outside the web root/could not be created/ „Automatisches Backup vor dem Schreiben fehlgeschlagen“:backup_dirauf ein beschreibbares Verzeichnis außerhalb des Web-Roots setzen (bei Connectoren danach neu herunterladen).deployGatewayzeigt unterbackups.dir, wohin das Gateway schreiben würde.Backup aborted: server time limit reached/larger than … MB: Laufzeitlimit bzw.backup_max_mb— Tabellen einschränken, Limit erhöhen oder beim Hostermax_execution_timeanheben.