Zum Inhalt springen
Projekt besprechen

8. Oktober 2026

Nach der Migration kann sich kein alter User mehr einloggen: Devise-Pepper prüfen

RailsSecurityMigration

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"]
end

Fehlt 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")
end

Mit 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
end

Der 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.