IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Schlagwort: SelfHosted (Seite 1 von 7)

Fundstücke aus dem Netz: Angie, llmfit, idiocracy.wtf und KI-Alert-Analyse

Beitragsbild: Fundstücke aus dem Netz – Angie Nginx-Fork, llmfit Hardware-Check, idiocracy.wtf und KI-gestützte Alert-Analyse für Kubernetes

Kurze Pause von den eigenen Projekten, heute kein tiefer Dive. Ein paar Fundstücke, die in den letzten Wochen offen im Browser lagen und aus unterschiedlichen Gründen hängen geblieben sind. Keine Reviews, keine lange Analyse, einfach „schaut euch das mal an“.

Angie: Nginx-Fork von den alten Entwicklern

Angie ist ein Drop-in-Ersatz für nginx, gestartet von ehemaligen nginx-Core-Entwicklern nachdem F5 den Laden übernommen hat. Konfig-Syntax 100 Prozent kompatibel, dazu out-of-the-box HTTP/3, eine REST-API für Metriken, Prometheus-Export, Docker-Integration für dynamische Upstreams und automatisches ACME-Handling ohne Certbot-Gefrickel. Ob ich hier irgendwann mal umsteige, keine Ahnung, aber im Auge behalten ist es definitiv wert.

llmfit: Welches Modell läuft eigentlich auf meiner Kiste?

llmfit ist ein kleines Terminal-Tool, das eure Hardware abklopft (RAM, VRAM, CPU, GPU) und euch sagt, welche lokalen Sprachmodelle darauf realistisch laufen. Über 400 Modelle in der Datenbank, filterbar nach Parametern, Quantisierung, Architektur und Kontextlänge, dazu automatische Runtime-Erkennung für vLLM, MLX oder llama.cpp. Spart eine Menge Zeit beim Rumprobieren, welche GGUF-Quant-Stufe jetzt noch auf die 16 GB VRAM passt.

idiocracy.wtf: Sind wir schon so weit?

idiocracy.wtf im Stil der alten „Is it weekend?“-Seiten, mit nur einer Frage: „Are We Idiocracy Yet?“. Sinnlos, minimalistisch und genau deshalb gut. Wer den Film von Mike Judge nicht kennt, unbedingt nachholen. Und wer ihn kennt, weiß schon warum hier gleichzeitig gelacht und geweint wird.

KI-gestützte Alert-Analyse für Kubernetes und CheckMK

Ein wirklich lesenswerter Beitrag von geekbundle.org: KI-gestützte Alert-Analyse für Kubernetes und CheckMK. Monitoring-Alerts gehen per Webhook an ein kleines Open-Source-Projekt, das diagnostische Daten sammelt (Prometheus-Metriken, Pod-Logs, SSH-Diagnose) und Claude für die Root-Cause-Analyse nutzt. Ergebnis kommt als Push via ntfy zurück. Sauber umgesetzt mit unprivilegiertem User, Command-Denylist und Secret-Redaction, also genau so wie man sowas bauen will. Wer sich mit Agentic-AI im Ops-Umfeld beschäftigt, findet hier einen ehrlichen, praxisnahen Einstieg.

Siehe auch

Eigene Fundstücke oder Ergänzungen zu dem was hier steht? Gerne in den Kommentaren oder per fragen.

BIND auf FreeBSD: DoT & DoH einrichten mit Views, IP‑Trennung und Testplan für IPv4/IPv6.

Wofür braucht man noch gleich DoT oder DoH?

Nun, wenn du eine Internetadresse eingibst, muss dein Gerät zuerst herausfinden, zu welchem Server diese Adresse gehört. Diese Nachfragen heißen DNS. Lange Zeit liefen sie unverschlüsselt durchs Netz, vergleichbar mit einer Postkarte. Jeder, der den Datenverkehr sehen konnte, wusste dadurch sehr genau, welche Webseiten aufgerufen werden, und konnte die Antworten sogar manipulieren.

Beitragsgrafik zu BIND 9.20 auf FreeBSD 15: schematische Trennung von autoritativem DNS und rekursivem Resolver. Links ein Authoritative-DNS-Server mit deaktivierter Rekursion und blockiertem UDP/53, rechts ein Resolver, der ausschließlich DNS over TLS (Port 853) und DNS over HTTPS (Port 443) anbietet. In der Mitte ein Schild mit DoT/DoH-Symbolen, Pfeile zeigen verschlüsselten DNS-Verkehr. Fokus auf Sicherheits- und Rollen-Trennung.

DoT und DoH lösen genau dieses Problem. Beide sorgen dafür, dass diese DNS-Nachfragen verschlüsselt übertragen werden. Bei DNS over TLS, kurz DoT, wird die Anfrage in eine eigene sichere Verbindung gepackt. Außenstehende sehen noch, dass eine DNS-Anfrage stattfindet, aber nicht mehr, welche Webseite gemeint ist. Bei DNS over HTTPS, kurz DoH, wird dieselbe Anfrage zusätzlich im normalen Webseitenverkehr versteckt. Von außen sieht sie aus wie ein ganz gewöhnlicher Zugriff auf eine Website.

Der Zweck von beiden ist also derselbe: Schutz der Privatsphäre und Schutz vor Manipulation. Der Unterschied liegt darin, wie sichtbar diese Nachfragen noch sind. DoT ist transparent und gut kontrollierbar, DoH ist unauffälliger, kann dafür aber lokale Regeln und Schutzmechanismen umgehen.

Mal angenommen, du möchtest eine gewisse Webseite aufrufen. Dann geht der Client los und holt über einen DNS-Server die IP-Adressen vom Server. Dies kann man mitlesen und ggf. verändern. Mitlesen sagt dem Mitlesenden, wo du dich so im Internet herumtreibst. Verändern könnte man als Angriff nutzen, indem man dir einfach eine andere Webseite vorsetzt, während du versuchst, dich in deinen Mailaccount einzuloggen. Beides wird durch DoH und DoT deutlich erschwert.

Dann soll es ja Netzwerke geben, in welchen dir ein bestimmter DNS-Server aufgezwungen wird, weil dieser DNS-Server nach Werbung oder ungewollten Inhalten filtert. Damit dies nun ebenfalls nicht einfach umgangen werden kann, blockt man den Zugriff aus dem Netzwerk einfach auf die Ports, welche sonst für eine DNS-Abfrage benutzt werden (TCP/53, UDP/53, TCP/853). Da kommt nun DoH ins Spiel, denn das läuft auf dem ganz normalen HTTPS-Port TCP/443. Blockt man den, kann keiner mehr auf Webseiten zugreifen (ok, unverschlüsselt, aber hey, das macht doch keiner mehr, oder?).

Die Zeit ging weiter – BIND auch.
Meine älteren Artikel zu DoT/DoH waren für ihren Zeitpunkt korrekt, aber inzwischen hat sich an zwei Stellen richtig was getan:

  1. BIND spricht DoT/DoH nativ (kein Stunnel-/Proxy-Zirkus mehr nötig – außer du willst bewusst terminieren/filtern).
  2. „Authoritative + Public Resolver auf derselben Kiste“ ist ohne klare Trennung schnell ein Sicherheitsproblem (Open-Resolver/Reflection-Missbrauch lässt grüßen).

Darum gibt’s hier das Update:

  • ns1.kernel-error.de: nur autoritativ auf UDP/TCP 53 (Zonen, DNSSEC wie gehabt)
  • dns.kernel-error.de: Public Resolver nur auf DoT 853/TCP und DoH 443/TCP (rekursiv, DNSSEC-validierend)
  • Trennung über zusätzliche IPs + Views. Ergebnis: Authoritative bleibt „stumm rekursiv“, Resolver ist nur über TLS/HTTPS erreichbar.

Zielbild

Uff, ich muss zugeben, diesen Beitrag schon VIEL zu lange als Draft zu haben. Es ist einfach viel zu schreiben, bschreiben und mir fehlte die Zeit. Aber das kennt ihr ja. OK… das Zielbild, was soll es werden?

Was soll am Ende gelten:

  • Port 53 auf Authoritative-IP(s):
    • beantwortet nur meine autoritativen Zonen
    • keine Rekursion → REFUSED bei google.com
  • DoT/DoH auf separaten Resolver-IP(s):
    • rekursiv für „das ganze Internet“
    • DNSSEC-Validation aktiv
    • kein offenes UDP/53 → weniger Angriffsfläche für Reflection/Amplification

Warum das wichtig ist:
Ein „Public Resolver“ ist per Definition attraktiv für Missbrauch. Der Klassiker ist DNS-Amplification über UDP/53. Wenn man Rekursion auf 53 offen hat, ist man sehr schnell Teil fremder Probleme. DoT/DoH sind TCP-basiert – das ist schon mal deutlich unattraktiver für Reflection. (Nicht „unmöglich“, aber praktisch viel weniger lohnend.)

Warum „Views“ – und warum zusätzliche IPs?

1) Views – weil Policy pro Anfrage gelten muss

Wir wollen auf derselben named-Instanz zwei sehr unterschiedliche Rollen:

  • Authoritative: recursion no;
  • Resolver: recursion yes; + Root-Hints/Cache

Das muss pro eingehender Anfrage entschieden werden. Dafür sind Views da.

2) Also: Trennung über Ziel-IP (match-destinations)

Wenn wir DoH/DoT auf andere IPs legen, kann die View anhand der Zieladresse entscheiden:

  • Anfrage geht an 93.177.67.26 / 2a03:4000:38:20e::53auth-View
  • Anfrage geht an 37.120.183.220 / 2a03:4000:38:20e::853resolver-View

Und genau deshalb brauchen wir:

  • zusätzliche IPs (damit die Rollen sauber getrennt sind)
  • separaten FQDN dns.kernel-error.de (damit Clients überhaupt sinnvoll DoT/DoH nutzen können – und für TLS/SNI/Cert-Match)

Wenn du also grade ein ripe from ausfüllst und angeben musst, warum da eine weitere IPv4 Adresse „verbrannt“ werden soll, hast du nun eine gute Antwort.

BIND-Config

Ich beschreibe hier nur die Teile, die für das Rollen-Split relevant sind. Die Zonendateien/Slaves bleiben wie sie sind.

1) /usr/local/etc/namedb/named.conf – Views

Wichtig: Sobald wir view {} nutzen, müssen alle Zonen in Views liegen, sonst bricht named-checkconf ab. Das ist kein „Feature“, das ist BIND. Leicht nervig, vor allem wenn man nun viel in seinem Setup umschreiben muss. Aber ich eigentlich schon mal erwähnt, dass ich auf der Arbeit mal einen, nennen wir es mal View Ersatz, für powerdns gesehen habe? Da hat tatsächlich jemand mit einer Cisco ASA in die DNS Pakete geschaut und je nachdem welche quelle angefragt hat, wurde dann durch die ASA eine neue Adresse in die DNS Pakete geschrieben. Furchtbar! Richtig schlimm. Bis man so etwas findet, wenn man es nicht weiß. DNSsec geht kaputt und aaahhhhhhaaaaaahhhhh. Egal, mein PTBS kickt da grade. Öhm wo waren wir? Genau…

Beispiel:

include "/usr/local/etc/namedb/named.conf.options";

view "auth" {
    match-clients { any; };
    match-destinations { 93.177.67.26; 2a03:4000:38:20e::53; };

    recursion no;
    allow-recursion { none; };
    allow-query-cache { none; };
    allow-query { any; };

    include "/usr/local/etc/namedb/named.conf.default-zones";
    include "/usr/local/etc/namedb/named.conf.master";
    include "/usr/local/etc/namedb/named.conf.slave";
};

view "resolver" {
    match-clients { any; };
    match-destinations { 37.120.183.220; 2a03:4000:38:20e::853; 127.0.0.1; ::1; };

    recursion yes;
    allow-recursion { any; };
    allow-query-cache { any; };
    allow-query { any; };

    zone "." { type hint; file "/usr/local/etc/namedb/named.root"; };
};

Warum Root-Hints nur im Resolver-View?
Weil nur dieser View rekursiv arbeiten soll. Ohne Root-Hints ist Rekursion tot; dat wolln wa so!

2) /usr/local/etc/namedb/named.conf.options – Listener-Trennung + DoH/DoT

Der „Aha-Moment“ hier: Wir trennen nicht nur per View, sondern auch per listen-on.
Damit bindet named die Ports wirklich nur auf den gewünschten IPs.

Authoritative (nur 53):

listen-on { 93.177.67.26; 127.0.0.1; };
listen-on-v6 { 2a03:4000:38:20e::53; ::1; };

DoT auf Resolver-IPs (+ Loopback für lokale Tests):

listen-on port 853 tls local-tls { 37.120.183.220; 127.0.0.1; };
listen-on-v6 port 853 tls local-tls { 2a03:4000:38:20e::853; ::1; };

