8. Oktober 2026
Das Fehlerbild ist trügerisch
Eine Rails Migration Login kann auf den ersten Blick vollständig gelungen wirken: Die Daten sind da, neue Registrierungen funktionieren, Passwort-Resets laufen an. Trotzdem kann sich kein alter Account anmelden. Die Anwendung meldet lediglich ein falsches Passwort.
Ich prüfe in so einer Situation nicht zuerst die Login-View oder die Session-Konfiguration, sondern den Devise pepper. Ein falscher oder fehlender Pepper ist kein Devise-Bug und auch kein kaputter bcrypt-Hash. Es ist eine Konfiguration, die unbemerkt eine andere Passwortprüfung auslöst.
Was der Devise pepper bei bcrypt verändert
Devise reicht das Passwort nicht unverändert an bcrypt weiter. Der konfigurierte Pepper wird bei der Hash-Berechnung und bei der Prüfung berücksichtigt. Vereinfacht sieht das Prinzip so aus:
bcrypt(Passwort + Pepper)Die tatsächliche Implementierung und Formatierung sind Sache von Devise. Entscheidend ist: Der Pepper muss beim Prüfen exakt derselbe sein wie beim Erzeugen des Hashes.
Angenommen, die migrierte Datenbank enthält Passwort-Hashes aus der bisherigen Anwendung. Fehlt in der neuen Umgebung der frühere Pepper oder ist ein anderer Wert gesetzt, erzeugt Devise aus dem eingegebenen Passwort eine andere Prüfbasis. Devise bcrypt vergleicht dann gegen den vorhandenen Hash und liefert schlicht false.
Das ist erwartetes Verhalten von bcrypt: Ein Hash soll nicht verraten, ob Passwort, Pepper oder eine andere Eingabe falsch war. Deshalb gibt es keine Exception, keinen speziellen Fehlercode und typischerweise auch keinen hilfreichen Logeintrag.
Warum neue Accounts trotzdem funktionieren
Das typische Muster ist besonders verwirrend:
- Ein migrierter Account gibt das korrekte Passwort ein und scheitert.
- Ein neu registrierter Account kann sich direkt anmelden.
- Nach einem Passwort-Reset funktioniert der alte Account eventuell wieder.
Die Erklärung: Neue Passwörter werden mit dem aktuell laufenden Pepper gespeichert. Sie passen deshalb zu genau dieser Konfiguration. Migrierte Hashes wurden dagegen mit dem früheren Pepper erzeugt und bleiben dauerhaft unbrauchbar, solange die Konfiguration abweicht.
Ein Passwort-Reset kann das Problem verdecken, weil er den vorhandenen Hash überschreibt. Für eine Migration ist das aber keine Ursachenanalyse, sondern höchstens ein Ausweichweg für einzelne Accounts.
Die Konfiguration muss beim Boot scheitern
Der entscheidende Fix ist klein. Sicherheitsrelevante Konfiguration sollte nicht optional gelesen werden.
Problematisch ist diese Variante:
Devise.setup do |config|
config.pepper = ENV["DEVISE_PEPPER"]
endFehlt die Umgebungsvariable, wird nil gesetzt. Die Anwendung kann trotzdem starten. Erst beim Login tritt der Schaden auf.
Besser ist eine verpflichtende Konfiguration:
Devise.setup do |config|
config.pepper = ENV.fetch("DEVISE_PEPPER")
endMit Rails ENV.fetch beendet ein fehlender Wert den Boot. Das ist genau die gewünschte Eigenschaft: Ein Deployment mit unvollständiger Sicherheitskonfiguration darf nicht erfolgreich wirken.
Wichtig ist dabei nicht nur, dass DEVISE_PEPPER existiert. Bei einer Übernahme oder Migration muss sein Wert zu den bereits gespeicherten Hashes passen. Den Pepper selbst sollte man weder in Logs ausgeben noch in das Repository schreiben.
Den Pepper-Wechsel als Test festhalten
Ich würde dieses Verhalten explizit testen. Der Test dokumentiert zugleich eine oft übersehene fachliche Abhängigkeit: Bestehende Passwort-Hashes hängen am Pepper.
RSpec.describe User do
it "akzeptiert bestehende Passwort-Hashes nicht mit anderem Pepper" do
original_pepper = Devise.pepper
user = described_class.create!(
email: "[email protected]",
password: "ein-test-passwort"
)
expect(user.valid_password?("ein-test-passwort")).to be(true)
Devise.pepper = "anderer-test-pepper"
expect(user.valid_password?("ein-test-passwort")).to be(false)
ensure
Devise.pepper = original_pepper
end
endDer konkrete Factory- oder Modellaufbau kann je nach Codebase abweichen. Entscheidend sind die zwei Aussagen: Mit unverändertem Pepper passt der Hash; nach dem Wechsel passt derselbe Hash nicht mehr.
Zweiter Fallstrick: ein leerer Default für encrypted_password
Ein verwandtes Problem entsteht durch ein Datenbank-Default wie default: '' für encrypted_password. Damit lassen sich Datensätze anlegen, obwohl kein gültiger Passwort-Hash vorhanden ist. Sie sind formal vollständig genug für die Datenbank, aber nie anmeldbar.
Für Passwort-Hashes ist ein leerer String kein sinnvoller Ersatz für einen fehlenden Wert. Bei Migrationen sollte daher geprüft werden, ob vorhandene Datensätze tatsächlich bcrypt-Hashes enthalten und ob Accounts ohne Passwort bewusst behandelt werden, etwa über einen vorgesehenen Einladungs- oder Reset-Prozess.
Prüft, ob eure sicherheitsrelevante Konfiguration beim Boot scheitert – oder erst beim User.