Sichere Speicherung von Geheimschlüsseln in Django-Modellen (Fernet-Version)

Sicherheit ist die Verantwortung des Entwicklers – Rohdaten sollten nicht im Klartext in der Datenbank abgelegt werden.

1. Warum ist Verschlüsselung nötig?



  • API‑Keys / Secrets / OAuth‑Tokens sind bei einem einzigen Leck gleichbedeutend mit einem kompletten Durchbruch des Dienstes.
  • Werden sie in settings.py hardcodiert, → Git‑Repository, Deploy‑Artefakte, Entwickler‑Laptops exponieren sie direkt.
  • Auch wenn sie in Umgebungsvariablen liegen, → Werden sie in die Datenbank geschrieben, ist das Datenbank‑Leck gleichbedeutend mit Klartext‑Exposition.

Ein weiterer wichtiger Punkt:

  • Passwörter
  • Der Server muss den Klartext nicht kennen → Hashing (BCrypt, Argon2)
  • API‑Keys / Secrets / Tokens
  • Der Server muss den Klartext wiederverwenden → Verschlüsselung, die entschlüsselt werden kann

Zusammenfassung

  • Sensibler Daten, die wiederverwendet werdenverschlüsselt speichern
  • Daten, die nicht wiederverwendet werden (Passwörter etc.)hashen

2. Was ist Fernet?

cryptography.fernet.Fernet ist eine hoch‑abstrakte API, die alles in einem Schritt bietet.

  • Nutzt intern sichere Algorithmen (AES + HMAC etc.)
  • Zufällige IV, Zeitstempel, Integritätsprüfung (HMAC) werden automatisch gehandhabt
  • Der Entwickler muss nur einen Schlüssel sicher aufbewahren und
  • encrypt(Plaintext) → Token‑String
  • decrypt(Token) → Klartext
  • Aufrufen reicht
from cryptography.fernet import Fernet

key = Fernet.generate_key()      # Schlüssel erzeugen
f = Fernet(key)

token = f.encrypt(b"hello")      # verschlüsseln
plain = f.decrypt(token)         # entschlüsseln

Das reicht, um es in der Praxis zu nutzen.

Was wir beachten müssen:

  1. Schlüsselverwaltung (Umgebungsvariable, Secret Manager etc.)
  2. In Django automatisches encrypt/decrypt beim Schreiben/Lesen

3. Fernet‑Schlüssel erzeugen und Django konfigurieren



3‑1. Fernet‑Schlüssel erzeugen

# Einen Fernet‑Schlüssel erzeugen (Base64‑String)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Beispielausgabe:

twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=

Speichere diesen String in einer Umgebungsvariable.

export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="

3‑2. Fernet‑Instanz in settings.py vorbereiten

# settings.py
import os
from cryptography.fernet import Fernet

DJANGO_FERNET_KEY = os.environ["DJANGO_FERNET_KEY"]  # String
FERNET = Fernet(DJANGO_FERNET_KEY.encode("utf-8"))

Jetzt kann überall in Django from django.conf import settings und settings.FERNET.encrypt(...), settings.FERNET.decrypt(...) verwendet werden.


4. Fernet‑verschlüsselte Felder in Django‑Modellen

Anstatt komplexe Block‑Verschlüsselung zu implementieren, reicht ein einfaches, benutzerdefiniertes Feld.

4‑1. Design‑Punkte

  • In der Datenbank wird nur der verschlüsselte Token‑String gespeichert
  • Im Code arbeitet man immer mit dem entschlüsselten Klartext
  • Das Muster _secret + secret trennt die DB‑Spalte von der logischen Bedeutung

4‑2. EncryptedTextField implementieren

# myapp/utils/encrypted_field.py
from django.db import models
from django.conf import settings


class EncryptedTextField(models.TextField):
    """Fernet‑basierte verschlüsselte TextField.
    - Speichern: Klartext(str) → Fernet‑Token(str)
    - Laden: Fernet‑Token(str) → Klartext(str)
    """

    description = "Fernet verschlüsseltes Textfeld"

    def get_prep_value(self, value):
        """Python‑Objekt → DB‑Wert"""
        if value is None:
            return None
        if isinstance(value, str):
            if value == "":
                return ""
            token = settings.FERNET.encrypt(value.encode("utf-8"))
            return token.decode("utf-8")
        return value

    def from_db_value(self, value, expression, connection):
        """DB‑Wert → Python‑Objekt"""
        if value is None:
            return None
        try:
            token = value.encode("utf-8")
            decrypted = settings.FERNET.decrypt(token)
            return decrypted.decode("utf-8")
        except Exception:
            return value

    def to_python(self, value):
        """ORM‑Python‑Objekt"""
        return value