DoH auf Resolver-IPs (+ Loopback):
BIND 9.18+ kann DoH nativ, Endpoint typischerweise /dns-query

http doh-local {
    endpoints { "/dns-query"; };
    listener-clients 1000;
    streams-per-connection 256;
};

listen-on port 443 tls local-tls http doh-local { 37.120.183.220; 127.0.0.1; };
listen-on-v6 port 443 tls local-tls http doh-local { 2a03:4000:38:20e::853; ::1; };

TLS-Block (DoT/DoH):

tls local-tls {
    cert-file "/usr/local/etc/nginx/ssl/wild.kernel-error.de/2025/ecp/chain.crt";
    key-file "/usr/local/etc/nginx/ssl/wild.kernel-error.de/2025/ecp/http.key";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256";
    cipher-suites "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256";
    prefer-server-ciphers yes;
    session-tickets no;
};

„Ich schalte nginx davor – muss BIND TLS können?“
Wenn nginx wirklich TLS terminiert, kann BIND auch ohne TLS dahinter laufen – dann sprichst du intern HTTP/2 cleartext oder HTTP/1.1, je nach Setup. Das habe ich ebenfalls so umgesetzt, es hängt immer etwas davon ab, was man so will und wie groß das Setup wird. Ich lasse es in diesem Beitrag aber mal weg, so läuft alles nur mit bind. Ob BIND dafür „tls none“/HTTP-Listener sauber unterstützt, hängt an der BIND-DoH-Implementierung – hier ist die BIND/ARM-Doku die Wahrheit. bind9.readthedocs.io+1

Testplan – Linux-CLI – bewusst IPv4 und IPv6

Wir wollen natürlich einmal reproduzierbar testen. Also: jede Stufe zweimal. Einmal -4, einmal -6. Also ob es bei IPv4 und bei IPv6 jeweils korrekt ist. Ihr könnt euch nicht vorstellen, wie oft ich fest davon überzeugt bin, es für beide Adressfamilien korrekt konfiguriert zu haben, dann aber noch ein unterschied zwischen v4 und v6 ist. Daher testen wir das.

Voraussetzungen auf Linux

which dig kdig curl openssl

Schritt 1 – DoT-TLS-Handshake prüfen (IPv4/IPv6)

IPv4

openssl s_client \
  -connect 37.120.183.220:853 \
  -servername dns.kernel-error.de \
  -alpn dot

Erwartung:

  • Zertifikat passt auf dns.kernel-error.de (SAN / Wildcard ok)
  • ALPN protocol: dot
  • Verify return code: 0 (ok)

IPv6

openssl s_client \
  -connect '[2a03:4000:38:20e::853]:853' \
  -servername dns.kernel-error.de \
  -alpn dot

Wenn das passt, ist TLS-Transport ok. Also nur die TLS Terminierung für IPv4 und IPv6, da war noch keine DNS Abfrage enthalten.

Schritt 2 – DoT-Query (kdig) – IPv4/IPv6

IPv4

kdig +tls @37.120.183.220 google.com A

Erwartung:

  • status: NOERROR
  • Flags: rd ra (Recursion Desired/Available)
  • eine A-Antwort

IPv6

kdig +tls @[2a03:4000:38:20e::853] google.com A

Gleiche Erwartungshaltung wie bei IPv4.

Schritt 3 – Sicherstellen: kein Resolver auf UDP/TCP 53

Resolver-IPs dürfen auf 53 nicht antworten

dig -4 @37.120.183.220 google.com A
dig -6 @2a03:4000:38:20e::853 google.com A

Erwartung:

  • Timeout / no servers reached
    Genau das wollen wir ja: kein UDP/53 auf den Resolver-IPs.

Authoritative-IPs dürfen nicht rekursiv sein

dig -4 @93.177.67.26 google.com A
dig -6 @2a03:4000:38:20e::53 google.com A

Erwartung:

  • status: REFUSED
  • idealerweise EDE: (recursion disabled)
    Das ist genau die „nicht missbrauchbar als Open-Resolver“-Bremse.

Und unser positiver Check:

dig -4 @93.177.67.26 kernel-error.de A
dig -6 @2a03:4000:38:20e::53 kernel-error.de A

Erwartung:

  • aa gesetzt (authoritative answer)
  • Antwort aus meiner Zone

Schritt 4 – DoH GET (Base64url) – IPv4/IPv6

4.1 Query bauen (DNS-Wireformat → base64url)

Beispiel google.com A:

echo -n -e '\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' \
| base64 -w0 | tr '+/' '-_' | tr -d '='

Das Ergebnis ist mein dns= Parameter (base64url ohne = padding). Das ist DoH-Standard nach RFC 8484.

4.2 DoH GET erzwingen – IPv4

curl -4 --http2 -s \
'https://dns.kernel-error.de/dns-query?dns=<DEIN_DNS_PARAM>' \
| hexdump -C

IPv6

curl -6 --http2 -s \
'https://dns.kernel-error.de/dns-query?dns=<DEIN_DNS_PARAM>' \
| hexdump -C

Erwartung:

  • HTTP/2 200
  • content-type: application/dns-message
  • Im Hexdump siehst du eine valide DNS-Response.

Schritt 5 – DoH POST (application/dns-message) – IPv4/IPv6

Das ist der „richtige“ DoH-Weg für Tools/Clients.

IPv4

printf '\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' \
| curl -4 --http2 -s \
  -H 'content-type: application/dns-message' \
  --data-binary @- \
  https://dns.kernel-error.de/dns-query \
| hexdump -C

IPv6

printf '\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' \
| curl -6 --http2 -s \
  -H 'content-type: application/dns-message' \
  --data-binary @- \
  https://dns.kernel-error.de/dns-query \
| hexdump -C

Erwartung:

  • DNS-Response im Wireformat
  • keine HTML-Antwort, kein Redirect-Quatsch

Was wir damit jetzt sicher(er) gelöst haben:

  • Kein Open-Resolver auf UDP/53 → massiver Gewinn gegen DNS-Amplification.
  • Authoritative bleibt Authoritative → Zonen-Betrieb unverändert stabil.
  • Resolver nur über DoT/DoH → TCP/TLS-Transport, weniger Missbrauchsfläche.
  • Saubere technische Trennung → Views per Ziel-IP sind simpel, robust, nachvollziehbar.

Und ja: „Public Resolver“ heißt trotzdem Monitoring/Rate-Limiting/Abuse-Handling.
Das Feintuning (RRL, QPS-Limits, minimal-responses, Response-Policy, ggf. ECS-Handling, Logging, Fail2ban-Signale) ist das nächste Kapitel. Wobei, wenn ich grade auf die TLS Parameter schaue, sollte ich da vielleicht noch mal nacharbeiten, hm?

Wenn ihr noch eine kleine liste von erreichbaren Servern sucht: GitHub-curl-wiki

Alles hilft natürlich nicht, wenn man am Ende doch komplett IP- oder Hostnamebasiert geblockt wird. In China ist da nicht viel zu holen und auch hier gibt es immer mal wieder etwas.


Japp… TLS geht besser. Im Beitrag habe ich es oben schon angepasst, es war:

tls local-tls {
    cert-file "/pfad/chain.crt";
    key-file  "/pfad/http.key";
    dhparam-file "/pfad/dhparam.pem";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256";
    prefer-server-ciphers yes;
    session-tickets no;
};
  • dhparam-file ist komplett raus weil, ja weil es nicht benutzt wird ich mach ja kein DHE sondern ECDHE
  • cipher-suites für TLS1.3 waren nicht gesetzt.
  • Dann konnten auch gleich die Cipher aufgeräumt werden.

Hey, da hat es sich doch gelohnt, das mal runter zu schreiben. So habe ich es direkt gefunden und nicht erst, weil mich jemand von euch darauf hinweist (macht das aber bitte immer wenn ich hier Mist schreibe) oder es beim nächsten eigenen Audit auffällt.

Siehe auch: HTTPS RR und SVCB Records — die passenden DNS-Records, damit Clients dieses DoH/DoT-Setup automatisch entdecken können (RFC 9461).

GPT in Rspamd aktivieren: so nutze ich das LLM-Signal im Score

Rspamd web interface showing GPT module spam scores

Seit einiger Zeit nutze ich das GPT-Modul von Rspamd, um bei der Spam-Erkennung ein zusätzliches Signal zu bekommen. Es ersetzt nichts — kein Bayes, kein DKIM, kein RBL — sondern ist ein weiterer Sensor im Gesamtbild. Wer sich fragt, wie das in der Praxis aussieht und worauf man achten muss: hier mein aktuelles Setup.

Update 2026-02-13: Dieser Beitrag wurde komplett überarbeitet. Die ursprüngliche Version nutzte json=false, was zu Parse-Problemen führte. Außerdem fehlte ein Custom Prompt — und genau das ist der entscheidende Punkt, wie sich herausgestellt hat.

Voraussetzungen

  • Rspamd >= 3.12 mit GPT-Plugin (bei mir aktuell 3.14.0 auf FreeBSD 15.0)
  • Ein OpenAI API-Key (oder kompatibler Endpoint)
  • Grundverständnis von Rspamd Metrics und Actions

OpenAI API-Key anlegen

OpenAI API usage dashboard for Rspamd GPT integration

Wer noch keinen Key hat: Auf platform.openai.com einloggen, unter API Keys einen neuen Service-Account-Key erzeugen. Der Key wird nur einmal angezeigt — sicher ablegen. Den Verbrauch sieht man im Dashboard. Bei gpt-4o-mini und Mailfiltering sind die Kosten minimal.

Die Konfiguration: gpt.conf

Hier meine aktuelle /usr/local/etc/rspamd/local.d/gpt.conf:

enabled = true;
type = "openai";
model = "gpt-4o-mini";
api_key = "GEHEIMER-KEY";

model_parameters {
  gpt-4o-mini {
    max_tokens = 160;
    temperature = 0.0;
  }
}

timeout = 10s;
allow_ham = true;
allow_passthrough = false;
json = true;

prompt = "You are an email spam detector. Analyze the email and respond with ONLY a JSON object, no other text. The JSON must have these fields: "probability" (number 0.00-1.00 where 1.0=spam, 0.0=ham), "reason" (one sentence citing the strongest indicator). Example: {"probability": 0.85, "reason": "Unsolicited offer with urgent language and suspicious links."}  LEGITIMATE patterns: verification emails with codes, transactional emails (receipts, confirmations), newsletter unsubscribe links. Flag as spam only with MULTIPLE red flags: urgent threats, domain impersonation, requests for credentials, mismatched URLs.";

symbols_to_except {
  RCVD_IN_DNSWL_MED   = -0.1;
  RCVD_IN_DNSWL_HI    = -0.1;
  DWL_DNSWL_MED        = -0.1;
  WHITELIST_RECP_ADDR = -0.1;
  BAYES_HAM           = -0.1;
  SPAMTRAP            = 0;
  RCPT_IN_SPAMTRAP    = 0;
  SPAMTRAP_ADDR       = 0;
  RCVD_VIA_SMTP_AUTH  = 0;
  LOCAL_CLIENT        = 0;
  FROM_LOCAL          = 0;
}

Was hat sich gegenüber der alten Version geändert?

json = true und der Custom Prompt

Das ist die wichtigste Änderung. In meiner ursprünglichen Konfiguration stand json = false. Das funktionierte, hatte aber einen Haken: die Antwort des Modells wurde als Freitext geparst, was unzuverlässig war.

Mit json = true aktiviert Rspamd den JSON-Modus. Das Modell wird angewiesen, strukturiertes JSON zurückzuliefern, und der Parser erwartet ein Feld probability in der Antwort.

Und hier kommt der Fallstrick: Der Default-Prompt von Rspamd passt nicht zum JSON-Modus. Er fordert das Modell auf, nummerierte Textzeilen zurückzugeben:

Output ONLY 2 lines:
1. Numeric score: 0.00-1.00
2. One-sentence reason...

Der JSON-Parser erwartet aber:

{"probability": 0.85, "reason": "..."}

Das Ergebnis: cannot convert spam score im Log und GPT_UNCERTAIN(0.00) bei jeder Mail. Das GPT-Modul lief, lieferte aber nie ein verwertbares Ergebnis.

Lösung: ein Custom Prompt, der explizit JSON mit dem probability-Feld verlangt. Damit funktioniert die Kette:

  1. Rspamd sendet Mail + Prompt an OpenAI
  2. OpenAI antwortet mit {"probability": 0.9, "reason": "..."}
  3. Rspamd parst das JSON, findet probability, mappt auf GPT_SPAM/GPT_HAM/GPT_SUSPICIOUS

reason_header entfernt

