Erscheinungsbild
Security
Grenzen des Starters
- Geheimnisse existieren nur in GitHub Secrets und in serverseitigen Konfigurationsdateien, niemals im Repository und niemals in einer
PUBLIC_*-Variable. - Formulare akzeptieren serverseitig ausschließlich Felder aus einer Allowlist.
- Fehlermeldungen nach außen sind neutral; Details stehen nur im Server-Log.
Was das Frontend beiträgt
Der Schutz gegen Missbrauch liegt im Backend — es ist die einzige Seite, die sich nicht umgehen lässt. Das Frontend liefert die Bausteine dafür und trifft bewusst keine Entscheidung:
- Honeypot. Ein Feld, das aus dem Layout genommen und aus dem Sichtfeld geschoben wird — ausdrücklich kein
display: none. Der gefüllte Zustand wird mitgesendet, statt im Browser abgelehnt zu werden: Sonst stünde die Prüfung im ausgelieferten Bündel zum Nachlesen. - Turnstile. Lädt erst bei der ersten Berührung des Formulars. Der Site Key ist öffentlich und gehört ins HTML; der Secret Key liegt ausschließlich im Backend.
- Übermittlungskennung. Eine Kennung je Vorgang, stabil über einen zweiten Versuch hinweg, damit ein Backend Doppelsubmissions erkennen kann.
- Feld-Allowlist. Was gesendet wird, entscheidet
src/content/forms.ts— nicht das, was zufällig im Formular steht.
Alles Weitere — Origin-Prüfung, Größenlimits, Rate Limiting, Captcha-Verifikation — gehört ins Backend.
Was das Backend entscheidet
Der PHP-Handler prüft in dieser Reihenfolge, und die Reihenfolge ist Teil der Zusage:
| Prüfung | Wogegen sie hilft |
|---|---|
POST + JSON-Content-Type | Ein <form> auf einer fremden Seite kann diesen Typ ohne Preflight nicht senden |
| Größenlimit | Ein Rumpf, der die Verarbeitung belasten soll |
| Origin-Allowlist | Eine fremde Seite, die im Browser eines Besuchers sendet |
| Vertragsprüfung | Alles, was nicht die vereinbarte Form hat |
| Honeypot | Ausfüllende Skripte |
| Rate Limiting | Wiederholung — je Adresse und für alle zusammen |
| Doppelsubmission | Der zweite Klick auf „Absenden" |
| Feldprüfung | Werte, die das Formular gar nicht angeboten hat |
| Captcha | Was die vorherigen Hürden überstanden hat |
Zwei Entscheidungen daran sind erklärungsbedürftig und stehen in ADR 0010: Die Feldprüfung läuft vor dem Captcha, weil ein Turnstile-Token einmal einlösbar ist; und ist Cloudflare nicht erreichbar, wird abgelehnt statt durchgelassen.
Datensparsamkeit
- Das Log enthält Zeitpunkt, Formular, Ausgang, Grund und Kennung — keine Feldinhalte. Die Adresse des Absenders steht nur als Hash darin, gebildet mit einem Wert, den nur diese Installation kennt.
- Rate-Limit-Zähler tragen denselben Hash als Dateinamen; im Ablageverzeichnis steht keine IP-Adresse.
- Log und Kennungen werden nach einer konfigurierten Frist gelöscht (
retentionDays, Vorgabe 30 Tage). - Der Handler speichert keine Anfragen. Was bleibt, ist die E-Mail im Postfach des Kunden.
Geheimnisse
| Wert | Wo er liegt |
|---|---|
| Turnstile Site Key | PUBLIC_TURNSTILE_SITE_KEY — öffentlich per Design |
| Turnstile Secret Key | KT_FORMS_TURNSTILE_SECRET beim Handler |
| SMTP-Zugangsdaten | config/config.local.php oder KT_FORMS_SMTP_* |
| SSH-Schlüssel fürs Deployment | GitHub Secret |
config/config.local.php steht in .gitignore, liegt auf dem Server und wird nicht mit dem Build ausgeliefert. Die .htaccess des Handler-Verzeichnisses sperrt sie zusätzlich gegen HTTP-Zugriff — greift sie nicht, ist das SMTP-Passwort lesbar.
Header und Content Security Policy
Bei deployment.target: 'mittwald-static' entsteht beim Bauen eine .htaccess mit den Sicherheits-Headern:
| Header | Wert |
|---|---|
X-Content-Type-Options | nosniff |
Referrer-Policy | strict-origin-when-cross-origin |
X-Frame-Options | SAMEORIGIN |
Permissions-Policy | Kamera, Mikrofon, Standort, Zahlung, USB gesperrt |
Strict-Transport-Security | nur nach ausdrücklicher Entscheidung |
Content-Security-Policy | erzeugt, standardmäßig als Report-Only |
Die Richtlinie entsteht aus der Projektkonfiguration und dem erzeugten HTML — die Einzelheiten und der Weg von „beobachten" zu „erzwingen" stehen unter Content Security Policy.
Zwei Inline-Skripte sind Absicht
Das Consent-Skript im Kopf muss laufen, bevor irgendetwas anderes lädt (ADR 0008); das Navigationsskript bündelt Astro so klein, dass es im Dokument landet. Beide werden über ihren SHA-256-Hash abgedeckt, nicht über 'unsafe-inline'. Die JSON-LD-Ausgabe braucht keinen Hash: Sie ist ein Datenblock, den der Browser nie ausführt (ADR 0013).
Auf GitHub Pages gibt es diese Header nicht
Dort wird keine .htaccess gelesen, und ein <meta http-equiv> deckt frame-ancestors und report-uri nicht ab. Ein Projekt, dem diese Header wichtig sind, gehört auf mittwald-static.