Viele Webanwendungen müssen Dateien zum Download bereitstellen, etwa wenn Nutzer:innen Vertragsunterlagen als PDF herunterladen sollen. Sobald du genauer hinschaust, landest du typischerweise bei Anforderungen wie diesen:
- REQ#1: Als Nutzer:in möchte ich Dateien aus der Anwendung herunterladen.
- REQ#2: Als Nutzer:in möchte ich den Download-Link an eine andere Person schicken.
- REQ#3: Als Nutzer:in möchte ich eine Datei auch später herunterladen, ohne angemeldet zu sein.
- REQ#4: Als Sicherheitsbeauftragte:r möchte ich, dass nur berechtigte Personen Dateien herunterladen.
- REQ#5: Als Compliance-Verantwortliche:r verlange ich, dass Dateien auf unserem eigenen Speicher liegen und nicht in einer Cloud-Umgebung.
- REQ#6: Als Compliance-Verantwortliche:r möchte ich nicht, dass ein Download-Link später erneut verwendet werden kann.
- REQ#7: Als DevOps-Engineer möchte ich, dass alle Zugangsdaten regelmäßig automatisch rotiert werden.
- REQ#8: Als Softwarearchitekt:in möchte ich die Verwaltung von Downloads in einem eigenen, separat deploybaren Modul oder Microservice haben.
Da kommt einiges zusammen. REQ#2 und REQ#3 stehen zum Beispiel in direktem Widerspruch zu REQ#4. Eine mögliche Lösung sind Einmal-Links (One-Time Links, OTL).
Einmal-Links
Ein Einmal-Link lässt sich per Definition nur einmal verwenden. Er startet einen Download und verliert nach der ersten Nutzung seine Gültigkeit.
Bevor ein OTL erzeugt wird, wird ein zufälliger Marker wie eine GUID gespeichert, zum Beispiel in einer Datenbanktabelle oder einem gemeinsamen Cache. Beim ersten Klick prüft der Service, ob der Marker noch existiert. Falls ja, wird er gelöscht und die Datei ausgeliefert. Falls nicht, bekommt die Person eine Fehlermeldung.
Setze beim Lesen des Markers eine Sperre. Sonst können parallele Anfragen dazu führen, dass dieselbe Datei zweimal heruntergeladen wird.

Frag dich vorher: Verlangen deine Anforderungen wirklich, dass eine Datei nur ein einziges Mal über einen Link abrufbar ist? Zugangsdaten oder Zertifikatsdateien müssen unter Umständen genau einmal erzeugt und heruntergeladen werden. In den meisten anderen Fällen reicht es, wenn der Link etwas später nicht mehr funktioniert.
Pseudo-Einmal-Links
In der Praxis werden auch Links, die nur kurze Zeit gültig sind, als Einmal-Links bezeichnet. Wir nennen sie hier Pseudo-OTLs.
Anders als OTLs brauchen Pseudo-OTLs keinen gespeicherten Marker. Stattdessen enthält der Link ein Ablaufdatum und eine Signatur. Das Ablaufdatum ergibt sich aus der aktuellen Zeit plus der Gültigkeitsdauer des Links (TTL, Time to Live). Die Signatur wird mit einem gemeinsamen Geheimnis (einem Pre-Shared Key oder einem Zertifikat) erzeugt und geprüft. Sie stellt sicher, dass niemand das Ablaufdatum manipuliert hat.
Mit einer kurzen TTL lässt sich die Datei nur in einem sehr begrenzten Zeitfenster herunterladen. Innerhalb dieses Fensters kann der Link technisch aber weiterhin mehrfach geklickt und weitergegeben werden.
In AWS S3 und MinIO gibt es Pseudo-OTLs als Presigned URLs, in Azure Blob Storage erreichst du dasselbe mit SAS-Tokens. Setzt du sie selbst um, etwa weil die Dateien lokal liegen, sieht der Ablauf so aus:

Als Pseudocode:
// `keyStore` ist eine Fassade für deinen Schlüssel- oder Zertifikatsspeicher, z. B. Vault
createSignedUrl(fileId, expiresAt):
key = keyStore.newestActiveKey()
url = "/files/" + fileId + "?expires_at=" + expiresAt.toIso8601()
return url + "&key=" + key.id + "&signature=" + sign(url, key)
sign(value, key):
// HMAC-SHA256 mit Pre-Shared Key oder RSA/ECDSA mit Zertifikat
return hmacSha256(value, key.secret)
isValid(request):
expiresAt = request.param("expires_at")
signature = request.param("signature")
key = keyStore.findActive(request.param("key"))
if expiresAt fehlt or signature fehlt or key fehlt:
return false
// nach dem Ablaufdatum darf der Link nicht mehr funktionieren
if now() > parseIso8601(expiresAt):
return false
url = "/files/" + request.pathSegment + "?expires_at=" + expiresAt
// in konstanter Zeit vergleichen, um Timing-Angriffe zu vermeiden
return constantTimeEquals(sign(url, key), signature)
Was du beachten solltest
Zeitzonen
Achte darauf, dass alle Server dieselbe Zeitzone verwenden oder sauber mit Zeitzonen umgehen. Wird der Link in Zeitzone A erzeugt, in Zeitzone B (die A voraus ist) geprüft und enthält das Ablaufdatum keine Zeitzoneninformation, schlägt die Prüfung fehl. Übertrage deshalb immer die Zeitzone, am besten als ISO-8601-Zeitstempel in UTC, oder konfiguriere alle Umgebungen identisch.
Gemeinsame Geheimnisse und Schlüsselrotation
Sowohl der Service, der Links erzeugt, als auch der Service, der Dateien ausliefert, müssen das gemeinsame Geheimnis oder Zertifikat kennen oder dieselbe Signaturkomponente nutzen.
Werden Geheimnisse regelmäßig rotiert (REQ#7), musst du das von Anfang an berücksichtigen: Halte während einer Übergangszeit zwei Schlüssel gleichzeitig gültig. Neue Links werden mit dem neuesten Schlüssel signiert, die Prüfung akzeptiert zusätzlich den vorherigen. Ist die Übergangszeit vorbei, deaktivierst du den alten Schlüssel. Die Übergangszeit muss länger sein als die Gültigkeit eines Links.
Deshalb schickt der Pseudocode oben eine Schlüssel-ID (key) im Link mit. Bei Zertifikaten erfüllt der Fingerprint denselben Zweck. So lädt der prüfende Service direkt den richtigen Schlüssel, statt alle durchzuprobieren.
Was solltest du wählen?
Pseudo-OTLs sind oft die bessere Wahl: Du brauchst keine zusätzliche Persistenz, und eine spätere Migration auf einen Object Storage wie S3, Azure Blob Storage oder ein selbst betriebenes MinIO ist einfacher. Echte OTLs lohnen den Mehraufwand, wenn eine Datei wirklich nur ein einziges Mal abgerufen werden darf, etwa bei Zugangsdaten oder Zertifikaten.
Am Ende hängt es wie immer von deinen konkreten Anforderungen ab. Wenn du eine zweite Meinung zu deiner Download-Architektur willst, frag uns im Advisory.
Ursprünglich veröffentlicht am 9. September 2025.