Kurz gesagt: ERR_SSL_PROTOCOL_ERROR heißt, dass eine Verbindung über SSL (genauer: TLS) unmöglich war. Chrome hat einen TLS-Handshake begonnen, eine Antwort bekommen, die kein gültiges TLS ist, und abgebrochen. Meist spricht der Port hinter der https://-Adresse reines HTTP: nginx mit listen 443;, aber ohne ssl, ein Docker-Port, der auf den falschen Container-Port zeigt, oder ein Dev-Server auf localhost. Siehst nur du den Fehler und andere nicht, stört etwas auf deinem Gerät oder in deinem Netz HTTPS. Firefox zeigt denselben Fall als „Fehler: Gesicherte Verbindung fehlgeschlagen“.
Zwei Befehle von einem beliebigen Rechner mit curl zeigen, auf welcher Seite das Problem liegt:
# 1. Wo bricht der Handshake ab?
curl -sv -o /dev/null https://example.com/
# 2. Antwortet Port 443 in reinem HTTP?
curl -sS -i http://example.com:443/Ersetze example.com durch den Host, der scheitert. Ein Port, der wirklich TLS erwartet, weist den zweiten Befehl ab: nginx und Cloudflare antworten mit 400 The plain HTTP request was sent to HTTPS port, Apache mit You’re speaking plain HTTP to an SSL-enabled server port, Caddy und andere Go-Server mit Client sent an HTTP request to an HTTPS server. Bekommst du stattdessen deine normale Seite oder eine Weiterleitung, spricht Port 443 reines HTTP, und du hast den Fehler gefunden: Ursache 1. Die Meldungen unten entsprechen curl 7.88 mit OpenSSL 3.0 auf Debian 12; das LibreSSL von macOS formuliert sie etwas anders.
Welche Meldung siehst du?
| Client | Meldung | Bedeutet meist |
|---|---|---|
| Chrome, Edge | ERR_SSL_PROTOCOL_ERROR; in Chrome unter „Diese Website kann keine sichere Verbindung bereitstellen“ und „example.com hat eine ungültige Antwort gesendet.“ | Die Antwort war kein TLS, oder der Handshake ist auf ungewöhnliche Weise gescheitert: reines HTTP auf 443, ein anderer Dienst, ein Filter |
| Chrome, Edge | „localhost hat eine ungültige Antwort gesendet.“ | Dein Dev-Server spricht HTTP, du hast HTTPS angefragt: localhost |
| Firefox | „Fehler: Gesicherte Verbindung fehlgeschlagen“ · SSL_ERROR_RX_RECORD_TOO_LONG | Reines HTTP auf dem HTTPS-Port |
| Firefox | PR_END_OF_FILE_ERROR | Die Verbindung wurde mitten im Handshake gekappt: Proxy, VPN, Antivirus, Filter |
| Safari | Safari kann keine sichere Verbindung zum Server herstellen (sinngemäß) | Jeder TLS-Fehler; mach den curl-Test oben |
| curl | curl: (35) … wrong version number | Reines HTTP oder ein anderes Protokoll auf dem Port |
| Chrome, Edge | „… verwendet ein nicht unterstütztes Protokoll.“ · ERR_SSL_VERSION_OR_CIPHER_MISMATCH | Nicht dieser Fehler: keine gemeinsame TLS-Version oder Cipher |
| Chrome, Edge | „Dies ist keine sichere Verbindung“ · NET::ERR_CERT_… | Nicht dieser Fehler: Der Handshake hat geklappt, und das Zertifikat wurde abgelehnt |
| Cloudflare-Seite | Error 525 SSL handshake failed | Derselbe Fehler zwischen Cloudflare und deinem Server: Cloudflare |
Was der Fehler tatsächlich bedeutet
Scheitert ein Handshake, übersetzt Chromium den Grund aus BoringSSL in einen net::ERR_*-Code. Einige Gründe bekommen einen eigenen Code; alles andere landet bei ERR_SSL_PROTOCOL_ERROR (Code -107). Der Code ist ein Sammelbecken, deshalb gehen die Ratschläge dazu so weit auseinander.
Die Zuordnung unten folgt openssl_ssl_util.cc von Chromium; die curl-Zeilen sind die Meldungen, die curl bei genau dieser Antwort des Servers ausgibt:
| Was der Server geschickt hat | curl meldet | Chrome zeigt |
|---|---|---|
| Reines HTTP oder ein SSH-Banner | wrong version number | ERR_SSL_PROTOCOL_ERROR |
| TLS-Alert access_denied (49), ohne dass ein Client-Zertifikat verlangt wurde | tlsv1 alert access denied | ERR_SSL_PROTOCOL_ERROR; Chromium vermerkt, dass manche Firewalls ihn schicken, um eine Seite zu sperren |
| TLS-Alert illegal_parameter (47) | sslv3 alert illegal parameter | ERR_SSL_PROTOCOL_ERROR |
| TLS-Alert handshake_failure (40) direkt nach dem ClientHello | sslv3 alert handshake failure | ERR_SSL_VERSION_OR_CIPHER_MISMATCH |
| TLS-Alert protocol_version (70), z. B. ein Server, der nur TLS 1.0/1.1 kann | tlsv1 alert protocol version, oder unsupported protocol, wenn der Server mit einem alten ServerHello antwortet (mit --tlsv1.2) | ERR_SSL_VERSION_OR_CIPHER_MISMATCH |
| TLS-Alert unrecognized_name (112) | tlsv1 unrecognized name | ERR_SSL_UNRECOGNIZED_NAME_ALERT |
| Nichts, Verbindung geschlossen | SSL_ERROR_SYSCALL | ERR_CONNECTION_CLOSED |
| Ein TCP-Reset | Recv failure: Connection reset by peer | ERR_CONNECTION_RESET |
curl 7.88 bietet noch TLS 1.0 und 1.1 an; gegen einen solchen Server bricht OpenSSL 3.0 dann mit legacy sigalg disallowed or unsupported ab. Häng --tlsv1.2 an, um so zu testen, wie ein Browser sich verbindet. Ein abgelaufenes Zertifikat, eine alte TLS-Version oder eine falsche Uhr lösen im aktuellen Chrome kein ERR_SSL_PROTOCOL_ERROR aus; sie haben eigene Codes.
Wenn nur du ihn siehst: Prüfungen auf deinem Gerät
Öffne die Adresse auf deinem Handy über mobile Daten. Lädt sie dort, liegt die Ursache bei deinem Rechner oder deinem Netz:
- Inkognito-Fenster und ein zweiter Browser. Erweiterungen sind im Inkognito-Modus aus, außer du hast sie erlaubt. VPN- und Proxy-Erweiterungen sind häufige Übeltäter, ebenso ein System-Proxy in den Netzwerkeinstellungen des Betriebssystems.
- HTTPS-Scan im Antivirus und Firmen-Firewalls. Sie sitzen mitten in jedem Handshake, und wenn sie sich verschlucken, sieht Chrome Datenmüll. Schalte HTTPS-Scan oder SSL/TLS-Filterung für einen Test ab. Klick auf einer funktionierenden Website auf das Symbol links neben der Adresse, dann auf Verbindung ist sicher und Zertifikat ist gültig: Ein Aussteller, der nach deinem Antivirus oder deinem Arbeitgeber benannt ist, heißt, dass dein HTTPS abgefangen wird.
- Post-Quanten-Schlüsselaustausch durch alte Prüfgeräte. Seit Version 131 bietet Chrome im TLS-1.3-Handshake einen Post-Quanten-Key-Share mit ML-KEM an. Laut Fortinets Hinweis zu diesem bekannten Problem antwortet die flow-basierte Deep Inspection von FortiGate mit älteren IPS-Engines darauf mit einem illegal_parameter-Alert, also
ERR_SSL_PROTOCOL_ERROR. Die Lösung ist ein Firewall-Update; die Chrome-Richtlinie zum Abschalten dieses Key Shares,PostQuantumKeyAgreementEnabled, endete mit Chrome 146. - Ein Eintrag in der Hosts-Datei. Eine Zeile für die Website in
/etc/hostsoderC:\Windows\System32\drivers\etc\hosts, übrig von einem Umzugstest, schickt dich zu einer alten Maschine, die vielleicht gar kein TLS spricht. Lösch sie. - QUIC.
chrome://flags/#enable-quicabzuschalten ist ein schneller Test, aber QUIC-Fehler zeigen sich normalerweise alsERR_QUIC_PROTOCOL_ERROR, und Chrome fällt von selbst auf TCP zurück. - Ein veralteter HSTS-Eintrag. Hat die Website früher HTTPS genutzt und es aufgegeben, erzwingt Chrome trotzdem weiter HTTPS.
chrome://net-internals/#hsts(in Edgeedge://net-internals/#hsts) hat ein Feld Delete domain security policies; vorab geladene Einträge lassen sich dort nicht löschen.
Für Betreiber: den Fehler von außen nachstellen
Teste von außen und über beide Adressfamilien: Ein Server kann auf IPv4 TLS sprechen und auf IPv6 reines HTTP, und dann sehen nur manche Besucher den Fehler.
curl -4 -sS -o /dev/null -w '%{http_code}\n' https://example.com/
curl -6 -sS -o /dev/null -w '%{http_code}\n' https://example.com/Ein Statuscode heißt, dass dieser Weg funktioniert. Für Details lässt du OpenSSL den Handshake machen:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>&1 \
| grep -E 'error|handshake has read'40378B48D87F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../ssl/record/ssl3_record.c:350:
SSL handshake has read 5 bytes and written 317 bytesread 5 bytes verrät es: OpenSSL hat einen Record-Header gelesen, dort, wo TLS beginnen sollte, HTTP/ gefunden und aufgegeben. Ein funktionierender Server zeigt keine Fehlerzeile und mehrere Kilobyte gelesene Daten, für example.com:443 knapp 4.000 Byte.
Ohne Shell führt unser SSL-Checker (engl.) von Amsterdam aus einen TLS-Handshake mit Port 443 durch und zeigt statt eines Zertifikats den Fehler, wenn keiner zustande kommt.
Meldet der Checker gar keine Verbindung, sagt dir der Open-Port-Checker (engl.), ob TCP 443 überhaupt erreichbar ist; ein geschlossener Port ergibt ERR_CONNECTION_REFUSED, nicht diesen Fehler.
Ursache 1: Port 443 antwortet in reinem HTTP
nginx spricht TLS nur auf einem Socket, dessen listen-Zeile ssl trägt. Fehlt es, liefert nginx auf 443 reines HTTP aus, selbst mit ssl_certificate-Zeilen im selben Block, und nginx -t beschwert sich nicht. Dieser Block besteht den Test unter nginx 1.22 aus Debian 12:
server {
listen 443; # "ssl" fehlt
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}Die IPv6-Zeile wird gern vergessen: listen [::]:443; neben einem korrekten listen 443 ssl; ist ein eigener Socket, IPv6-Besucher bekommen also reines HTTP. Liste jede 443-Zeile ohne ssl in der Konfiguration auf, die nginx tatsächlich geladen hat:
sudo nginx -T 2>/dev/null | grep -E '^\s*listen\s+(\S*:)?443\b' | grep -vE 'ssl|quic'Im Idealfall kommt keine Ausgabe. Ergänze bei jeder ausgegebenen Zeile ssl, dann testen und neu laden:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/example.com;
}sudo nginx -t && sudo systemctl reload nginxnginx hinterlässt außerdem einen Fingerabdruck. Schickt ein Browser sein TLS-ClientHello an einen Socket mit reinem HTTP, protokolliert das Access-Log eine „Anfrage“, die mit dem TLS-Record-Header \x16\x03\x01 beginnt, und den Status 400:
sudo grep -c '\\x16\\x03\\x01' /var/log/nginx/access.logEin paar Treffer am Tag sind Scanner; ein Treffer bei jedem HTTPS-Besuch heißt, dass ein 443-Block reines HTTP spricht. Andere Stacks haben dieselbe Falle:
- Apache: ein
<VirtualHost *:443>, kopiert vom Block für Port 80, ohneSSLEngine on. mod_ssl ist für jeden virtuellen Host aus, solange diese Zeile es nicht einschaltet; unter Debian zusätzlichsudo a2enmod sslausführen. - Docker:
-p 443:80oder"443:80"in Compose veröffentlicht den reinen HTTP-Port des Containers auf dem HTTPS-Port des Hosts, indocker pszu sehen als0.0.0.0:443->80/tcp. Veröffentliche die App auf127.0.0.1:8080:80und überlass 443 einem Reverse Proxy mit Zertifikat. - Node-Apps:
app.listen(443)in Express startet einen reinen HTTP-Server. Nimm das Modulhttps, oder besser: Lass die App auf einem lokalen Port hinter nginx laufen.
Ursache 2: Die Korrektur wurde nie geladen
Zertifikat und Schlüssel, die nicht zusammengehören, können diesen Fehler nicht auslösen: nginx, Node und Python weigern sich alle, das Paar zu laden. Die Falle ist leiser. Du korrigierst die listen-Zeile, das Neuladen scheitert, und nginx liefert weiter die alte Konfiguration aus. Die beiden üblichen Meldungen von nginx -t (nginx 1.22, OpenSSL 3.0, Beispielpfade):
nginx: [emerg] SSL_CTX_use_PrivateKey("/etc/ssl/private/example.com.key") failed (SSL: error:05800074:x509 certificate routines::key values mismatch)
nginx: configuration file /etc/nginx/nginx.conf test failednginx: [emerg] no "ssl_certificate" is defined for the "listen ... ssl" directive in /etc/nginx/sites-enabled/example.com:1
nginx: configuration file /etc/nginx/nginx.conf test failedDie Zeilennummer zeigt auf den Anfang des server-Blocks. Ein gescheitertes systemctl reload lässt das laufende nginx unberührt; ein restart mit demselben Fehler lässt nginx gestoppt zurück, und aus diesem Fehler wird ERR_CONNECTION_REFUSED. Setz immer nginx -t && vor das Neuladen und teste danach von außen.
Ursache 3: Etwas anderes als ein Webserver antwortet
Port 443 kommt durch strenge Netze, deshalb belegt ihn oft etwas, das kein HTTPS ist: SSH, ein OpenVPN-Server über TCP, ein Multiplexer wie sslh mit falscher Voreinstellung, oder ein Router, der 443 an den falschen internen Port weiterleitet. So sieht die Meldung von curl bei einem Endpunkt aus, der mit einem OpenSSH-Banner antwortet:
curl: (35) OpenSSL/3.0.22: error:0A00010B:SSL routines::wrong version numberSchau auf dem Server nach, welcher Prozess den Port belegt:
sudo ss -ltnp 'sport = :443'Die letzte Spalte nennt das Programm, zum Beispiel users:(("nginx",…)). Ist es nicht dein Webserver, oder steht dort gar nichts, obwohl der Port von außen antwortet, landet der Verkehr woanders: Prüf die Portweiterleitung des Routers, den Zielport des Load Balancers, das Docker-Mapping und ob die A- und AAAA-Einträge (dig +short example.com A, dann AAAA) noch einen alten Server nennen.
Ursache 4: HSTS schickt Browser zu HTTPS, wo es keins gibt
Hat eine Website einmal Strict-Transport-Security gesendet, schreibt der Browser jede http://-Anfrage für diesen Host in https:// um, so lange, wie max-age sagt; mit includeSubDomains auch für jedes interne Werkzeug auf einer Subdomain.
Die Falle ist der Port. Chromium tauscht nur das Schema, aus http://tools.example.com:8080/ wird also https://tools.example.com:8080/, und die App mit reinem HTTP auf 8080 bekommt einen TLS-Handshake: ERR_SSL_PROTOCOL_ERROR. Die DevTools zeigen die Umschreibung im Tab Network als 307 Internal Redirect mit Non-Authoritative-Reason: HSTS. Nur aus Port 80 wird 443.
Ganze Top-Level-Domains stehen auf der HSTS-Preload-Liste von Chrome, darunter .dev, .app und .page: Alles darunter funktioniert nur über HTTPS. Liefere unter diesem Namen TLS aus oder zieh das Werkzeug aus dem Geltungsbereich der Richtlinie. HSTS lässt sich nicht schnell zurücknehmen: Browser vergessen es erst, wenn max-age abläuft oder wenn sie max-age=0 von diesem Host über gültiges HTTPS abrufen.
Hinter Cloudflare
Bei einem Eintrag mit Proxy (orange Wolke) endet der TLS-Handshake des Browsers bei Cloudflare, nicht bei deinem Server. Ein kaputter Ursprungsserver zeigt dann die eigene Fehlerseite von Cloudflare, 525 SSL handshake failed oder 526 Invalid SSL certificate, nie ERR_SSL_PROTOCOL_ERROR. Prüf, welcher Fall vorliegt:
dig +short example.com A
dig +short example.com AAAACloudflare-Adressen (aufgeführt unter cloudflare.com/ips) heißen, dass dieser Fehler fast immer auf der Seite des Besuchers liegt. Die IP deines eigenen Servers heißt, der Eintrag steht auf „DNS only“ (graue Wolke) oder Cloudflare ist pausiert.
Genau bei diesem Umschalten fallen Websites im Modus Flexible um: Flexible spricht reines HTTP mit deinem Server, der Ursprungsserver brauchte also nie TLS auf 443. Direkt erreicht, gibt er HTTPS-Besuchern diesen Fehler oder ERR_CONNECTION_REFUSED. Installier ein echtes Zertifikat auf dem Ursprungsserver und stell auf Full (strict) um. Ein Cloudflare-Origin-CA-Zertifikat funktioniert nur mit eingeschaltetem Proxy, weil Browser ihm nicht vertrauen.
Fehler 525 und 526 mit Tests für den Ursprungsserver behandelt unsere Anleitung zu SSL handshake failed (engl.).
ERR_SSL_PROTOCOL_ERROR auf localhost
„localhost hat eine ungültige Antwort gesendet“ heißt fast immer, dass der Dev-Server reines HTTP spricht und der Browser HTTPS angefragt hat. So sieht das bei einem http.server von Python auf 127.0.0.1:18443 aus, einmal in curl, einmal im Log des Servers:
curl: (35) OpenSSL/3.0.22: error:0A00010B:SSL routines::wrong version number127.0.0.1 - - [12/Oct/2026 10:09:16] "\x16\x03\x01\x02\x00\x01\x00\x01…" 400 -Warum der Browser HTTPS genommen hat:
- Du hast es eingetippt, oder ein Lesezeichen enthält es. Öffne
http://localhost:PORT. - Die App leitet auf HTTPS um. Rails
config.force_ssl, DjangoSECURE_SSL_REDIRECTundUseHttpsRedirection()in ASP.NET Core sind für die Produktion gedacht; schalte sie in der Entwicklungskonfiguration ab. - HSTS aus einem früheren Projekt. Seit Version 132 ignoriert Chrome HSTS für
localhostund*.localhost. Ältere Versionen und eigene Namen wiemyapp.testbehalten es: Lösch den Eintrag unterchrome://net-internals/#hsts. Ein lokaler Name unter.devoder.appbraucht immer HTTPS; nimm stattdessen.testoder.localhost.
Brauchst du HTTPS in der Entwicklung, gib dem Dev-Server ein Zertifikat. Next.js hat next dev --experimental-https, das ein selbst signiertes erzeugt; mkcert erzeugt lokal vertrauenswürdige Zertifikate für jeden Dev-Server.
Nicht dieser Fehler: Probleme mit Version, Cipher und Zertifikat
Ein anderer Code heißt, dass der Handshake weitergekommen ist. Ein Server nur mit TLS 1.0 oder 1.1 oder ohne Cipher, den Chrome akzeptiert, ergibt ERR_SSL_VERSION_OR_CIPHER_MISMATCH. Ein Zertifikat, das abgelaufen, selbst signiert oder für einen anderen Namen ausgestellt ist, ergibt einen NET::ERR_CERT_*-Code unter „Dies ist keine sichere Verbindung“ (in Edge „Ihre Verbindung ist nicht privat“).
Jeden dieser Codes erklärt unsere Anleitung zu „Ihre Verbindung ist nicht privat“.
Fehler bei Version, Cipher und SNI, mit einer vollständigen TLS-Konfiguration für nginx, stehen in SSL handshake failed (engl.).
Checkliste nach jeder HTTPS-Änderung
sudo nginx -tmeldettest is successful, und das Neuladen hat nichts ausgegeben.curl -sS -i http://example.com:443/wird mit einer 400 abgewiesen, undcurl -4undcurl -6gegenhttps://liefern beide einen Statuscode, von außerhalb des Servers.- Ports 80 und 443 sind in der Firewall offen: Port 443 und 80 zeigt, wie du beide öffnest und testest.
Auf einem Linux VPS von VMHeaven hast du für jeden Schritt hier root, und falls dich eine Änderung an nginx oder der Firewall aussperrt, erreicht die Browser-VNC-Konsole im Panel den Server trotzdem.
Häufige Fragen
Liegt ERR_SSL_PROTOCOL_ERROR an mir oder an der Website?
Öffne die Website in einem anderen Netz, etwa auf dem Handy über mobile Daten, oder führe curl -sv https://example.com/ auf einem Server aus. Scheitert es überall, ist die Website kaputt, meist mit reinem HTTP auf Port 443. Scheitert es nur auf deinem Rechner oder in deinem Netz, prüf HTTPS-Scan im Antivirus, eine Firmen-Firewall, ein VPN oder einen Proxy.
Was bedeutet „Fehler: Gesicherte Verbindung fehlgeschlagen“ in Firefox?
Firefox konnte keine verschlüsselte Verbindung aufbauen. Der Code darunter sagt, warum: SSL_ERROR_RX_RECORD_TOO_LONG heißt, dass auf dem HTTPS-Port reines HTTP antwortet, wie bei ERR_SSL_PROTOCOL_ERROR in Chrome; PR_END_OF_FILE_ERROR heißt, dass ein Proxy, VPN, Antivirus oder Filter die Verbindung mitten im Handshake gekappt hat.
Wie behebe ich ERR_SSL_PROTOCOL_ERROR auf localhost?
Dein Dev-Server spricht reines HTTP, und der Browser hat HTTPS angefragt. Öffne stattdessen http://localhost:PORT, schalte eine HTTPS-Weiterleitung in der Entwicklungskonfiguration der App ab oder gib dem Dev-Server ein Zertifikat, zum Beispiel mit next dev --experimental-https oder einem Zertifikat von mkcert.