Funktionen
Überblick über das, was das Gateway tatsächlich kann. Bedienung dazu: Admin-UI; Einbau auf der Kundenseite: Formular einbinden.
Mandantenfähigkeit
Abschnitt betitelt „Mandantenfähigkeit“Ein Mandant ist eine Kundenseite. Angelegt wird er im Admin-UI, alles Weitere hängt daran:
- Site-Key (
site_…) — die öffentliche Kennung im Client-JS. Kein Geheimnis, nur Zuordnung; die Sicherheit liegt komplett serverseitig. - Empfänger-Adresse — das Ziel der Formularmails. Kommt nie aus dem Formular, sondern immer aus der Mandanten-Konfiguration.
- Origin-Allowlist — von welchen Domains Absendungen angenommen werden. Serverseitig geprüft, nicht bloß per CORS.
- Eigene SMTP-Route oder geteilte Default-Route — jeder Mandant kann über den eigenen Mailserver versenden oder über den der Plattform.
- Kill-Switch — ein Klick, und der Mandant lehnt sofort alles ab.
Die Formulardaten jedes Mandanten liegen in einem eigenen ORM++-Tenant — Submissions und Nonces sind dadurch hart getrennt.
Kein Formular-Schema
Abschnitt betitelt „Kein Formular-Schema“Das Gateway kennt keine Feldliste. Jedes <input name="…"> wird
übertragen und landet im Mailtext; ein neues Feld braucht nur HTML, keine
Konfiguration im Gateway. Nur drei Feldnamen sind reserviert:
_gw_reply (wird Reply-To), _gw_form (Formular-Kennung) und _gw_hp
(Honeypot, setzt das Snippet selbst).
Details und Grenzen: Formular einbinden.
Schutz vor Spam und Missbrauch
Abschnitt betitelt „Schutz vor Spam und Missbrauch“Die Prüf-Pipeline arbeitet „billig zuerst” — was am wenigsten kostet, läuft zuerst, damit ein Angriff früh abbricht:
| # | Schicht | Wirkung |
|---|---|---|
| 1 | Rate-Limits (global, IP, Site-Key) | in-memory, vor allem anderen |
| 2 | Mandant aktiv? Origin erlaubt? | Kill-Switch und Allowlist, serverseitig |
| 3 | Challenge-Token | HMAC-signiert, TTL 10 min, an Site und Origin gebunden |
| 4 | Mindest-Ausfüllzeit | Formular in unter n Sekunden ausgefüllt = Bot |
| 5 | Honeypot | unsichtbares Feld; Treffer täuscht Erfolg vor |
| 6 | IP-/ASN-Reputation | Tor-Exits, iptoasn.com, eigene Blocklist |
| 6b | Herkunftssperre | Länder-/Regionsregel des Mandanten (IPv4 und IPv6) |
| 7 | Proof-of-Work | pro Mandant konfigurierbare Bit-Schwelle |
| 7b | ALTCHA (optional) | sichtbares Widget statt unsichtbarer Rechenarbeit, geprüft über den eigenen ALTCHA-Dienst |
| 8 | Nonce | atomar eingelöst — ein Token gilt genau einmal (Replay-Schutz) |
| 9 | Eingabe-Härtung | Feld- und Längenlimits, CRLF-Schutz, Reply-To-Validierung |
| 10 | E-Mail-Blockmuster | Sperrmuster gegen die Antwortadresse |
| 11 | Tageskontingent | persistent, zählt wartende und gesendete Mails |
| 12 | Spam-Score | ab Schwelle Quarantäne statt Versand |
ALTCHA statt Proof-of-Work
Abschnitt betitelt „ALTCHA statt Proof-of-Work“Das eingebaute Proof-of-Work arbeitet unsichtbar: Der Browser rechnet, der
Besucher merkt nichts. Manche Formularseiten wollen stattdessen ein
sichtbares, barrierefreies Widget — dafür lässt sich je Mandant ALTCHA
zuschalten (altcha_enabled).
ALTCHA ist ein Captcha-Ersatz ohne Cookies und ohne fremde Anbieter. Geprüft wird nicht im Gateway, sondern über den eigenen ALTCHA-Dienst: Dort liegt der Signaturschlüssel, und dort wird auch verhindert, dass dieselbe Lösung zweimal zählt.
Einrichten: In der config.json den Dienst hinterlegen —
"altcha": { "url": "https://altcha.techeve.de", "token": "…", "timeout_seconds": 5}— und beim Mandanten altcha_enabled setzen. Ohne url bleibt alles beim
Proof-of-Work; Mandanten mit altcha_enabled werden dann abgewiesen statt
ungeprüft durchgelassen.
Auf der Formularseite gehört das ALTCHA-Widget ins Formular. Es legt selbst
ein verstecktes Feld namens altcha an, dessen Wert das Snippet mitschickt:
<script src="https://altcha.techeve.de/widget.js"></script><form data-gateway="…"> … <altcha-widget challenge="https://altcha.techeve.de/challenge" name="altcha" auto="onload"></altcha-widget> <button type="submit">Absenden</button></form>Das Widget platziert der Seitenbetreiber selbst — so bleibt ihm die Gestaltung überlassen.
Zwei Elemente tragen denselben Namen. Das
<altcha-widget>selbst führt dasname-Attribut, das eigentliche Ergebnis steht in einem<input type="hidden">dahinter. Wer den Wert selbst ausliest, muss deshalbinput[name="altcha"]schreiben — ein Selektor ohneinputfindet zuerst das Widget und hält das Formular fälschlich für ungelöst. Fehlt es, obwohl der Mandant ALTCHA verlangt, bricht das Snippet mit einer Meldung ab, statt eine Einreichung zu schicken, die ohnehin abgewiesen würde.
Ist der Dienst gestört, wird abgewiesen. Das ist Absicht: Ohne ihn hätte der Besucher überhaupt keine Aufgabe bekommen, und ein stiller Durchlass würde den Schutz bei jeder Störung aushebeln.
Wiederholt auffällige IPs kann das Gateway zusätzlich an eine CrowdSec LAPI melden — die vorgelagerte Firewall sperrt sie dann, bevor der nächste Request überhaupt ankommt (Sicherheit).
Nach außen bleibt das Gateway wortkarg: jede Ablehnung ist
400 {"ok":false,"reason":"rejected"}. Welche Schicht gegriffen hat,
steht nur im Log und im Admin — ein Spammer bekommt kein Orakel, an dem
er seine Versuche kalibrieren kann. Honeypot-Treffer und Quarantäne sehen
für den Client sogar wie Erfolg aus.
Alle Zahlenwerte (PoW-Bits, Spam-Schwelle, Tageskontingent,
Mindest-Ausfüllzeit) sind pro Mandant einstellbar und einzeln
abschaltbar (0).
Quarantäne statt Wegwerfen
Abschnitt betitelt „Quarantäne statt Wegwerfen“Was über der Spam-Schwelle liegt, wird nicht verworfen, sondern quarantänisiert. Im Mandanten-Detail lässt sich jede Absendung freigeben (geht dann in den Versand) oder verwerfen. Falsch positive Treffer kosten damit keine Nachricht.
Versand
Abschnitt betitelt „Versand“- SMTP-Routen mit STARTTLS (587), implizitem TLS (465) oder unverschlüsselt (nur für Relays auf derselben Maschine).
- Fester Absender pro Route — kein frei wählbares
Fromaus dem Formular. Das schützt die Reputation der Absenderdomain; die Antwortadresse des Einsenders reist alsReply-To. - Retry mit Backoff über eine Queue in der Datenbank; ein kurzzeitig nicht erreichbarer Mailserver verliert keine Nachricht.
- Test-Versand-Button im Admin prüft Verbindung und Login, bevor echte Formulare darauf laufen.
- Health-Anzeige je Route: letzter Fehler, letzter erfolgreicher Versand.
Eingangsbestätigung an den Einsender
Abschnitt betitelt „Eingangsbestätigung an den Einsender“Optional pro Mandant: Wer das Formular abschickt, bekommt eine kurze
Bestätigung an die Adresse, die er selbst eingetragen hat (Feld
_gw_reply). Den Text pflegt der Admin als Freitext — er steht so in
der Mail, wie er im Admin steht.
- Aus per Default. Ohne eingeschaltete Option und ohne Antwortadresse im Formular passiert nichts.
- Ohne die eingesendeten Daten. Die Bestätigung enthält bewusst nur den festen Text. Würden die Formularfelder mitgeschickt, ließe sich über das Formular beliebiger Text an eine beliebige fremde Adresse schicken — das Gateway wäre ein Verstärker.
- Erst nach bestandener Prüfung. Quarantänisierte Einsendungen lösen keine Bestätigung aus; erst die Freigabe im Admin tut es. Ein mutmaßlicher Spammer erfährt so auch nicht, dass seine Adresse zugestellt werden kann.
- Zählt auf das Tageskontingent, weil sie an eine vom Absender bestimmte Adresse geht — das begrenzt zugleich das Missbrauchspotenzial.
- Antworten erreichen den Seitenbetreiber:
Reply-Toder Bestätigung ist die Empfänger-Adresse des Mandanten.
Herkunftssperre (Geo)
Abschnitt betitelt „Herkunftssperre (Geo)“Pro Mandant lässt sich einschränken, aus welchen Ländern Formulare abgeschickt werden dürfen:
| Modus | Bedeutung |
|---|---|
off | keine Einschränkung (Standard) |
allow | nur die gelisteten Länder dürfen absenden |
deny | die gelisteten Länder dürfen nicht absenden |
Gelistet werden Ländercodes (DE, AT, US …) oder Gruppen:
DACH (DE/AT/CH), EU (die 27 Mitgliedstaaten), EEA (EU + IS/LI/NO),
EUROPE (geografisches Europa inkl. CH, GB, UA).
Die Zuordnung IP → Land kommt aus demselben iptoasn-Dump, der ohnehin täglich für die ASN-Prüfung geladen wird — kein zusätzlicher Dienst, keine Anfrage an Dritte pro Formular. IPv4 und IPv6 werden gleich behandelt; eine Sperre lässt sich also nicht durch einen IPv6-Anschluss umgehen.
Wichtig — unbekannte Herkunft blockt nicht. Lässt sich eine IP
keinem Land zuordnen (nicht geroutet, Dump noch nicht geladen oder
Download gescheitert), gilt sie als erlaubt — auch im allow-Modus.
Andernfalls würde ein gescheiterter Download eines Fremdservers das
Kontaktformular komplett stilllegen. Die übrigen Schichten (PoW,
Honeypot, Spam-Score, Quarantäne) greifen weiter.
Jeder Geo-Treffer erscheint als blocked-Submission im Admin (mit
Grund, z. B. „herkunftsland US gesperrt (modus allow)”) und zählt für
die Angriffserkennung mit. Hämmert dieselbe IP dauerhaft dagegen,
reißt sie die CrowdSec-Schwelle und wird von der vorgelagerten
Firewall gesperrt — bewusst mit niedrigem Gewicht, damit ein einzelner
Geschäftsreisender im Ausland nicht sofort in der Firewall landet.
Blocklist
Abschnitt betitelt „Blocklist“Eigene Sperrliste für IP, CIDR, ASN, E-Mail-Adressen und Länder/Regionen, jeweils mit Notiz. Einträge wirken sofort — sie werden in den Reputations-Speicher gespiegelt und greifen in Schicht 6 der Pipeline. Ein Länder-Eintrag hier gilt global für alle Mandanten; die mandantenspezifische Regel steht im Mandanten-Formular.
Admin-Bereich
Abschnitt betitelt „Admin-Bereich“Web-UI und API im selben Binary:
- Benutzer mit argon2id-Passwörtern und Rollen (RBAC, statischer Katalog im Code).
- API-Keys (
fgw_…) für Prozesse ohne Browser — mit Ablaufdatum, Widerruf,read-Scope und eigenem Rate-Limit. - Statistiken pro Mandant und Submission-Historie mit Statusfilter.
Details: Sicherheit.
Datenschutz
Abschnitt betitelt „Datenschutz“- Selbst gehostet — die Formulardaten verlassen die eigene Infrastruktur nicht.
- Client-IPs landen nur als gekürzter SHA-256-Hash in der Datenbank.
- Keine Cookies, kein Tracking im Client-Snippet, keine Drittressourcen.
- DSGVO-Export und -Purge pro Kundenseite über die ORM++-Tenant-Registry — Auskunft und Löschung sind eine Operation, kein SQL-Abenteuer.
- Audit-Log durch Event-Sourcing: jede Submission trägt ihre
Statushistorie (
received → scored → sent/quarantined → released/discarded).
Client-Snippet
Abschnitt betitelt „Client-Snippet“Dependency-freies Vanilla-JS, ~1,6 KB gzip, minifiziert vom Gateway
ausgeliefert. Holt die Challenge, löst den Proof-of-Work in einem Web
Worker (mit Fallback auf den Hauptthread), sendet ab und zeigt den Status
inline an. Mehrere Formulare pro Seite werden automatisch verdrahtet;
ohne JavaScript bleibt das <form> ein normales Formular.
Betrieb
Abschnitt betitelt „Betrieb“- Ein Binary — Go-Backend, Svelte-Admin-UI und Client-Snippet sind
einkompiliert. Eine Installation besteht aus Binary,
config.jsonund Datenbankdatei. - SQLite oder PostgreSQL, umgeschaltet durch eine Konfigurationszeile ohne Codeänderung.
- CGO-frei ⇒ Cross-Compiling für Linux (amd64/arm64), Windows und macOS ohne Toolchains.
- Docker-Image (Alpine, non-root, read-only, Healthcheck) und systemd-Betrieb sind dokumentiert.
- Graceful Shutdown bei SIGINT/SIGTERM, inklusive Hintergrund-Worker.
- Versions- und Build-Info über
-version,GET /api/v1/system/infound den Footer der Web-App.