Gefangen in der AWS-Teergrube
Während ich eine Reihe von Unit-Tests durchführte, die API-Aufrufe an Amazon Web Services (AWS) richteten, bemerkte ich etwas Seltsames: Tests schlugen unvorhersehbar fehl. Manchmal bestanden alle Tests, dann schlugen beim nächsten Durchlauf einige fehl, und in der darauffolgenden Zeit schlug ein anderer Satz fehl.
Die Fehler, die ich erhielt, schienen keinen Sinn zu ergeben:
Aws::RDS::Errors::SignatureDoesNotMatch: Die von uns berechnete Anforderungssignatur stimmt nicht mit der von Ihnen bereitgestellten Signatur überein. Überprüfen Sie Ihren geheimen AWS-Zugriffsschlüssel und Ihre Signaturmethode. Weitere Informationen finden Sie in der Servicedokumentation.
Aws::Lambda::Errors::InvalidSignatureException: Die von uns berechnete Anforderungssignatur stimmt nicht mit der von Ihnen bereitgestellten Signatur überein. Überprüfen Sie Ihren geheimen AWS-Zugriffsschlüssel und Ihre Signaturmethode. Weitere Informationen finden Sie in der Servicedokumentation.
Diese Ausnahmen schienen darauf hinzudeuten, dass die Tests falsche Anmeldeinformationen verwendeten, aber ich wusste, dass das nicht stimmte. Ich hatte diese Anmeldeinformationen in der Vergangenheit viele Male verwendet, und sie hatten jedes Mal funktioniert. Darüber hinaus wurde jeder dieser unvorhersehbaren Komponententests mindestens einmal bestanden, sodass die Anmeldeinformationen gültig gewesen sein müssen. Warum habe ich dann diese Fehler erhalten?
Bei diesen Tests wurde eine große Anzahl von Anfragen gestellt. Könnte es sich um eine Ratenbegrenzung handeln (mit ausgesprochen wenig hilfreichen Fehlermeldungen)? Dies schien eine vernünftige Ursache zu sein, aber ich war nicht in der Lage, das Problem zu reproduzieren, selbst bei einem höheren Volumen an Anfragen als bei den Tests.
Vielleicht waren die Anmeldeinformationen manchmal wirklich ungültig. Zur Plausibilitätsprüfung habe ich die Tests so geändert, dass die Anmeldeinformationen ausgedruckt werden, die für jeden API-Aufruf verwendet werden. War dies einfach ein Fall von falschem, unvorhersehbarem Code?
Nein. Kein Glück. Die Anmeldeinformationen waren in der Tat gültig, jedes Mal.
Naja, mit Ausnahme des einen Unit-Tests, bei dem ungültige Anmeldeinformationen getestet wurden.
Abwarten. Die fehlgeschlagenen Tests wurden immer nach dem Test ausgeführt, bei dem ungültige Anmeldeinformationen getestet wurden. Was passiert, wenn ich diesen Test deaktiviere?
Die restlichen Tests haben alle zuverlässig bestanden. Was war hier los? Ich habe mit dem AWS Ruby SDK herumgespielt und versucht, das Problem zu reproduzieren, als mir etwas Interessantes aufgefallen ist.
Ich konnte das Problem reproduzieren, indem ich eine Reihe von Anfragen mit ungültigen Anmeldeinformationen sendete, unmittelbar gefolgt von einer Anfrage mit gültigen Anmeldeinformationen:
ruby
# aws_test.rb
erfordert "aws-sdk"
ACCESS_KEY=""
SECRET_KEY=""
def make_request(secret_key)
Aws::EC2::Client.new(
region: "us-east-1",
access_key_id: ACCESS_KEY,
secret_access_key: secret_key
).describe_instances
"Erfolg"
rescue StandardError => e
"Fehler: #{e.class.name}"
end
(30).times { make_request("invalid_key") }
puts "Verwenden gültiger Anmeldeinformationen:"
setzt make_request(SECRET_KEY)
$ruby aws_test.rb
Verwenden gültiger Anmeldeinformationen:
Fehler: Aws::EC2::Errors::AuthFailure
Nun, das Problem war auf jeden Fall real. Interessant ist mir, dass der erste API-Aufruf an AWS in der Regel etwa eine halbe Sekunde dauert, die nachfolgenden jedoch erheblich schneller sind. Wenn ich jedoch etwa fünf Sekunden wartete, war die nächste Anfrage wieder (vergleichsweise) langsam.
Dies schien mir auf eine Art Caching-Problem hinzudeuten. Ich habe mich durch den Code für das AWS Ruby SDK gewühlt, aber nichts gefunden, was mit dem Caching von Antworten zu tun hat. Der Unterschied in der Anforderungsgeschwindigkeit wurde durch Verbindungspooling verursacht - das AWS Ruby SDK hält HTTP-Verbindungen standardmäßig fünf Sekunden lang aktiv. Das Deaktivieren des Verbindungspoolings hat das Problem nicht beseitigt, und ich habe sogar ein Skript geschrieben, um HTTP-Anfragen manuell zu erstellen und zu senden. Dennoch blieb das Problem bestehen.
Als ich unseren CTO darauf ansprach, hatte er sofort eine Hypothese für das, was vor sich ging: AWS hat mich unter Druck gesetzt. Der Dienst lehnte absichtlich sogar gültige Anmeldeinformationen ab, wenn zu viele fehlgeschlagene Authentifizierungsversuche vorausgegangen waren, um Brute-Force-Angriffe zu verhindern. Das passte zu den Fakten, die wir sahen, und war die einfachste Erklärung, die wir uns vorstellen konnten.
Die Lösung? Ein einfacher Wiederholungsalgorithmus mit exponentiellem Backoff. Aber das ist keine ideale Lösung. Da es keine Möglichkeit gibt, eine Antwort auf eine Anforderung mit ungültigen Anmeldeinformationen von einer Antwort auf eine blockierte Anforderung zu unterscheiden, muss der Wiederholungsversuch auf beide Szenarien angewendet werden, was bedeutet, dass legitim ungültige Anmeldeinformationen zu erheblichen Verzögerungen bei der Authentifizierung führen: Wie können wir feststellen, ob die Anmeldeinformationen tatsächlich ungültig waren? Vielleicht werden wir nur unter Druck gesetzt, so dass wir keine andere Wahl haben, als zu warten, bis die maximale Anzahl von Wiederholungen überschritten ist.
Alles, was wirklich getan werden kann, ist, den Wiederholungsalgorithmus zu ändern, je nachdem, wann und wo die API-Aufrufe stattfinden. Bei Aufgaben, die im Hintergrund ausgeführt werden, ist es in der Regel in Ordnung, eine große Anzahl von Wiederholungen und lange Verzögerungen zu haben. Wenn Sie jedoch eine Anforderung von einem Benutzer verarbeiten, müssen Sie nur wenige Wiederholungen verwenden und mit der Möglichkeit leben, dass dem Benutzer ein falscher Fehler über ungültige Anmeldeinformationen angezeigt wird und er es erneut versuchen muss.
Sollte ich diese Art von Teergrube in meinem Service implementieren?
Nein.
Dies ist nicht der richtige Weg, um Dinge zu tun, und dies ist nicht der richtige Zeitpunkt™, um in die Fußstapfen von Amazon zu treten. Die Tatsache, dass legitim ungültige Anmeldeinformationen nicht von Ratenbegrenzung oder Teergrube unterschieden werden können, ist ein ernsthaftes Problem für die Benutzerfreundlichkeit. Hier sind einige Möglichkeiten, wie AWS dieses Problem beheben und gleichzeitig Brute-Force-Angriffe verhindern könnte:
- Gibt eine andere Fehlermeldung zurück, wenn zu viele Authentifizierungsversuche unternommen wurden.
- Führen Sie eine Netzwerkverzögerung ein, wenn zu viele Authentifizierungsversuche unternommen wurden.
- Fordern Sie Aufrufer auf, Anforderungssignaturen mit einer Schlüsselableitungsfunktion zu verarbeiten (obwohl dies dazu führt, dass alle Anforderungen verlangsamt und die CPU-Auslastung erhöht wird).
- Verwenden Sie geheime Schlüssel lange genug, damit Brute Force rechnerisch nicht lösbar ist, und es gibt keine Verzögerungen oder Teergruben (nur die normale Ratenbegrenzung, um die Serverlast zu reduzieren). Denken Sie daran, dass diese geheimen Schlüssel automatisch generiert werden - es gibt keine Möglichkeit, dass Benutzer schwache Passwörter wählen.
Oder, die beste Lösung: Verwenden Sie keine HMACs zur Authentifizierung! Wenn Sie HMACs für die Authentifizierung verwenden, speichern Sie die geheimen Schlüssel notwendigerweise im Klartext. Haben wir nicht schon gelernt, dass das Speichern von Klartext-Passwörtern eine schlechte Idee ist?
>Generieren Sie stattdessen ein asymmetrisches Schlüsselpaar, geben Sie den privaten Schlüssel an den Benutzer weiter, und speichern Sie nur den öffentlichen Schlüssel auf dem Server. Unter der Annahme, dass Sie sichere Parameter verwendet haben, um die Schlüssel zu generieren (Sie haben keinen Exponenten von 3 verwendet, oder?) und Sie verwenden ein sicheres Signaturschema (vermeiden wir vielleicht Lehrbuch-RSA), kann der Benutzer Anfragen sicher mit diesem Schlüssel signieren, mit der Gewissheit, dass es rechnerisch unlösbar ist, Signaturen zu fälschen (zumindest bis wir Quantencomputer haben...).