In der alten Version hatte ich reason_header = "X-GPT-Reason" gesetzt. Das schrieb die GPT-Begründung als eigenen Header in die Mail. Mit json = true ist das nicht mehr nötig — die Reason steckt im JSON und taucht im Rspamd-Log auf. Außerdem entferne ich ohnehin GPT-Header per Milter-Config, damit keine internen Analyse-Details an den Empfänger durchsickern.

symbols_to_except angepasst

Änderungen gegenüber der alten Version:

  • GREYLIST entfernt: Greylisting ist kein Vertrauens-Signal. Eine Mail die Greylisting besteht, kann trotzdem Spam sein. GPT soll diese Mails weiterhin bewerten.
  • BAYES_HAM hinzugefügt: Wenn Bayes die Mail bereits sicher als Ham einstuft, spart man sich den GPT-Call. Sinnvoll für Newsletter und regelmäßige Korrespondenz.
  • SPAMTRAP-Symbole hinzugefügt: Mails an Spamtrap-Adressen brauchen keine GPT-Analyse, die sind per Definition Spam.

Scoring: Gewichte und Thresholds

Die GPT-Symbole und ihre Gewichte in der metrics.conf (bzw. local.d/groups.conf):

symbols {
  GPT_SPAM       { weight = 9.0;  description = "GPT: classified as SPAM"; }
  GPT_SUSPICIOUS { weight = 4.5;  description = "GPT: classified as SUSPICIOUS"; }
  GPT_HAM        { weight = -0.5; one_shot = true; description = "GPT: classified as HAM"; }
}

Warum diese Gewichte?

  • GPT_SPAM (9.0): Kräftig, aber alleine nicht genug zum Rejecten. Erst in Kombination mit anderen Signalen (Bayes, RBL, fehlende Auth) wird der Reject-Threshold erreicht.
  • GPT_SUSPICIOUS (4.5): Schiebt Grenzfälle in Richtung Greylist oder Add-Header. Genau dafür ist GPT am nützlichsten.
  • GPT_HAM (-0.5): Bewusst niedrig und one_shot. GPT soll Spam erkennen, nicht Ham retten.

Dazu die Action-Thresholds:

actions {
  greylist   = 4;
  add_header = 6;
  reject     = 12;
}

Reject-Threshold bei mir: 12 statt Default 15. Das geht, weil die traditionellen Checks (SPF, DKIM, DMARC, RBL, Bayes, DNSBL) bereits solide arbeiten. GPT kommt als zusätzliches Signal obendrauf.

Praxis-Beispiel

Hier eine echte Spam-Mail aus dem Log, bei der GPT korrekt angeschlagen hat:

rspamd_task_write_log: (default: T (reject): [13.83/12.00]
  [BAYES_SPAM(5.10){100.00%;},
   ABUSE_SURBL(5.00){next.schnapper-empfehlung.de:url;...},
   GPT_SPAM(2.40){0.9;},
   FROM_NEQ_ENVFROM(0.50){...},
   FORGED_SENDER(0.30){...},
   ...]

Was man hier sieht:

  • GPT_SPAM(2.40){0.9;} — GPT hat Probability 0.9 (90% Spam) zurückgeliefert. Rspamd mappt den Probability-Wert nicht 1:1 auf das konfigurierte Gewicht, sondern skaliert intern — hier ergeben sich 2.40 von maximal 9.0 Punkten.
  • Zusammen mit BAYES_SPAM (5.10) und ABUSE_SURBL (5.00) kommt die Mail auf 13.83 — deutlich über dem Reject-Threshold von 12.
  • GPT war hier nicht das ausschlaggebende Signal, hat aber zur Gesamtbewertung beigetragen.

Das ist genau das Verhalten, das ich will: GPT als ein Baustein unter vielen, der bei Grenzfällen den Ausschlag geben kann.

Datenschutz

Das muss gesagt werden: Mit diesem Setup fließen Mailinhalte an OpenAI. Wer personenbezogene Daten verarbeitet oder in einem regulierten Umfeld arbeitet, muss prüfen ob das zulässig ist. Alternative: selbst gehostete Modelle über Ollama oder kompatible lokale Endpoints. Rspamd unterstützt das über den type-Parameter.

Für meinen privaten Mailserver ist das Risiko vertretbar — und die Ergebnisse sprechen für sich.

Update 2026-05-06: Rspamd hat den Default-Prompt deutlich verbessert

Im Feedback zu diesem Beitrag kam ein guter Hinweis aus dem Fediverse von @bash2@momou.social: Vsevolod Stakhov, Maintainer von Rspamd, hat am 2. Oktober 2025 den Default-Prompt komplett überarbeitet. Commit 893ee871, Titel „Improve LLM prompt and add sender frequency tracking“. Der neue Prompt ist deutlich strukturierter, mit expliziten Sektionen für legitime Muster (Verifikations-Mails, Transaktions-Mails, Password-Resets) und für Phishing-Indikatoren, die mehrfach auftreten müssen, bevor klassifiziert wird. Das senkt False-Positives auf legitime Absender und ist eine klare Verbesserung gegenüber dem alten, knappen Default. Danke für den Hinweis!

Wichtig zur Einordnung: Der neue Default-Prompt liefert weiterhin strukturierten Plain-Text, kein JSON. Output-Format sind 2 bis 3 fest definierte Zeilen (Score, Reason, optional Kategorie). Was das für die beiden gängigen Setups bedeutet:

  • json = false: Wer ohne JSON-Modus fährt, profitiert direkt vom neuen Default-Prompt. Rspamd auf eine Version mit dem Commit aktualisieren, fertig.
  • json = true wie in diesem Beitrag: Custom Prompt mit JSON-Format bleibt Pflicht. Der neue Default ist immer noch kein JSON und kollidiert weiterhin mit dem Parser.

Zusätzlich neu im selben Commit: Sender-Frequency-Tracking. Rspamd klassifiziert in lualib/llm_context.lua jeden Absender per Redis-Counter als new, occasional, known oder frequent und gibt dem Modell die Info als Context-Snippet mit. Der Prompt weist das LLM dann an, bei known oder frequent die Phishing-Wahrscheinlichkeit zu reduzieren, sofern keine starken Gegen-Signale vorliegen. Voraussetzung ist, dass die LLM-Context-Funktion mit Redis-Backend läuft, weil das Feature über den sender_counts-Counter dort getrackt wird.

Mein Setup hier im Beitrag bleibt also unverändert: json = true plus Custom Prompt, der explizit ein probability-Feld verlangt. Wer stattdessen den neuen Default-Prompt nutzen möchte, sollte gleichzeitig auf json = false umstellen, sonst läuft man wieder in die alte Falle aus diesem Beitrag.

Zusammenfassung

ParameterWertWarum
jsontrueStrukturiertes Parsing, zuverlässiger als Freitext
promptCustomPflicht bei json=true! Default-Prompt liefert Textformat, Parser erwartet JSON
temperature0.0Deterministische Antworten, kein Kreativitäts-Bonus beim Spamfiltern
allow_hamtrueKleines positives Signal für legitime Mails
symbols_to_exceptBAYES_HAM, DNSWL, Whitelists, SMTP_AUTH, SpamtrapsUnnötige API-Calls vermeiden
reason_headernicht gesetztNicht nötig mit json=true, interne Details gehören nicht in den Header

Die wichtigste Erkenntnis: json = true ohne Custom Prompt ist kaputt. Der Default-Prompt und der JSON-Parser sprechen unterschiedliche Sprachen. Wer json = true setzt, muss einen Prompt mitliefern, der JSON mit einem probability-Feld verlangt. Sonst steht im Log cannot convert spam score und GPT liefert nur GPT_UNCERTAIN(0.00).

Siehe auch: rspamd mit Dovecot und IMAPSieve

Fragen? Einfach melden.

HTTPS RR und SVCB: Moderne DNS-Records für schnellere und sicherere Verbindungen

HTTPS RR und SVCB DNS-Records – schnellere Verbindungen mit HTTP/3, QUIC und DNSSEC

Wenn ein Browser eine HTTPS-Verbindung aufbaut, braucht er normalerweise mehrere DNS-Lookups und Round-Trips, bevor er überhaupt weiß, welche Protokolle der Server unterstützt. Erst A/AAAA-Record abfragen, dann TCP-Verbindung, dann TLS-Handshake, dann Alt-Svc-Header parsen für HTTP/3. Das ist ineffizient und seit November 2023 gibt es mit RFC 9460 eine saubere Lösung dafür: den HTTPS Resource Record.

Die großen Browser Hersteller unterstützen das ebenfalls schon, eigentlich mehr aus Eigeninteresse, denn viele Vorschläge kommen sogar direkt von ihnen. Oh, natürlich sollte die jeweilige Zone auch per DNSSec geschützt sein, denn wir wollen uns hier ja auf´s DNS verlassen können. Richtig?! Wenn ihr also noch kein DNSsec für eure Domain aktiviert habt (warum nicht?) dann bitte jetzt, wir haben bald 2026!

Ich habe das jetzt auf meiner DNS-Infrastruktur (BIND 9.20, FreeBSD, Master-Slave-Setup) für alle relevanten Dienste ausgerollt und dabei auch gleich SVCB-Records für die DNS-Server selbst gesetzt. Hier die Details.

Was ist der HTTPS RR?

Der HTTPS Resource Record (Typ 65) ist in RFC 9460 definiert („Service Binding and Parameter Specification via the DNS“, November 2023). Die Idee ist simpel: ein einziger DNS-Lookup liefert dem Client alles, was er für den Verbindungsaufbau braucht. IP-Adressen, unterstützte Protokolle wie HTTP/2 oder HTTP/3, Ports, und perspektivisch auch die ECH-Konfiguration für verschlüsselten SNI.

Ohne HTTPS RR sieht der Ablauf so aus: Der Client fragt A und AAAA ab, baut eine TCP-Verbindung auf, macht den TLS-Handshake, und erfährt erst aus dem Alt-Svc-Header oder durch ALPN im TLS, dass der Server auch HTTP/3 kann. Beim nächsten Request kann er dann QUIC probieren. Das sind mindestens zwei Verbindungsversuche, bis er auf dem optimalen Protokoll landet.

Mit HTTPS RR weiß der Client schon nach dem DNS-Lookup: „Dieser Server spricht h3 und h2, ist unter diesen IPs erreichbar, und hier ist die ECH-Config.“ Er kann direkt mit QUIC/HTTP/3 starten, ohne vorher TCP probiert zu haben.

Die SvcParams im Detail

Ein HTTPS RR besteht aus einer Priorität (SvcPriority), einem Zielnamen (TargetName) und einer Reihe von Service Parameters (SvcParams). Hier ein Überblick über alle definierten Parameter:

alpn (Application-Layer Protocol Negotiation): Signalisiert welche Protokolle der Server unterstützt. Typische Werte sind h2 (HTTP/2 über TLS), h3 (HTTP/3 über QUIC) oder dot (DNS over TLS). Der Client weiß damit vor dem Verbindungsaufbau, welche Protokolle zur Verfügung stehen.

ipv4hint / ipv6hint: IP-Adressen als Hint. Der Client kann diese nutzen, statt einen separaten A/AAAA-Lookup zu machen. Das spart einen Round-Trip. Wichtig: das sind Hints, keine autoritativen Antworten. Der Client darf und sollte trotzdem den normalen A/AAAA-Record prüfen.

ech (Encrypted Client Hello): Enthält den öffentlichen Schlüssel und die Parameter für ECH. Damit verschlüsselt der Client den SNI (Server Name Indication) im TLS-Handshake, sodass ein Beobachter auf dem Netzwerkpfad nicht sehen kann, welche Domain angefragt wird. Das ist der größte Privacy-Gewinn, den HTTPS RR bieten kann. Dazu später mehr.

port: Falls der Service auf einem nicht-Standard-Port läuft. Bei normalen Webservern auf 443 nicht nötig.

no-default-alpn: Signalisiert, dass die Standard-ALPNs (die sich aus dem Schema ergeben) nicht gelten. Wird benötigt wenn ein Server z.B. nur h3, aber nicht h2 unterstützt.

mandatory: Listet Parameter auf, die ein Client zwingend verstehen muss, um den Record nutzen zu können. Ein Client, der einen mandatory-Parameter nicht kennt, muss den ganzen Record ignorieren.

SvcPriority: Die Priorität des Records. 0 bedeutet AliasMode (Weiterleitung auf einen anderen Namen, ähnlich CNAME), Werte größer 0 sind ServiceMode. Mehrere Records mit unterschiedlichen Prioritäten ermöglichen Fallback-Ketten.

TargetName: Der Zielserver. Wenn er sich vom abgefragten Namen unterscheidet, leitet der Client die Anfrage an diesen Host weiter. Das ermöglicht Indirektion, ähnlich wie bei SRV-Records.

SVCB: Das generische Pendant

Der SVCB Resource Record (Typ 64) kommt aus demselben RFC 9460, ist aber nicht auf HTTPS beschränkt. HTTPS RR ist technisch gesehen nur eine spezialisierte Variante von SVCB für das HTTPS-Schema. SVCB kann für beliebige Protokolle genutzt werden.

Besonders interessant wird SVCB für die DNS Service Discovery nach RFC 9461 („Service Binding Mapping for DNS Servers“, ebenfalls 2023). Damit kann ein DNS-Server per DNS-Record signalisieren, dass er DoT (DNS over TLS) und DoH (DNS over HTTPS, RFC 8484) unterstützt. Der Record liegt unter dem Prefix _dns. vor dem Servernamen.

Der dohpath-Parameter aus RFC 9461 teilt dem Client direkt den URI-Pfad zum DoH-Endpoint mit, z.B. /dns-query{?dns}. Damit braucht der Client keine separate Konfiguration mehr, wo der DoH-Endpoint liegt. Zusammen mit RFC 9462 („Discovery of Designated Resolvers“, DDR) kann ein Client damit automatisch erkennen, dass sein Resolver verschlüsselte Protokolle unterstützt, und automatisch upgraden.

Was ich konkret deployt habe

Insgesamt 5 neue Records in zwei Zonen. Für www.kernel-error.de und cloud.kernel-error.com existierten bereits HTTPS RRs.

Zone kernel-error.de:

Apex HTTPS RR für kernel-error.de selbst:

dig HTTPS kernel-error.de +short
1 kernel-error.de. alpn="h3,h2" ipv4hint=148.251.30.200 ipv6hint=2a01:4f8:262:4716::443

HTTPS RR für den DoH-Endpoint dns.kernel-error.de:

dig HTTPS dns.kernel-error.de +short
1 dns.kernel-error.de. alpn="h3,h2" ipv4hint=37.120.183.220 ipv6hint=2a03:4000:38:20e::853

SVCB Records für DNS Service Discovery nach RFC 9461. Zwei Records mit unterschiedlichen Prioritäten, DoH bevorzugt vor DoT:

dig SVCB _dns.dns.kernel-error.de +short
1 dns.kernel-error.de. alpn="h2,dot" dohpath=/dns-query{?dns} port=443
2 dns.kernel-error.de. alpn="dot" port=853

Priorität 1 bietet DoH über HTTP/2 (Port 443), Priorität 2 reines DoT (Port 853). Ein DDR-fähiger Client (RFC 9462) kann damit automatisch erkennen, welche verschlüsselten DNS-Protokolle mein Resolver unterstützt.

Zone kernel-error.com:

Apex HTTPS RR für kernel-error.com (Matrix Federation und Web):

dig HTTPS kernel-error.com +short
1 kernel-error.com. alpn="h3,h2" ipv4hint=148.251.30.204 ipv6hint=2a01:4f8:262:4716::52

HTTPS RR für matrix.kernel-error.com (Synapse Reverse Proxy). Über CNAME-Auflösung deckt dieser Record auch chat.kernel-error.com und admin.kernel-error.com ab:

dig HTTPS matrix.kernel-error.com +short
1 matrix.kernel-error.com. alpn="h3,h2" ipv4hint=148.251.30.204 ipv6hint=2a01:4f8:262:4716::52

CNAME-Interaktion: Ein wichtiges Detail

Laut RFC 9460 können HTTPS RR und CNAME nicht am selben DNS-Namen koexistieren. Das hat direkte Auswirkungen auf mein Setup: chat.kernel-error.com und admin.kernel-error.com sind CNAMEs auf matrix.kernel-error.com. Ein separater HTTPS RR für diese Namen ist also nicht möglich und auch nicht nötig. Der Client folgt dem CNAME und nutzt dann den HTTPS RR des Ziels.

Gleiches gilt für signaling.kernel-error.com, das ein CNAME auf rtc.kernel-error.com ist.

Was bewusst nicht umgesetzt wurde

ECH (Encrypted Client Hello): Wäre der größte Privacy-Gewinn. ECH verschlüsselt den SNI im TLS-Handshake, sodass ein Beobachter nicht sehen kann, welche Domain der Client anfragt. OpenSSL 3.5 hat die API dafür, aber nginx nutzt sie nicht. Selbst in Version 1.29.7 gibt es keine native ECH-Unterstützung. Dafür bräuchte es entweder Patches für nginx oder einen anderen Reverse Proxy. Sobald sich das ändert, kommt der ech-Parameter in die HTTPS RRs.

DoQ (DNS over QUIC, RFC 9250): DoQ ist ein eigenes Protokoll, das DNS direkt über QUIC transportiert, ohne HTTP-Overhead. Das ist nicht dasselbe wie DoH über HTTP/3! BIND 9.20 unterstützt kein DoQ. Dafür müsste man ein separates Frontend wie dnsproxy oder AdGuard DNS davor setzen.

SVCB für SMTP/IMAP: Es gibt IETF-Drafts, die SVCB auf Mail-Protokolle ausweiten wollen (SMTP Submission, IMAPS). Da diese aber noch kein finaler RFC sind und aktuell kein MTA oder Client sie auswertet, habe ich darauf verzichtet. Die bestehenden SRV-Records (_imaps._tcp, _submission._tcp, _submissions._tcp) sind heute das Richtige.

HTTPS RR für turn.kernel-error.com: Der primäre Zweck ist TURN/STUN, nicht Web. Clients bekommen den Server aus der Synapse-Konfiguration, ein HTTPS RR bringt hier keinen Vorteil.

HTTPS RR für rtc.kernel-error.com: Kein HTTP/3 auf diesem Server, da der nginx dort ohne h3-Modul läuft. Ein HTTPS RR mit nur alpn="h2" würde kaum Mehrwert bringen.

Deployment in DNSSEC-signierten Zonen

Beide Zonen sind mit DNSSEC signiert (ECDSAP256SHA256, inline-signing). Der Workflow für Änderungen an signierten Zonen ist immer derselbe:

rndc freeze kernel-error.de
# Zonendatei editieren, Serial hochzählen
named-checkzone kernel-error.de /path/to/zone/file
rndc thaw kernel-error.de

Nach dem thaw signiert BIND die neuen Records automatisch und der Slave (ns1) übernimmt die Änderungen sofort per NOTIFY und AXFR. BIND 9.20 unterstützt HTTPS und SVCB Records nativ, es ist also kein TYPE65-Workaround mit generischer Record-Syntax nötig.

Records prüfen

Wer sich die Records anschauen will:

dig HTTPS kernel-error.de +short
dig HTTPS dns.kernel-error.de +short
dig SVCB _dns.dns.kernel-error.de +short
dig HTTPS kernel-error.com +short
dig HTTPS matrix.kernel-error.com +short

Ausblick

Die offensichtlichste Lücke ist ECH. Sobald nginx native Unterstützung bekommt, wird der ech-Parameter in alle HTTPS RRs eingetragen. Das wäre dann echte SNI-Verschlüsselung für alle Dienste.

SVCB für SMTP und IMAP wäre der nächste logische Schritt, sobald die aktuellen IETF-Drafts zu finalen RFCs werden und MTAs/Clients anfangen, sie auszuwerten. Immer mal wieder setzte ich auch IETF-Drafts in meinem Setup oder Labor Setup um. In diesem speziellen Fall sehe ich darin aber keinen Nutzen. Aus irgendeinem Grund schaffen es solche IT Security Themen bei E-Mails nur sehr selten in eine „schnelle“ Umsetzung. Die Browserhersteller machen da bei HTTPS wohl genug selbst. Viele Ideen kommen ja sogar von diesen.

Und DoQ (RFC 9250) steht auf der Liste, sobald BIND oder ein brauchbarer Proxy es unterstützt. Dann würden die SVCB-Records um alpn="doq" ergänzt. Ich möchte nicht wieder etwas vor meinen DNS stellen. Das wird aber bereits von den großen Browsern unterstützt!

Siehe auch:

Bei Fragen oder Anmerkungen, einfach fragen.

Kernel-Error jetzt auch im Tor-Netz: meine .onion-Adresse

Kurzfassung: www.kernel-error.de ist zusätzlich als Tor Hidden Service erreichbar.
Meine .onion-Adresse: jjyvff6eh3kp7ydfkamm27cldhsee2cl6wzfa5lfjyrfyribgeaesgqd.onion

Illustration: Kernel-Error Blog als HTTPS-Website und Tor Hidden Service (.onion) mit Fokus auf Privatsphäre, ohne Exit-Node und ohne DNS-Leaks

Der ursprüngliche Beitrag war ziemlich kurz gehalten. Seither werde ich immer wieder gefragt, wie das eigentlich zusammenspielt und was man bei so einem Setup alles bedenken muss. Also habe ich ihn deutlich erweitert und versuche, den Aufbau von der Idee bis zum fertigen vHost nachvollziehbar zu machen – inklusive der Stolperfallen, die ich unterwegs eingesammelt habe.

Warum überhaupt eine Onion-Variante?

Die Seite läuft ohnehin unter HTTPS mit aktuellen Cipher-Suites, Post-Quantum Key Exchange und HSTS. Technisch gesehen passiert zwischen Besucher und Server also schon heute nichts Unverschlüsseltes. Trotzdem gibt es ein paar Gründe, die für einen zusätzlichen Hidden Service sprechen:

  • Metadaten-Minimierung: Beim Clearnet-Abruf sieht der Provider mindestens, dass jemand mit meiner IP spricht. DNS-Resolver, Transit-Router und der Exit-Punkt kennen das Ziel. Bei einem Hidden Service ist außerhalb des Tor-Netzwerks gar nichts mehr zu sehen – weder IP noch DNS-Namen.
  • Kein Exit-Node: Wer meine Seite über einen normalen Tor-Browser mit Clearnet-URL besucht, spricht am Ende über einen Exit-Node mit meinem Server. Der Exit sieht zwar nur TLS, kann aber Metadaten wie SNI oder Zielhost mitbekommen und je nach Land auch gesperrt werden. Mit einer Onion-Adresse fällt der Exit-Node komplett weg.
  • Keine CA-Abhängigkeit: Die .onion-Adresse ist der Public-Key-Hash selbst. Wer die korrekte Adresse kennt, spricht kryptografisch nachweisbar mit meinem Server – ganz ohne Zertifikatsausstellerin und ohne OCSP-Kaskade.
  • Zensurresistenz: Sollte der Clearnet-Zugang aus irgendeinem Grund blockiert sein – falsche DNS-Antworten, gesperrte IP, generelle Sperre der Domain – bleibt die Onion-Variante unabhängig davon erreichbar.
  • Spielerei und Lerneffekt: Ich finde das Konzept hinter v3-Onions schlicht spannend. Ed25519-Keys, Rendezvous-Protokoll, Introduction Points – da steckt eine Menge gut durchdachte Krypto drin, die man am eigenen Dienst deutlich besser versteht als aus Diagrammen.

Wie das Ganze aufgebaut ist

Der Blog läuft in einer FreeBSD-Jail mit Nginx, PHP-FPM und einer eigenen Tor-Instanz. Für den Hidden Service sind drei Bausteine relevant:

  • Der Tor-Daemon hält das Schlüsselmaterial und den Rendezvous-Teil. Er lauscht nicht auf einer öffentlichen IP, sondern hängt sich in das Tor-Netzwerk und meldet dort den Dienst an.
  • Nginx stellt einen eigenen vHost bereit, der ausschließlich auf einer Loopback-Adresse (127.0.0.6:80) lauscht. Öffentlich erreichbar ist dieser vHost nie – er wird ausschließlich vom lokalen Tor-Prozess angesprochen.
  • PHP-FPM liefert wie gewohnt über einen Unix-Socket die WordPress-Inhalte aus – allerdings ohne FastCGI-Cache für den Onion-Pfad. Dazu gleich mehr.

Der Vorteil dieser Trennung: Alles, was der Tor-Daemon an den Nginx weiterreicht, kommt garantiert aus dem Tor-Netzwerk. Und alles, was der Clearnet-vHost ausliefert, geht garantiert nicht über das Onion-Interface. Zwei saubere Welten, gleiche Codebasis, null Überschneidung im Cache.

Tor-Daemon: torrc

Die minimale Konfiguration für einen v3-Hidden-Service ist überraschend kurz:

HiddenServiceDir      /var/db/tor/hidden_service/
HiddenServiceVersion  3
HiddenServicePort     80 127.0.0.6:80
SocksPort             127.0.0.6:9050
Log                   notice file /var/log/tor/notices.log

Beim ersten Start legt Tor in HiddenServiceDir das komplette Schlüsselmaterial selbst an: den privaten ed25519-Schlüssel, die daraus abgeleitete hostname-Datei mit der .onion-Adresse und ein paar weitere Dateien für die Introduction-Points. Diese Dateien sind privilegiert – wer sie hat, kann den Dienst übernehmen und sich gegenüber der Welt als mein Server ausgeben. Entsprechend gehören sie dem _tor-User, haben mode 700 und sind im Backup separat behandelt.

Die Adresse selbst ist 56 Zeichen lang und wird aus dem ed25519-Public-Key abgeleitet. Aenderbar ist sie nicht – wer eine bestimmte Wunschadresse will, muss mit Tools wie mkp224o so lange Schlüssel durchprobieren, bis der Base32-Prefix passt. Hat mich nicht gereizt, dafür ist meine Adresse eben zufällig.

Nginx-vHost für den Onion-Service

Der eigentliche vHost ist bewusst klein gehalten. Kein Cache, kein TLS, keine QUIC-Spielereien – alles, was spezifisch für den Clearnet-Teil ist, fehlt hier:

server {
    listen 127.0.0.6:80;
    server_name jjyvff6eh3kp7ydfkamm27cldhsee2cl6wzfa5lfjyrfyribgeaesgqd.onion;

    root  /usr/local/www/www.kernel-error.de;
    index index.php index.html index.htm;

    # access/error-logs per Default aus (keine Besucher-Spuren im Dateisystem).
    # Nur bei Bedarf zu Debugging-Zwecken aktivieren und später wieder aus.
    #access_log /var/log/nginx/www-kernel-error-de-access_tor.log combined;
    #error_log  /var/log/nginx/www-kernel-error-de-error_tor.log;

    add_header X-Robots-Tag      "noarchive, noimageindex" always;
    add_header Permissions-Policy "interest-cohort=()"    always;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~* \.(css|gif|ico|jpeg|jpg|js|png|svg|webp|woff2?)$ {
        access_log off;
        expires 30d;
        add_header Cache-Control "public";
        try_files $uri =404;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php-fpm.sock;
        fastcgi_index index.php;

        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO       $fastcgi_path_info;

        # Cache hart deaktivieren - kein gemeinsamer Pool mit dem Clearnet-vHost
        fastcgi_no_cache      1;
        fastcgi_cache_bypass  1;
    }
}

Ein paar Punkte sind bewusst so gewählt:

  • listen 127.0.0.6:80: Die Adresse ist frei wählbar innerhalb von 127.0.0.0/8, muss aber mit dem HiddenServicePort in der torrc übereinstimmen. Ich nehme bewusst nicht 127.0.0.1, damit der Onion-Listener klar vom Standard-Loopback zu unterscheiden ist – hilft beim Debugging und bei sockstat.
  • Kein TLS: Zwischen Tor-Daemon und Nginx liegt nur die Loopback-Schnittstelle, dafür braucht es kein Zertifikat. Und .onion-Zertifikate gibt es nur über kommerzielle CAs (Extended Validation), Let’s Encrypt stellt keine aus. Der eigentliche Schutz auf dem Draht kommt komplett aus dem Tor-Protokoll – End-to-End verschlüsselt vom Tor-Client bis hier zur Loopback-Adresse.
  • Getrennte Logs (per Default aus): Im Normalbetrieb sind access_log und error_log im Onion-vHost auskommentiert – so entstehen erst gar keine Dateien mit IP-, User-Agent- oder Pfad-Informationen der Onion-Besucher. Zum Debugging lassen sich die beiden Zeilen kurz einkommentieren, danach bewusst wieder aus. Wichtig ist auf jeden Fall, dass Clearnet- und Onion-Zugriffe nie im gleichen Logfile landen – über Zeitstempel und User-Agent-Muster ließen sich sonst leicht Korrelationen bilden.
  • X-Robots-Tag: noarchive, noimageindex verhindert, dass Suchmaschinen die Onion-Variante cachen oder Bilder indexieren. noindex setze ich bewusst nicht – wer die Onion-Adresse kennt, darf sie auch in Verzeichnissen auftauchen lassen.
  • Permissions-Policy: interest-cohort=() schaltet Googles FLoC-/Topics-API explizit aus. Für einen Privacy-Blog im Tor-Netz ist das eher symbolisch, schadet aber nicht.

Der Knackpunkt: Cache-Isolation

Der Clearnet-vHost nutzt einen FastCGI-Cache (fastcgi_cache_path), damit WordPress nicht jede Seitenauslieferung neu rendern muss. Das ist für einen Blog mit statischen Inhalten ein riesiger Performance-Boost. Genau dieser Cache ist aber auch der Punkt, an dem man sich beim Onion-Betrieb ins Knie schießen kann.

Wenn der Onion-vHost denselben Cache-Pool verwenden würde, könnten zwei unerwünschte Effekte auftreten:

  • Cross-Origin-Leakage im HTML: WordPress baut absolute Links anhand der siteurl/home-Option. Die steht auf https://www.kernel-error.de. Wenn eine Clearnet-Anfrage cached, landen in jedem Link Clearnet-URLs im Cache-Eintrag. Liefert der Onion-vHost dann dieselbe Seite aus dem Cache, sieht der Tor-Besucher eine Seite voller Clearnet-Links – und jeder Klick würde ihn aus dem Hidden Service hinauskatapultieren.
  • Fingerprinting über Cache-Keys: Abhängig davon, wer die Seite zuerst aufgerufen hat, liefert der Cache deterministisch „warme“ oder „kalte“ Antworten. Ein Angreifer, der beide Varianten beobachtet, kann Rückschlüsse ziehen. Klein, aber unnötig.

Die Lösung ist bewusst stumpf: Der Onion-vHost bekommt gar keinen fastcgi_cache_path-Verweis. Zusätzlich stehen im location ~ \.php$-Block die beiden Schalter fastcgi_no_cache 1 und fastcgi_cache_bypass 1, damit selbst bei versehentlich geerbter Konfiguration weder gelesen noch geschrieben wird. Jeder Request rendert frisch durch den FPM.

Bei der Besucher-Zahl über den Hidden Service ist das unkritisch. Die Clearnet-Seite bleibt gleichzeitig durch ihren eigenen Cache schnell – beide Welten profitieren von ihrer jeweils passenden Strategie.

Der Onion-Location-Header und die Meldung im Tor Browser

Damit Besucher überhaupt erfahren, dass es eine Onion-Variante gibt, setzt der Clearnet-vHost zwei zusätzliche Header:

add_header Onion-Location "http://jjyvff6eh3kp7ydfkamm27cldhsee2cl6wzfa5lfjyrfyribgeaesgqd.onion$request_uri" always;
add_header Alt-Svc        'http="jjyvff6eh3kp7ydfkamm27cldhsee2cl6wzfa5lfjyrfyribgeaesgqd.onion:80"; ma=86400' always;

Der Onion-Location-Header ist ein vom Tor-Project definiertes Signal. Ruft man den Blog im Tor Browser (Version 9.5 oder neuer) über die Clearnet-Adresse auf, blendet der Browser eine unauffällige „.onion available“-Schaltfläche in der URL-Leiste ein und bietet an, auf die Hidden-Service-Variante umzuschalten. Das ist die „Meldung“, die einige irritiert: Sie kommt nicht von meiner Seite, sondern direkt vom Tor Browser, der den Header interpretiert.

Wichtig dabei: Der Tor Browser akzeptiert den Header nur, wenn ein paar Bedingungen erfüllt sind. Das verhindert, dass ein kompromittierter Server Besucher auf fremde Hidden Services umlenken kann:

  • Die Ursprungsseite muss per HTTPS ausgeliefert worden sein.
  • Die im Header angegebene .onion-Adresse muss wohlgeformt sein (v3, 56 Zeichen, Base32).
  • Die Nutzerin muss die Umleitung aktiv bestätigen – es gibt keine automatische Weiterleitung.

Der Alt-Svc-Header (RFC 7838) ist die generische Variante. Einige alternative Clients nutzen ihn, um Alternative-Services im Hintergrund vorzumerken. Für den Tor Browser ist primär der Onion-Location-Header relevant, Alt-Svc ist eher Absicherung.

DNS als zusätzliche Verifikation

Ein Header auf der Seite ist eine gute Sache – wer dem Server vertraut, bekommt den Hinweis auf den Onion-Service. Was aber, wenn jemand meine Angabe kontrollieren will, ohne die Seite selbst aufgerufen zu haben?

Dafür habe ich einen einfachen TXT-Record im DNS hinterlegt:

dig +short TXT www.kernel-error.de
"onion=jjyvff6eh3kp7ydfkamm27cldhsee2cl6wzfa5lfjyrfyribgeaesgqd.onion"

Das Format onion=… ist kein offizieller Standard, sondern meine persönliche Konvention. Es gibt einen Internet-Draft von Alec Muffett (draft-muffett-same-origin-onion-v2), der einen ähnlichen Ansatz beschreibt, aber als RFC nie durchgegangen ist. Bis sich etwas Standardisiertes etabliert, bleibe ich bei meinem einfachen Ansatz: Wer wissen will, ob die Onion-Adresse wirklich zu mir gehört, vergleicht den Header, den TXT-Record und meine öffentlichen Ankündigungen. Die Zone selbst ist DNSSEC-signiert, der TXT-Record ist also nicht spürbar manipulierbar, solange der Resolver DNSSEC validiert.

Wem das zu wacklig ist: Für .onion gibt es auch kommerzielle Zertifikate (DigiCert stellt EV-Zertifikate für Onion-Services aus). Damit könnte man die Onion-Seite zusätzlich hinter HTTPS legen und in der Tor-Browser-URL-Leiste wäre die Organisation sichtbar. Für einen privaten Blog ist der Aufwand und die Kosten aber deutlich höher als der Nutzen.

Warum kein TLS-Zertifikat nötig ist

Der Punkt irritiert am Anfang fast jeden: Eine Webseite ohne HTTPS, im Jahr 2026, fühlt sich einfach falsch an. Wenn man sich aber anschaut, wofür TLS im Clearnet eigentlich da ist, wird schnell klar, warum es bei einem Tor Hidden Service tatsächlich überflüssig ist.

TLS erfüllt zwei Aufgaben gleichzeitig:

  • Vertraulichkeit auf der Leitung: Niemand zwischen Client und Server soll mitlesen können – weder der ISP, noch das WLAN im Cafe, noch ein Transit-Provider.
  • Identitätsnachweis des Servers: Der Client muss sicher sein können, dass er tatsächlich mit dem Server spricht, der hinter dem Domainnamen steht. Diese Aufgabe übernimmt die CA-Kette, die das Zertifikat an den DNS-Namen bindet.

Bei einem Tor Hidden Service sind beide Aufgaben bereits in das Protokoll selbst eingebaut – ohne dass irgendwo ein Zertifikat im Spiel wäre:

  • Vertraulichkeit kommt aus dem Tor-Tunnel: Der Datenstrom zwischen Client und Hidden Service lauft durch mehrere übereinander gelegte Verschlüsselungsschichten. Jedes Relay kann nur die eigene Schicht entfernen und sieht dadurch nur das nächste Ziel, nicht den Inhalt und auch nicht den gesamten Pfad. Auf dem Draht ist der Traffic zu keinem Zeitpunkt im Klartext zu sehen – ein zusätzliches TLS würde dieselbe Eigenschaft nur ein zweites Mal liefern.
  • Authentizität steckt in der Adresse: Die 56 Zeichen einer v3-Onion sind keine willkürliche Zeichenfolge, sondern die Base32-Darstellung eines ed25519-Public-Keys (plus Checksumme und Versionsbyte). Beim Verbindungsaufbau fordert der Tor-Client vom Server eine Signatur an und prüft sie gegen genau diesen Public-Key. Passt sie nicht, kommt keine Verbindung zustande. Damit übernimmt die Adresse selbst die Rolle, die im Clearnet das Zertifikat spielt – nur dass hier keine CA existieren muss, die das beglaubigt.

Anders formuliert: Der Hidden Service ist von der ersten Byte an selbst-authentisierend. Die Sicherheitsgarantie steht und fällt mit der korrekten Onion-Adresse, nicht mit einem Dritten, der sie beglaubigen müsste. Deshalb hat Let’s Encrypt für .onion-Adressen bewusst nie Zertifikate ausgestellt – es gibt schlicht nichts, was eine CA hier noch verifizieren könnte, was nicht schon durch die Adresse selbst belegt wäre.

Den einzigen verbleibenden Nutzen eines Zertifikats – das sichtbare Organisationskürzel in der Tor-Browser-URL-Leiste bei EV-Zertifikaten – kann man sich für einen privaten Blog guten Gewissens sparen.

Firewall und Systemhärtung

Die schöne Eigenschaft eines Hidden Service: Es muss kein zusätzlicher Port nach außen geöffnet werden. Der Tor-Prozess baut nur ausgehende Verbindungen auf, der Nginx-Onion-vHost lauscht nur auf der Loopback-Adresse. Aus Sicht einer Firewall ändert sich durch den Hidden Service gar nichts.

Trotzdem gibt es ein paar Punkte, die ich bewusst setze:

  • Jail-Isolation: Tor und Nginx laufen in derselben FreeBSD-Jail. Die Jail hat nur die öffentliche IP für den Clearnet-Nginx und separate Loopback-Adressen für interne Dienste. Dadurch kann der Tor-Prozess nicht „aus Versehen“ auf andere Dienste zugreifen – und er teilt sich seinen Kernel-Namespace nur mit dem, was ich bewusst in dieselbe Jail gelegt habe.
  • Dateirechte: /var/db/tor/hidden_service/ gehört dem _tor-User, mode 700. Das private Schlüsselmaterial muss genauso behandelt werden wie ein TLS-Private-Key – wer es hat, kann meine .onion übernehmen.
  • WordPress-Härtung: Der Login (wp-admin, wp-login.php) wird über das Plugin WPS Hide Login auf eine nicht-offensichtliche URL gelegt und ist ohnehin nur via Clearnet mit HTTPS erreichbar. Im Onion-vHost wird die Login-URL weder verlinkt noch erwähnt. Administration findet ausschließlich über die Clearnet-Variante statt.
  • Keine Log-Korrelation: Die getrennten Access-Logs habe ich schon erwähnt. Zusätzlich analysiere ich sie separat und führe keine IPs aus dem Clearnet-Log mit dem Onion-Log zusammen. Das wäre technisch möglich, würde aber den Sinn des Setups konterkarieren.
  • Keine externen Ressourcen: Das Theme und die eingebundenen Plugins laden keine Fonts von Google, keine Scripts von CDNs, keine Gravatare aus der Welt. Wenn ein Element doch mal nach www.kernel-error.de auflaufen sollte (etwa durch ein falsch konfiguriertes Plugin), würde der Tor Browser das als Mixed-Origin-Request anzeigen und viele Nutzerinnen würden es nicht mehr laden – das fällt dann schnell auf und ich fixe es.

Warum das als „sicher“ gilt

„Sicher“ ist immer relativ – gegenüber welchem Angreifer, unter welchem Modell? Ein paar Eigenschaften kann man aber konkret benennen:

  • End-to-End verschlüsselt: Tor baut zwischen Client und Hidden Service einen dreistufigen Tunnel durch Rendezvous- und Introduction-Points. Keiner der Zwischenknoten sieht, wer mit wem spricht oder was übertragen wird. Auf dem Draht sieht außerdem niemand eine .onion-Adresse – die Rendezvous-Logik wählt den Zielknoten indirekt.
  • Kryptografische Authentizität der Adresse: Die 56 Zeichen der v3-Onion sind der Base32-codierte ed25519-Public-Key samt Checksumme und Versionsbyte. Wer die richtige Adresse getippt oder kopiert hat, landet kryptografisch garantiert auf dem Server, der den zugehörigen privaten Schlüssel hat. Keine CA, kein DNS, kein Zwischenhändler kann das verändern, ohne dass die Adresse anders aussieht.
  • Kein TLS nötig: Verschlüsselung kommt aus dem Tor-Tunnel, Authentizität aus der Adresse selbst. Details dazu stehen weiter oben im eigenen Abschnitt – kurz: TLS würde die Eigenschaften nicht ergänzen, sondern nur doppeln.
  • Anonymität der Besucher: Der Server sieht keine echte Client-IP, nur einen Rendezvous-Punkt im Tor-Netz. Zensur und Traffic-Analyse auf dem letzten Stück entfallen vollständig.
  • Vertraulichkeit für mich: Der Server ist nicht öffentlich erreichbar, es gibt keinen offenen Port, den man mit nmap finden könnte. Die öffentliche IP des Servers ist durch die Tor-Rendezvous-Logik nicht aus dem Netzwerkverkehr ableitbar, solange ich nicht durch Fehlkonfiguration (etwa hart verlinkte Clearnet-URLs im HTML) Hinweise leake.

Stolperfallen und was man besser lässt

  • WordPress-URLs: siteurl und home bleiben auf der Clearnet-URL. Einige Anleitungen empfehlen, bei Aufruf der Onion-Variante die URL dynamisch umzuschreiben. Das habe ich bewusst nicht gemacht – es führt zu kaputten Canonical-Links, mischt Content- und Admin-Bereich und bricht meist den Blöck-Editor. Stattdessen akzeptiere ich, dass im Onion-HTML auch mal eine Clearnet-Link vorkommt (etwa im Footer), und setze darauf, dass der Tor Browser korrekt warnt, bevor er dort hinschickt.
  • Externe Embeds: YouTube-Videos, Twitter-Embeds, Gravatare – alles Dinge, die eine Onion-Seite sofort deanonymisieren könnten, wenn sie geladen würden. Das Theme und die Plugins auf diesem Blog laden bewusst keine externen Ressourcen.
  • Redis/Object-Cache: Der Object-Cache speichert keine gerenderten HTML-Seiten, sondern nur WP-interne Objekte. Hier ist die Vermischung unkritisch, weil die resultierenden URLs erst beim Rendern (durch den jeweiligen vHost) entstehen.
  • FastCGI-Cache: Wie oben beschrieben – für die Onion-Variante komplett deaktiviert. Wer es trotzdem aktivieren will, braucht zwingend einen eigenen fastcgi_cache_path, eigenes Key-Schema und muss den Host-Teil im Cache-Key haben.
  • Monitoring: Klassische Uptime-Checks von außen funktionieren auf .onion-Adressen nicht ohne weiteres. Dienste wie Uptime-Kuma können inzwischen Tor-Proxy nutzen, ansonsten hilft ein kleiner curl --socks5-hostname-Check.

Was man noch härter machen kann

Für meinen privaten Blog halte ich das beschriebene Setup für ein solides Minimum. Wer seinen Hidden Service strenger absichern will – etwa weil dort Whistleblower-Material, Redaktions-Inhalte oder andere wirklich schutzwürdige Dinge liegen – sollte ein paar zusätzliche Punkte beachten:

  • Zusätzliche HTTP-Header: Referrer-Policy: no-referrer verhindert, dass beim Klick auf einen externen Link die .onion-URL als Referer mitgeschickt wird. Content-Security-Policy (restriktiv, z. B. default-src 'self'; img-src 'self' data:;) blockt Mixed-Origin-Requests hart, bevor der Browser sie überhaupt versucht. Dazu X-Content-Type-Options: nosniff und X-Frame-Options: DENY, damit Clickjacking und MIME-Sniffing ausgeschlossen sind.
  • server_tokens off: Im Haupt-vHost ist der Nginx-Version-String schon länger abgeschaltet und durch einen eigenen Custom-Header ersetzt. Im Onion-vHost gehört server_tokens off; genauso rein – ohne das steht die Nginx-Version in jeder 404-Seite und erleichtert Fingerprinting.
  • Tor-Daemon härten: Sandbox 1 in der torrc aktiviert unter FreeBSD Capsicum und unter Linux Seccomp-Filter. Damit bekommt der Tor-Prozess nur die Syscalls, die er wirklich braucht. Zusätzlich lässt sich mit HiddenServiceEnableIntroDoSDefense 1 sowie HiddenServiceEnableIntroDoSRatePerSec und HiddenServiceEnableIntroDoSBurstPerSec ein integrierter DoS-Schutz am Introduction-Point aktivieren – seit Tor 0.4.2 gibt es das als Plugin-freie Bordmittel.
  • Client Authorization: Für wirklich nicht-öffentliche Dienste kennt Tor einen Mechanismus, bei dem nur Clients mit passendem x25519-Key-Paar den Hidden Service überhaupt erreichen. Die Adresse ist dann zwar im Tor-Netz bekannt, ohne die Private-Key-Datei auf dem Client kommt man aber keinen Zentimeter weit. Für einen öffentlichen Blog unpassend, für ein Journalisten-Dropbox-Setup die wichtigste Absicherung überhaupt.
  • Offline-Backup der Hidden-Service-Keys: Wenn der Inhalt von /var/db/tor/hidden_service/ verloren geht, ist die .onion-Adresse weg – es gibt keinen Weg, sie wiederherzustellen. Das private Schlüsselmaterial gehört deshalb auf einen verschlüsselten Offline-Datenträger und sollte genauso behandelt werden wie ein TLS-Root-Key. Wer die Adresse überträgt, überträgt gleichzeitig die Fähigkeit, sie zu betreiben – das ist nichts, was in einem Cloud-Backup liegen sollte.
  • Jail-/Container-Trennung von Tor und Web: Aktuell laufen Tor-Daemon und Nginx in derselben FreeBSD-Jail, weil sie über die Loopback-Adresse sowieso miteinander sprechen müssen. Wer paranoider sein will, packt den Tor-Prozess in eine eigene Jail mit eigener Loopback-IP und forwardet nur den HiddenService-Port – dann kann ein kompromittierter WordPress-Prozess nicht mal versehentlich an die Schlüsseldateien. Für mich persönlich ist der Aufwand zurzeit nicht gerechtfertigt, für einen Hochrisiko-Service aber eine ernsthafte Option.
  • Ehrliche Einschätzung zur Anonymität des Betreibers: Wer einen Hidden Service betreibt, um die eigene Identität zu schützen, muss auch alles drumherum sauber haben – Domain-Whois, Zertifikats-SANs auf dem Clearnet-Host, Uptime-Monitoring, Backups, Zeitstempel in Git-Commits, selbst die Systemzeit auf dem Server. Die .onion-Adresse alleine macht den Betreiber nicht anonym. Sie ist ein Baustein, kein Gesamtkonzept.

Für diesen Blog ist das bewusst nicht alles umgesetzt. Ich veröffentliche unter Klarnamen und möchte nur die Daten meiner Besucher besser schützen. Für jemanden, der aus guten Gründen wirklich anonym bleiben muss, sind die Punkte oben Pflicht, nicht Kür.

Fazit

Ein Tor Hidden Service für einen bestehenden WordPress-Blog ist überraschend geradlinig aufzusetzen. Die technische Umsetzung umfasst im Kern einen Tor-Daemon mit v3-Konfiguration, einen getrennten Nginx-vHost ohne HTTPS und ohne FastCGI-Cache, sowie zwei Header im Clearnet-vHost. Der Aufwand bleibt überschaubar, der Gewinn an Erreichbarkeit und Metadaten-Minimierung ist messbar.

Wer die Adresse im Tor Browser aufruft, bekommt exakt den gleichen Blog zu sehen – nur ohne TLS-Handshake, ohne Exit-Node und ohne Spur im eigenen DNS-Resolver. Die Seite wird nicht häufig aus dem Tor-Netz aufgerufen, aber sie ist für die Fälle da, in denen sie gebraucht wird. Und ehrlich gesagt: Es macht auch einfach Spaß, ein System so zu bauen, dass beide Welten sauber nebeneinander existieren.

Siehe auch: HTTP/3 und QUIC, Post-Quantum TLS für Nginx, TLS-ECDHE einfach erklärt

Fragen? Einfach melden.

WordPress wp-cron.php: Ist die angebliche Sicherheitslücke real?

Picture of an hacker checken for wordpress vulnerability

In letzter Zeit begegnen mir immer wieder sogenannte „Vulnerability Report Scams“. Klar, mit Angst und Unwissenheit kann man Geld verdienen, also wird es auch jemand tun. Besonders fällt mir das im Zusammenhang mit der wp-cron.php auf.

Ich habe häufig Reports gesehen, die in etwa so aussehen:

Critical Vulnerability Report- {Critical BUG #P1} - https://www.example.com/ - vulnerable to attack via wp-cron.php

Hello  Security team,

I am a Security Engineer, Cyber Security Researcher, Bug Bounty Hunter  & Ethical Hacker. While testing your domain https://www.example.com/ I have found some important vulnerabilities in your site.

Vulnerability Name:   https://www.example.com/  -  vulnerable to DoS attack via wp-cron.php

Vulnerable Domain:  https://www.example.com/wp-cron.php

Description:

The WordPress application is vulnerable to a Denial of Service (DoS) attack via the wp-cron.php script. This script is used by WordPress to perform scheduled tasks, such as publishing scheduled posts, checking for updates, and running plugins.
An attacker can exploit this vulnerability by sending a large number of requests to the wp-cron.php script, causing it to consume excessive resources and overload the server. This can lead to the application becoming unresponsive or crashing, potentially causing data loss and downtime.

I found this vulnerability at https://www.example.com/wp-cron.php endpoint.

Steps to Reproduce: reference- https://hackerone.com/reports/1888723

navigate to: https://www.example.com/wp-cron.php
intercept the request through the burp suite
right click on the request and send it to the repeater
Now send a request, and you will see the response as  200 OK

---

this can be also done by the curl command given below

curl -I "https://www.example.com/wp-cron.php"

POC: Attached

Impact:

If successful, this misconfigured wp-cron.php file can cause lots of damage to the site, such as:

Potential Denial of Service (DoS) attacks, resulting in unavailability of the application.
Server overload and increased resource usage, leading to slow response times or application crashes.
Potential data loss and downtime of the site.
Hackers can exploit the misconfiguration to execute malicious tasks, leading to security breaches.

Exploitation:
Exploitation can be done through a GitHub tool called doser.go https://github.com/Quitten/doser.go
I did not do that as this can impact your website.
Get the doser.py script at https://github.com/Quitten/doser.py
Use this command to run the script: python3 doser.py -t 999 -g 'https://www.example.com/wp-cron.php'
Go after https://www.example.com/ 1000 requests of the doser.py script.
The site returns code 502.

Suggested Mitigation/Remediation Actions:

To mitigate this vulnerability, it is recommended to disable the default WordPress wp-cron.php script and set up a server-side cron job instead. Here are the steps to disable the default wp-cron.php script and set up a server-side cron job:
Access your website's root directory via FTP or cPanel File Manager.
Locate the wp-config.php file and open it for editing.
Add the following line of code to the file, just before the line that says "That's all, stop editing! Happy publishing.":

1define('DISABLE_WP_CRON', true);

Save the changes to the wp-config.php file.
Set up a server-side cron job to run the wp-cron.php script at the desired interval. This can be done using the server's control panel or by editing the server's crontab file.
References:

For more information about this vulnerability, please refer to the following resources:

https://hackerone.com/reports/1888723

https://medium.com/@mayank_prajapati/what-is-wp-cron-php-0dd4c31b0fee

Cron
Fix Them ----- I have protected your company and saved it from a big loss so give me some appreciation Bounty Reward. I am sharing my PayPal ID with you. Paypal ID: woop woop Current Market Value Minimum Bounty Reward for Critical BUG P1 Type. The bug I reported is part of type P1 Vulnerability severity Bug bounty reward amount (in USD) P1 (Critical) $2500 P2 (High) $1500 P3 (Medium) $1000 P4 (Low) $500 Please feel free to let me know if you have any other questions or need further information. I am happy to secure it. I hope this will be fixed soon. Feel free to let me know if you have any other questions. Thanks & Regards

Ist das nun ein echtes Problem oder nicht?

Ja und Nein. In der Nachricht wird korrekt beschrieben, was die wp-cron.php tut und warum sie wichtig ist. Auch die Tatsache, dass sie extern unendlich oft aufgerufen werden kann und dadurch potenziell eine Überlastung auslösen könnte, ist nicht falsch. Selbst der Tipp, auf eine lokale Crontab-Version umzusteigen, ist nicht verkehrt. Allerdings muss man das Ganze in den richtigen Kontext setzen: wp-cron.php ist standardmäßig in WordPress aktiviert und wird für geplante Aufgaben genutzt. Die geplanten Aufgaben werden in der Datenbank abgelegt. Gibt es etwas zu tun und die wp-cron.php wird aufgerufen, dann wird auch gearbeitet. Gibt es nichts zu tun, dann gibt es auch keine Arbeit. Die Empfehlung, sie zu deaktivieren und durch einen serverseitigen Cron-Job zu ersetzen, ist eher eine Performance-Optimierung als eine echte Sicherheitsmaßnahme.

Es handelt sich nicht um einen Zero-Day-Exploit und es gibt keine direkte Gefahr eines Datenabflusses. Falls es wirklich zu Performance-Problemen kommt, gibt es einfache Gegenmaßnahmen. Sollte tatsächlich jemand versuchen, die wp-cron.php gezielt anzugreifen, hilft ein simples Rate Limiting, entweder über die Firewall oder direkt mit mod_security (Apache) bzw. limit_req (nginx).

Rate Limiting mit nginx

limit_req_zone $binary_remote_addr zone=cronlimit:10m rate=1r/s;

server {
    location = /wp-cron.php {
        limit_req zone=cronlimit burst=3 nodelay;
        limit_req_status 429;
    }
}

Das begrenzt die Anfragen auf 1 pro Sekunde, mit maximal 3 Anfragen in kurzer Zeit.

Sollte man wp-cron.php deaktivieren?

Nicht unbedingt. Klar, im Fall eines Angriffs kann das als erste Maßnahme helfen. Besser ist es aber, wp-cron.php lokal auszuführen und den Zugriff darauf über den Webserver auf bestimmte IP-Adressen zu beschränken. Anschließend kann man einen Cronjob anlegen, der alle 15 Minuten ausgeführt wird:

*/15 * * * * wget -q -O - https://www.example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

Zugriff per nginx einschränken:

location ~* ^/wp-cron.php$ {
    allow 1.2.3.4;  # Ersetze mit deiner IP
    deny all;
}

Fazit

Das ist ganz sicher kein P1-Bug. Und wenn der Report direkt eine Preistabelle mitliefert, ist das schon ein ziemlich eindeutiges Zeichen für einen Scam.

  • Ja, wp-cron.php könnte unter bestimmten Umständen zu Problemen führen.
  • Nein, es ist kein echter Sicherheits-Bug.
  • Wer weiß, was er tut, hat bereits die richtigen Maßnahmen getroffen.

Keine Panik. Stattdessen lieber kurz die eigene Konfiguration prüfen und gut ist. Wer auf das nginx Rate Limit setzt und dieses testen möchte, kann mein rate_limit_test.sh auf GitHub nutzen.

Siehe auch: Ist mein Netzwerk kompromittiert?

Fragen? Einfach melden.

DNS over TLS mit BIND, Stunnel und Android 9: Eigener DoT-Server

Die Zeit ging weiter, die Entwicklung bei BIND und DNS ebenfalls. Daher gibt es nun einen neuen Beitrag, der das aktuelle Setup mit BIND 9.20 auf FreeBSD 15 beschreibt – inklusive sauberer Trennung von authoritative DNS (Port 53) und öffentlichem Resolver (DoT/DoH) sowie reproduzierbaren CLI-Tests für IPv4 und IPv6. Bitte dort weiterlesen.

Über die Techniken DoT (DNS over TLS) habe ich bereits im Zusammenhang mit Bind 9.16 geschrieben. Ebenfalls DoH (DNS over HTTPS) gibt es einen kleinen Beitrag.

Bilder der Bind 9 TLS Konfiguration

Zu diesem Zeitpunkt bracht BIND 9 die Unterstützung für DoH und DoT noch nicht selbst mit. Daher waren zu diesem Zeitpunkt noch Umwege über stunnel oder nginx zusammen mit doh-proxy nötig.

Zum Glück kommt die letzte stable Version 9.18.0 (26. Januar 2022) mit dem nötigen Support.

named now supports securing DNS traffic using Transport Layer Security (TLS). TLS is used by both DNS over TLS (DoT) and DNS over HTTPS (DoH).

Warum möchte man noch gleich DoH oder DoT benutzen? Ganz einfach… Über diese Techniken werden DNS Abfragen verschlüsselt übertragen. Dieses ist ein weiterer Schutz davor manipulierte Antworten zu bekommen und selbstverständlich, damit die eigenen DNS Abfragen erst überhaupt nicht mitgelesen werden. Denn wenn von einem Gerät im Netzwerk die DNS Abfrage zu z.B.: www.tagesschau.de kommt, könnte man davon bereits Dinge ableiten.

Wie die meisten Bind Konfigurationen ist dieses ebenfalls straightforward. Ab Version 9.18 bringt Bind alles Nötige mit. Da wir nun TLS mit dem Bind sprechen möchten, benötigen wir natürlich ein gültiges Zertifikat, wie z.B. beim nginx für seine Webseite.

Ebenfalls sollte man ein paar frische Diffie-Hellmann Parameter generieren:

openssl dhparam -out dhparam.pem 4096

Die eigentliche bind Konfiguration kann in der named.conf.options geschehen:

options {
        [...]
        listen-on port 853 tls local-tls { 37.120.183.220; };
        listen-on-v6 port 853 tls local-tls { 2a03:4000:38:20e::853; };
        listen-on port 443 tls local-tls http default { 37.120.183.220;  };
        listen-on-v6 port 443 tls local-tls http default { 2a03:4000:38:20e::853; };
        [...]
        allow-recursion-on { 127.0.0.0/8; ::1/128; 2a03:4000:38:20e::853; 37.120.183.220; };
        [...]
};

Da der bind auf weiteren Ports lauschen soll erweitert man diese für IPv4 und IPv6. Der Default Port für DoH ist dabei 443 und der default Port für DoT ist 853, beides TCP.

listen-on sowie listen-on-v6 sind wohl selbsterklärend.
port ist der TCP Port und erklärt sich ebenfalls.
tls sagt dem Bind das wir tls sprechen möchten.
local-tls verweißt auf den gleichnamigen tls Block über welchen man seine TLS Konfiguration vornimmt.
http ist für DoH.
default gibt den eigentlichen endpoint für die DoH Abfragen an, im default ist es /dns-query

Da der Server unsere DNS Abfragen erledigen soll, müssen wir ihm dieses noch per allow-recursion-on auf den jeweiligen Adressen erlauben.

Als nächstes wird die eigentliche TLS Terminierung konfiguriert (das lässt sich ebenfalls auslagern, wenn gewünscht). Dafür wird der folgende Block, außerhalb der Options Blocks, ergänzt:

tls local-tls {
    cert-file "/usr/local/etc/ssl/wild.kernel-error.de/2022/ecp/chain.crt";
    key-file "/usr/local/etc/ssl/wild.kernel-error.de/2022/ecp/http.key";
    dhparam-file "/usr/local/etc/ssl/dhparam.pem";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256";
    prefer-server-ciphers yes;
    session-tickets no;
};

local-tls ist dabei der name des Blocks. Auf diesen verweisen wir oben.
cert-file ist der Pfad zum Zertifikat. Ich habe dort nicht nur das Zertifikat, sondern die gesamte Chain, also mit Intermediate und Root.
key-file ist der Pfad zum Key des Zertifikates.
dhparam-file ist der Pfad zu den Diffie-Hellman Parametern.
protocols definiert die zu verwendenden TLS Protokolle. In diesem Beispiel TLS1.2 sowie TLS1.3.
ciphers definiert die zu verwendenden cipher. Es soll ja „sicher“ bleiben.
prefer-server-ciphers übermittelt dem Client die Information, in welcher Reihenfolge protokoll/cipher Kombinationen probiert werden sollen um einen Match zu finden. Erst das vermeintlich sicherste und dann immer „schlechter“.
session-tickets regelt ob eine Wiederaufnahme von TLS Sessions erlaubt ist oder nicht. Da ich forward secrecy nutzen möchte, ist es deaktiviert.

Damit ist die Konfiguration schon abgeschlossen (Firewall ggf. nicht vergessen!). Also testen….

Ein einfaches Tool dafür ist dog, oder natürlich dig aus den bind-tools aber Version 9.18. Für bind gibt es dann die Optionen +https oder auch +tls

dig +https @dns.kernel-error.de www.kernel-error.de A
dig +tls @dns.kernel-error.de www.kernel-error.de A

Der gleiche Test mit dog, sieht wie folgt aus:

dog www.kernel-error.de --tls "@dns.kernel-error.de"
A www.kernel-error.de. 6h00m00s   148.251.40.23
dog www.kernel-error.de --https "@https://dns.kernel-error.de/dns-query"
A www.kernel-error.de. 6h00m00s   148.251.40.23

Das war es auch schon! Viele Spaß mit einem „besseren“ DNS und wenn es noch Fragen gibt, einfach fragen.

Kodi auf dem Raspberry Pi 4: Ruckelfreie Wiedergabe einrichten

Der Raspberry Pi 4 , egal ob mit 4GB oder 8GB RAM, ist in der Kombination mit Kodi eine wunderbare Erweiterung am Fernseher. Leider sorgte die letzte Version Kodi v19.3 (Matrix) bei mir für ein paar Problemchen. So stockte oder ruckelte die Wiedergabe von Videos oder die Wiedergabe lief für einige Minuten gut, dann wurde gebuffert, nur damit sich dieses Spielchen alle paar Minuten wiederholte. Egal ob im WLAN oder direkt am LAN.

Folgende Änderungen haben bei mir für eine Lösung der Probleme gesorgt:

  1. Erstellen einer XML Datei, welche die default Einstellungen des Cachings überschreibt.

    Speicherort und Dateiname ist: /storage/.kodi/userdata/advancedsettings.xml
<?xml version="1.0" encoding="utf-8"?>
<advancedsettings>
        <cache>
                <memorysize>524288000</memorysize>
                <buffermode>1</buffermode>
                <readfactor>6</readfactor>
        </cache>
</advancedsettings>

Achtung… Bei XML Dateien, spielt das richtige „Einrücken“ schon mal eine Rolle.

2. Erweitern des Arbeitsspeichers für die GPU, sowie das Erzwingen des „Turbo“ Modus.
Dafür einfach die Datei /flash/config.txt um folgende Zeilen erweitern/einpassen:

# Default GPU memory split, 76MB are needed for H264 decoder
gpu_mem=256
force_turbo=1

Wer dieses gerne per SSH machen möchte, muss das Volume /flash einmal schreibfähig mounten:

mount -o remount,rw /flash

Die Option gpu_mem setzt recht einfach den, für die Grafikkarte, reservierten Arbeitsspeicher fest auf 256MB. Dieses macht selbst bei der 4GB Raspberry PI 4 Version kein Problem.

force_turbo deaktiviert das dynamische, lastabhängige takten der CPU, GPU und des Arbeitsspeichers, sowie der Spannungen. Alles läuft daher auf Maximum, aber ohne zu übertakten. Dieses hat weniger Auswirkungen auf die Probleme bei der Wiedergabe, sorgt aber für ein allgemein „flüssigeres“ Verhalten. Dafür steigt die Stromaufnahme und die Temperatur. Da wir hier über einen Raspberry sprechen, ist es wohl für die Meisten zu vernachlässigen.

3. Um Temperatur und Geräuschpegel im Zaum zu halten, empfiehlt sich ein gutes passiv gekühltes Gehäuse. Folgendes kann ich empfehlen: https://amzn.to/3qF61pe

Das mitgelieferte Netzteil hat ausreichend Power, man kommt noch an „alles“ ran, das Gehäuse ist sehr massiv und selbst bei großer Last/langem Betrieb, wird alles nur handwarm.


Update Februar 2026 — Kodi 21 (Omega) / LibreELEC 12.x

Seit Kodi 21 (Omega), das mit LibreELEC 12.x ausgeliefert wird, funktioniert die oben beschriebene Cache-Konfiguration über die advancedsettings.xml nicht mehr korrekt. Die <cache> Sektion wird zwar noch eingelesen und im Log angezeigt — die tatsächlich aktiven Werte kommen aber aus den GUI-Settings (guisettings.xml). Im Kodi-Log erkennt man das an dieser Zeile:

New Cache GUI Settings (replacement of cache in advancedsettings.xml) are:
    Buffer Mode: 4
    Memory Size: 20 MB
    Read Factor: 4.00 x

Das bedeutet: Trotz konfigurierter 500 MB in der advancedsettings.xml läuft Kodi mit nur 20 MB Puffer — dem Default. Nicht gerade ideal.

Die neue Methode

Die Cache-Werte müssen jetzt direkt in /storage/.kodi/userdata/guisettings.xml gesetzt werden. Dafür Kodi stoppen, Datei bearbeiten, Kodi starten:

systemctl stop kodi
sleep 3

In der guisettings.xml diese drei Zeilen suchen und anpassen. Wichtig: Das default="true" muss entfernt werden, damit Kodi die Werte als benutzerdefiniert erkennt.

Vorher:

<setting id="filecache.buffermode" default="true">4</setting>
<setting id="filecache.memorysize" default="true">20</setting>
<setting id="filecache.readfactor" default="true">400</setting>

Nachher (Beispiel für 8 GB RAM):

<setting id="filecache.buffermode">1</setting>
<setting id="filecache.memorysize">500</setting>
<setting id="filecache.readfactor">600</setting>

Dann Kodi wieder starten:

systemctl start kodi

Was die Werte bedeuten

buffermode = 1

Legt fest, welche Quellen gepuffert werden:

WertBedeutung
0Nur Internet-Streams (HTTP, FTP…)
1Alles (Internet + LAN + lokal) ← empfohlen
2Nur „echte“ Internet-Streams
3Kein Puffer
4Alle Netzwerk-Quellen (Default in Kodi 21)

Wir setzen 1 statt 4, damit NFS-Quellen garantiert gepuffert werden — egal wie Kodi die Quelle intern klassifiziert.

memorysize = 500 (bzw. 250)

Die Größe des Puffers in MB. Das ist der Speicher, den Kodi im RAM reserviert, um Film-Daten vorauszulesen.

Praktisches Beispiel: Ein typischer 4K-Film hat ~80 Mbit/s Bitrate (ca. 10 MB/s).

  • 20 MB (Default): Nur ~2 Sekunden Film im Puffer. Wenn das Netzwerk kurz schwankt, stockt die Wiedergabe sofort.
  • 500 MB: Ca. 50 Sekunden Film im Puffer. Selbst wenn NFS mehrere Sekunden hängt, läuft die Wiedergabe weiter.

Empfohlene Werte nach verfügbarem RAM:

  • 8 GB RAM: memorysize = 500
  • 4 GB RAM: memorysize = 250

readfactor = 600 (= 6×)

Der Wert wird intern durch 100 geteilt, also 600 = 6,0×. Kodi liest Daten mit der 6-fachen Geschwindigkeit der benötigten Bitrate voraus. Bei einem 80 Mbit/s Film liest Kodi also mit ~480 Mbit/s vom NFS, bis der Puffer voll ist. Danach drosselt es auf die tatsächlich benötigte Rate. Das sorgt dafür, dass der Puffer sich schnell füllt und möglichst voll bleibt.

Verifizierung

Nach dem Neustart im Kodi-Log prüfen, ob die neuen Werte aktiv sind:

grep "New Cache GUI Settings" -A4 /storage/.kodi/temp/kodi.log

Hinweis zum Raspberry Pi 5

Die config.txt Anpassungen (gpu_mem=256 und force_turbo=1) gelten weiterhin für den Raspberry Pi 4. Beim Raspberry Pi 5 sind diese nicht nötig — er nutzt eine andere GPU-Architektur (VideoCore VII) und gpu_mem hat dort keine Wirkung.

Jetzt mit HTTP/3 und QUIC: Schnelleres Surfen leicht gemacht

Von QUIC habt ihr sicher alle schon gehört, seit knapp Mitte 2021 ist dieser neue Standard fertig und in einem recht einfach zu merkendem RFC 9000 beschrieben.

Im Grunde geht es darum, HTTP-Verbindungen schneller zu machen und dabei sogar UDP zum Einsatz zu bringen. Nicht ganz korrekt ist es einfach eine Weiterentwicklung von SPDY.

Um zu testen, ob eine Webseite bereits HTTP/3 also QUIC unterstützt, kann ich euch http3check.net ans Herz legen. Diese gibt, wenn gewünscht, sogar noch ein paar Detailinformationen aus.

Wer sehen möchte, ob sein Browser QUIC „macht“, kann auch nginx.org nutzen. Steht oben „Congratulations! You’re connected over QUIC.“ Dann ist man ein Gewinner.

Die Konfiguration am Nginx ist wie immer sehr einfach und ein sehr gutes Beispiel findet sich direkt von nginx.

Mein Nginx spricht dieses nun ebenfalls, mal sehen ob es Probleme gibt.


Update März 2026: Drei Jahre HTTP/3 im Betrieb

Es sind jetzt gut drei Jahre vergangen und ich kann sagen: Es gab keine Probleme. Kein einziges. HTTP/3 läuft hier seit 2022 auf allen vHosts und ich habe nie einen Fehler gesehen, der auf QUIC zurückzuführen war. Auch in den Logfiles nichts Auffälliges.

Illustration zu HTTP/3 und QUIC: schneller Web-Transport über UDP mit moderner Verschlüsselung und Browser-Support

Was sich geändert hat: Damals war HTTP/3 in Nginx noch experimentell und brauchte einen separaten Build mit dem quiche-Patch oder BoringSSL. Seit Nginx 1.25.0 (Mai 2023) ist HTTP/3 offiziell im Mainline-Branch enthalten und wird mit dem normalen --with-http_v3_module Build-Flag aktiviert. Kein Patch mehr, kein BoringSSL mehr, einfach OpenSSL 3.x und fertig. Mein aktueller Stack: Nginx 1.29.4 mit OpenSSL 3.5.4 auf FreeBSD 15.

Was bringt HTTP/3 in der Praxis?

Der größte Vorteil von QUIC gegenüber TCP ist die Verbindungsaufbauzeit. Bei TCP+TLS braucht ihr mindestens zwei Roundtrips, bevor Daten fließen (TCP Handshake + TLS Handshake). QUIC macht das in einem einzigen Roundtrip. Bei einem Wiederverbindungsversuch sogar in null Roundtrips (0-RTT).

Auf einer Glasfaserleitung mit 5 ms Latenz merkt ihr das kaum. Aber auf einem Smartphone im Zug mit 80 ms Latenz und gelegentlichem Paketverlust macht das einen spürbaren Unterschied. Dazu kommt, dass QUIC auf UDP basiert und damit das Head-of-Line-Blocking Problem von TCP löst: Ein verlorenes Paket blockiert nicht mehr alle Streams, sondern nur den einen betroffenen.

Konfiguration 2026

Die Konfiguration hat sich seit 2022 etwas verändert. Hier mein aktuelles Setup für den Blog-vHost:

server {
    listen [::]:443 ssl;
    listen [::]:443 quic;

    http2 on;

    server_name  www.kernel-error.de;

    # TLS (wird per include eingebunden)
    include tls-default.conf;
    ssl_certificate      /path/to/fullchain.pem;
    ssl_certificate_key  /path/to/privkey.pem;

    # ...
}

Wichtig sind zwei Dinge. Erstens: Der quic Listener läuft auf demselben Port 443 wie der SSL-Listener, nur eben über UDP statt TCP. Zweitens: Die Clients müssen wissen, dass HTTP/3 verfügbar ist. Das passiert über den Alt-Svc Header:

add_header Alt-Svc 'h3=":443"; ma=86400' always;

Dieser Header sagt dem Browser: „Ich spreche auch h3 auf Port 443, merk dir das für 24 Stunden.“ Beim nächsten Besuch nutzt der Browser dann direkt QUIC. Ohne diesen Header bleibt alles bei HTTP/2 über TCP.

Optional könnt ihr auch einen HTTPS-DNS-Record (SVCB) setzen, damit der Browser schon beim DNS-Lookup weiß, dass HTTP/3 verfügbar ist:

$ dig +short HTTPS www.kernel-error.de
1 . alpn="h3,h2" ipv4hint=148.251.30.200 ipv6hint=2a01:4f8:262:4716::443

Mit alpn="h3,h2" im HTTPS-Record kann der Browser die QUIC-Verbindung schon beim allerersten Besuch aufbauen, ohne erst auf den Alt-Svc Header warten zu müssen.

Firewall nicht vergessen

Ein Klassiker, der mich 2022 kurz stolpern ließ: QUIC braucht UDP Port 443. Wenn eure Firewall nur TCP 443 durchlässt, sehen die Clients den Alt-Svc Header, versuchen QUIC und laufen ins Timeout. Auf FreeBSD mit pf:

pass in quick on $ext_if proto udp to $jail_nginx port 443

Post-Quantum-Kryptografie inklusive

QUIC verwendet intern TLS 1.3 für die Verschlüsselung. Das heißt: Wenn ihr in eurer Nginx-TLS-Konfiguration X25519MLKEM768 als Key-Exchange-Gruppe konfiguriert habt, gilt das automatisch auch für QUIC-Verbindungen. Kein extra Aufwand. Euer HTTP/3 Traffic ist dann ebenfalls mit hybridem Post-Quantum-Schlüsselaustausch abgesichert.

Browser-Support

2022 war HTTP/3 noch ein Feature für Early Adopter. 2026 ist es Standard. Chrome, Firefox, Safari und Edge unterstützen QUIC seit Jahren. Laut den Logfiles dieses Blogs nutzen inzwischen gut 40% der Besucher HTTP/3. Tendenz steigend, weil immer mehr Mobilgeräte von dem schnelleren Verbindungsaufbau profitieren.

Wer es noch nicht aktiviert hat: Der Aufwand ist minimal, die Vorteile real und das Risiko gleich null. Drei Jahre Betrieb ohne ein einziges Problem sprechen für sich.

Siehe auch: HTTPS RR und SVCB Records — per DNS-Record signalisieren, dass HTTP/3 verfügbar ist. Damit können Clients direkt mit QUIC starten, ohne vorher TCP zu probieren.

Wechsel zur WordPress

Ich nutze Joomla seit 2006, als es aus Mambo hervorging. Die Datenbank und einige Plugins/Teils sind inzwischen also sehr alt und machen bei Versionssprüngen immer mehr Probleme. Den Wechsel vom CMS habe ich dennoch immer von mir weggeschoben, denn es macht viel Arbeit und für eine solch kleine Spielerei lohnt der Aufwand kaum. Da ich es inzwischen eh nur noch für einfache Blogeinträge nutze… Nun ich werde wohl in der nächsten Zeit auf WordPress wechseln. Damit wird es ebenfalls einen neuen RSS Feed geben und sicher wird ebenfalls viel Content verschwinden.

Fragen? Einfach melden.

« Ältere Beiträge

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