Sichere Datei-Uploads mit cURL und Client-Zertifikaten
Um eine Datei auf einen HTTPS-Endpunkt hochzuladen, der ein Client-Zertifikat verlangt, kombinieren Sie
--upload-file, --cert und --key
in einem cURL-Befehl. Diese Anleitung bietet Ihnen einen lokalen Server mit gegenseitiger
TLS-Authentifizierung (mTLS), temporäre Zertifikate und einen Upload, den Sie Byte für Byte prüfen können.
Serververtrauen von Client-Authentifizierung trennen
HTTPS verschlüsselt die Verbindung und ermöglicht cURL, die Identität des Servers zu prüfen. Bei mTLS verlangt der Server zusätzlich den Nachweis, dass der Client den privaten Schlüssel zu einem vertrauenswürdigen Zertifikat besitzt. Diese Prüfungen erfolgen in entgegengesetzte Richtungen:
| cURL-Option | Zweck in diesem Beispiel |
|---|---|
--cacert ca.crt | Der CA vertrauen, die das Serverzertifikat ausgestellt hat; den Hostnamen der URL weiterhin prüfen. |
--cert client.crt | Dem Server das öffentliche Zertifikat des Clients vorlegen. |
--key client.key | Den Besitz des zugehörigen privaten Schlüssels nachweisen. |
Ein Client-Zertifikat macht einen nicht vertrauenswürdigen Server nicht vertrauenswürdig. Lassen Sie
die Serverprüfung von cURL aktiviert;
--insecure würde sie umgehen.
Der temporären CA unten vertrauen nur diese Befehle und dieser Empfänger. Der Vertrauensspeicher
Ihres Systems bleibt unverändert.
Lokale Werkzeuge vorbereiten
Voraussetzungen
Verwenden Sie eine Linux-Shell mit Bash, einem mit OpenSSL kompilierten cURL, OpenSSL 3, Python 3
und cmp. Python-Pakete sind nicht erforderlich. Prüfen Sie die
installierten Werkzeuge:
curl --version
openssl version
python3 --version
Der cURL-Befehl verwendet --fail-with-body, verfügbar seit cURL 7.76.0. Siehe die
getesteten Kombinationen unten. Diese Anleitung belegt nicht das
Verhalten mit Windows-Zertifikatsspeichern oder anderen TLS-Backends.
Temporäre Zertifikate erstellen
Führen Sie dies in einem Verzeichnis aus, in dem Sie mtls-demo erstellen können.
Die Subshell stoppt bei Fehlern und verweigert die Wiederverwendung eines vorhandenen Verzeichnisses.
Ihr Terminal bleibt im übergeordneten Verzeichnis. Alle Schlüssel und Zertifikate bleiben in
mtls-demo, und die Zertifikate laufen nach zwei Tagen ab.
(
set -eu
umask 077
mkdir mtls-demo
cd mtls-demo
openssl req -x509 -newkey rsa:2048 -noenc -sha256 -days 2 \
-keyout ca.key -out ca.crt -subj '/CN=Local upload demo CA' \
-addext 'basicConstraints=critical,CA:TRUE' \
-addext 'keyUsage=critical,keyCertSign,cRLSign'
openssl req -new -newkey rsa:2048 -noenc \
-keyout server.key -out server.csr -subj '/CN=localhost'
printf '%s\n' 'basicConstraints=critical,CA:FALSE' \
'keyUsage=critical,digitalSignature,keyEncipherment' \
'extendedKeyUsage=serverAuth' 'subjectAltName=DNS:localhost' > server.ext
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-set_serial 1 -days 2 -sha256 -extfile server.ext -out server.crt
openssl req -new -newkey rsa:2048 -noenc \
-keyout client.key -out client.csr -subj '/CN=Local upload client'
printf '%s\n' 'basicConstraints=critical,CA:FALSE' \
'keyUsage=critical,digitalSignature' 'extendedKeyUsage=clientAuth' > client.ext
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-set_serial 2 -days 2 -sha256 -extfile client.ext -out client.crt
)
Die Zertifikatserweiterungen weisen Server und Client unterschiedliche Rollen zu. Der alternative
Antragstellername (Subject Alternative Name) des Servers ist localhost.
Er muss mit dem Hostnamen in der Upload-URL übereinstimmen. OpenSSL dokumentiert diese
Zertifikatserweiterungen und die
Option -noenc, die diese kurzlebigen Schlüssel
unverschlüsselt lässt. umask 077 beschränkt den Zugriff auf das neue Verzeichnis
und die Dateien auf Ihr Benutzerkonto.
Einen Empfänger starten, der ein Client-Zertifikat verlangt
Speichern Sie Folgendes als mtls-demo/receiver.py. Der Empfänger akzeptiert eine rohe
Anfrage vom Typ PUT /upload mit bekanntem Content-Length
bis zu 1 MiB, auch mit leerem Body. Jeder abgeschlossene Upload ersetzt
received.bin; abgelehnte Anfragen lassen die vorherige Datei unverändert.
import argparse
import ssl
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
class UploadHandler(BaseHTTPRequestHandler):
def do_PUT(self):
if self.path != "/upload":
self.send_error(404, "Use /upload")
return
length = self.headers.get("Content-Length", "")
if self.headers.get("Transfer-Encoding") or not length.isascii() or not length.isdecimal():
self.send_error(411, "A Content-Length is required")
return
size = int(length)
if size > 1024 * 1024:
self.send_error(413, "Limit is 1 MiB")
return
self.connection.settimeout(10)
data = self.rfile.read(size)
if len(data) != size:
self.send_error(400, "Incomplete upload")
return
Path("received.bin").write_bytes(data)
reply = f"Stored {len(data)} bytes\n".encode()
self.send_response(201)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(reply)))
self.end_headers()
self.wfile.write(reply)
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=0)
args = parser.parse_args()
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.load_cert_chain("server.crt", "server.key")
context.load_verify_locations("ca.crt")
context.verify_mode = ssl.CERT_REQUIRED
with HTTPServer(("127.0.0.1", args.port), UploadHandler) as server:
server.socket = context.wrap_socket(server.socket, server_side=True)
print(f"https://localhost:{server.server_port}/upload", flush=True)
server.serve_forever()
Pythons CERT_REQUIRED lehnt Clients ohne gültiges
Zertifikat ab, das von einer vertrauenswürdigen CA ausgestellt wurde. In dieser Demo erlaubt jedes
gültige Client-Zertifikat unserer CA einen Upload. Ein realer Dienst benötigt zusätzlich eine eigene
Autorisierungsrichtlinie.
Starten Sie den Empfänger aus dem übergeordneten Verzeichnis und lassen Sie ihn laufen:
(cd mtls-demo && python3 receiver.py)
Er bindet sich nur an IPv4-Loopback und gibt eine Upload-URL mit einem verfügbaren Port aus. Kopieren
Sie diese URL für den nächsten Schritt. Ein fehlendes Zertifikat oder ein Bindungsfehler stoppt den
Start, bevor eine URL ausgegeben wird. Dies ist ein lokaler Server zu Lernzwecken; Pythons
http.server ist nicht für den Produktivbetrieb gedacht.
Datei hochladen und vergleichen
Öffnen Sie in einem zweiten Terminal dasselbe übergeordnete Verzeichnis und erstellen Sie eine
kleine Datei. Dieser Befehl ersetzt eine bereits vorhandene Datei payload.txt
im Demo-Verzeichnis:
(cd mtls-demo && printf 'mTLS upload\n' > payload.txt)
Ersetzen Sie im folgenden Block PORT durch den Port, den der Empfänger
ausgegeben hat. Führen Sie den Block aus dem übergeordneten Verzeichnis aus:
(
set -eu
cd mtls-demo
upload_url='https://localhost:PORT/upload'
status=$(curl --disable --silent --show-error --fail-with-body \
--noproxy '*' --connect-timeout 5 --max-time 20 \
--cacert ca.crt --cert client.crt --key client.key \
--upload-file payload.txt --output response.txt --write-out '%{http_code}' \
"$upload_url")
cat response.txt
printf 'HTTP %s\n' "$status"
test "$status" = 201
cmp payload.txt received.bin
)
Erwartete Ausgabe:
Stored 12 bytes
HTTP 201
cmp gibt nichts aus, wenn die ursprünglichen und empfangenen Bytes
übereinstimmen. Für diesen Endpunkt bedeutet HTTP 201, dass der Empfänger
die Datei geschrieben hat. Dies sagt nichts über einen Sicherheitsscan, Backups oder eine dauerhafte
Speicherung aus. Die Datei bleibt auf dem Datenträger, wenn Sie den Empfänger stoppen.
--upload-file wählt HTTP PUT mit der Datei als
Anfrage-Body. Eine API, die einen Multipart-POST erwartet, benötigt ein anderes Anfrageformat.
Prüfen Sie die Anforderungen des Endpunkts an Methode und Body, bevor Sie diesen Befehl anpassen.
--fail-with-body sorgt dafür, dass HTTP-Fehler den
cURL-Exit-Code 22 zurückgeben. Der Antwort-Body wird dabei in
response.txt gespeichert. Die Subshell stoppt bei diesem Fehler. Die explizite
Prüfung auf 201 lehnt auch unerwartete Erfolgs- oder Weiterleitungsstatus ab.
Eine empfangene Antwort überschreibt response.txt; bei einem frühen TLS-Fehler
kann eine ältere Antwortdatei erhalten bleiben. Verwenden Sie diese Datei daher nicht allein als
Erfolgsnachweis. --disable überspringt Ihre standardmäßige cURL-Konfiguration,
und --noproxy '*' verhindert, dass diese Loopback-Anfrage über Proxys läuft, die
in der Umgebung konfiguriert sind.
Häufige Probleme beheben
Lesen Sie den cURL-Fehler, bevor Sie eine gespeicherte Antwort untersuchen. Um den Exit-Status der
Subshell zu sehen, führen Sie echo "$?" unmittelbar nach dem Upload-Block aus.
Führen Sie diese Prüfungen einzeln durch und stellen Sie zwischen den Versuchen jeweils den
funktionierenden Befehl wieder her:
Zertifikatsprüfung fehlgeschlagen
Wenn Sie den Hostnamen der URL von localhost in 127.0.0.1
ändern, sollte der cURL-Fehler 60 auftreten: Das Zertifikat nennt
localhost, nicht die IP-Adresse. Auch eine nicht zugehörige CA, die mit
--cacert angegeben wird, lässt die Prüfung fehlschlagen. Verwenden Sie die
vertrauenswürdige CA des Serverbetreibers und den Hostnamen, den das Zertifikat abdeckt. Beheben Sie
keinen dieser Fehler, indem Sie die Prüfung deaktivieren.
Fehler beim Client-Zertifikat
Wenn Sie --cert client.crt --key client.key entfernen, sollte der TLS-Handshake fehlschlagen, bevor
der Upload-Handler ausgeführt wird. Auch ein nicht vertrauenswürdiges Client-Zertifikat führt zum
Fehlschlag. Der genaue cURL-Code bei einem abgelehnten Handshake kann je nach TLS-Version und Backend
variieren; er unterscheidet sich von einer HTTP-Ablehnung. Einige Builds melden einen Sende- oder
Empfangsfehler statt einer zertifikatsspezifischen Meldung.
Der Fehler 58 weist dagegen auf Probleme beim Laden oder Verwenden der
lokalen Client-Zugangsdaten hin. Prüfen Sie, ob das Zertifikat im PEM-Format vorliegt, der private
Schlüssel dazu passt und Ihr Benutzerkonto beide Dateien lesen kann. Verwenden Sie die
Zertifikatsoptionen, die zu Ihrem TLS-Backend passen. In cURL 8.22.0
führt eine fehlende Schlüsseldatei früher zur Ablehnung, mit dem Code
43 und einer Diagnose zum Laden der Datei.
HTTP-Ablehnung
Ändern Sie /upload in /missing und behalten Sie die
gültigen Zertifikate bei. TLS ist erfolgreich, aber der Server gibt HTTP
404 zurück, und cURL beendet sich mit 22.
Eine Datei über 1 MiB wird mit HTTP 413 abgelehnt. In keinem der beiden
Fälle wird received.bin ersetzt. Prüfen Sie bei diesen HTTP-Fehlern den aktuellen
Antwort-Body in mtls-demo/response.txt. Andere Zertifikate beheben weder einen falschen Pfad
noch eine zu große Datei.
Bewährte Verfahren für die Sicherheit
Halten Sie ca.key, server.key und
client.key geheim, ebenso jede kombinierte PEM-Datei, die einen Schlüssel enthält.
Nehmen Sie sie nicht in die Versionsverwaltung auf und laden Sie das Demo-Verzeichnis nicht hoch.
Stoppen Sie den Empfänger nach Abschluss mit Ctrl+C. Prüfen Sie anschließend, ob das temporäre
Verzeichnis nichts enthält, das Sie behalten möchten, und entfernen Sie es dann.
Umgang mit Zertifikatspassphrasen
Die Schlüssel der Demo sind unverschlüsselt, damit die lokale Übung ohne Eingabeaufforderungen läuft.
Bei einem verschlüsselten PEM-Schlüssel und dem OpenSSL-Backend kann cURL dessen Passphrase abfragen.
Hängen Sie die Passphrase niemals an --cert an und übergeben Sie sie nicht
als Befehlszeilenargument: Dadurch kann sie im Shell-Verlauf oder in Prozessargumenten sichtbar werden.
Unbeaufsichtigte Jobs benötigen einen separaten Weg zur Bereitstellung sensibler Zugangsdaten,
etwa einen Secrets-Manager und eine geschützte Zugangsdaten-Datei. Der Zugriff muss dabei auf das
Dienstkonto beschränkt sein.
Versionskompatibilität
Die vollständige lokale Anleitung wurde unter Linux mit diesen Kombinationen durchgespielt:
| Umgebung | cURL und TLS-Backend | OpenSSL-CLI | Python |
|---|---|---|---|
| Ubuntu-24.04-Container | cURL 8.5.0, OpenSSL 3.0.13 | 3.0.13 | 3.12.3 |
| Arch-basierter Host | cURL 8.22.0, OpenSSL 3.6.4 | 3.6.4 | 3.14.7 |
Diese Prüfungen decken die Datei-Bytes und das Fehlerverhalten mit dem lokalen Empfänger ab. Windows, macOS, andere TLS-Backends und produktive Upload-Dienste erfordern eine eigene Überprüfung.