Praxis‑Tipp

  • Falls alte Daten noch im Klartext liegen, kann from_db_value bei einem Decrypt‑Fehler den Klartext zurückgeben und später beim Speichern automatisch verschlüsselt werden.

5. Im Modell einsetzen

# myapp/models.py
from django.db import models
from .utils.encrypted_field import EncryptedTextField


class MyModel(models.Model):
    name = models.CharField(max_length=100)
    _secret = EncryptedTextField()   # DB‑Spalte (Fernet‑Token)

    @property
    def secret(self):
        """Externe Nutzung – immer Klartext"""
        return self._secret

    @secret.setter
    def secret(self, value: str):
        """Zuweisung – automatisch verschlüsselt"""
        self._secret = value
  • _secrettatsächliche DB‑Spalte
  • secretlogisches Feld im Code, immer Klartext

So bleibt die Trennung klar, und Entwickler sehen sofort, welche Spalte verschlüsselt ist.


6. Beispiel‑Anwendung

6‑1. Django‑Shell

>>> from myapp.models import MyModel

# Objekt erstellen
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key"   # Klartext zuweisen
>>> obj.save()

# In der DB gespeicherte Spalte
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'

# Im Code
>>> obj.secret
'my-very-secret-key'

6‑2. In einer API nutzen

# myapp/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import MyModel


class SecretView(APIView):
    def get(self, request, pk):
        obj = MyModel.objects.get(pk=pk)
        return Response({"secret": obj.secret})

In der Praxis wird obj.secret meist nur intern für Aufrufe verwendet und nicht an den Client weitergegeben.


7. Wichtige Punkte bei Fernet

7‑1. Einschränkungen bei Suche/Filter

Fernet erzeugt bei jedem Aufruf einen anderen Token.

obj1.secret = "abc"
obj2.secret = "abc"

Obwohl beide Klartexte gleich sind, unterscheiden sich die gespeicherten Tokens.

Daher funktionieren folgende Abfragen nicht:

MyModel.objects.filter(_secret="abc")       # Sinnlos
MyModel.objects.filter(_secret=obj._secret) # Nur Token‑Vergleich möglich

Wenn du nach Klartext filtern musst, brauchst du entweder * eine zusätzliche Spalte (Hash, Präfix) oder * die Verschlüsselung überdenken.

7‑2. Schlüsselrotation

Ein einzelner Schlüssel sollte nicht ewig verwendet werden.

  • Start: Nur DJANGO_FERNET_KEY
  • Weiter: OLD_KEY, NEW_KEY
  • Verschlüsseln: nur NEW_KEY
  • Entschlüsseln: beide versuchen
  • Daten werden schrittweise auf NEW_KEY migriert

8. Sicherheits‑Checkliste (Fernet)

Punkt Check
Schlüsselverwaltung DJANGO_FERNET_KEY nicht im Git, sondern aus Secrets‑Manager, Parameter Store oder Vault laden
Zugriffs‑Trennung Nur Produktionsserver erhalten den Schlüssel, Entwickler/Local nutzen andere Schlüssel
Verschlüsselungs‑Ziel Nur wirklich sensible Werte (API‑Keys, Tokens) verschlüsseln, andere als normale Spalten belassen
Logging Keine Klartexte, Tokens oder Schlüssel in Logs, besonders bei DEBUG
Backups Ohne Schlüssel sind Backups nutzlos – stelle sicher, dass Schlüssel gesichert sind
Tests Unit‑Tests für encrypt → decrypt und Migration von Klartextdaten

9. Fazit

  • Niedrig‑level AES‑Implementierungen sind fehleranfällig.
  • Mit Fernet erhältst du:
  • Sichere Algorithmen‑Kombination
  • Automatisches IV, Zeitstempel, Integritätsprüfung
  • Einfache API
  • In Django kannst du mit EncryptedTextField und dem _secret/secret‑Pattern
  • Entwickler arbeiten mit Klartext, die DB speichert immer verschlüsselt.

Schlussfolgerung

  • Alle sensiblen Daten verschlüsseln
  • Komplexe Algorithmen delegieren an bewährte Bibliotheken – so steigt die Sicherheit deiner Django‑Anwendung signifikant.

image