Django-modellen veilig geheimen opslaan met Fernet

Beveiliging is de verantwoordelijkheid van de ontwikkelaar – het is niet toegestaan om gevoelige gegevens in platte tekst in de database op te slaan.

1. Waarom is versleuteling nodig?



  • API‑sleutels / geheimen / OAuth‑tokens zijn, zodra ze lekken, gelijk aan een open deur voor de hele dienst.
  • Als je ze hard‑codeert in settings.py, kunnen ze in een Git‑repo, een deployment‑artifact of een ontwikkelaars‑laptop worden blootgesteld.
  • Zelfs als je ze in een omgevingsvariabele opslaat, leidt het opslaan van die waarde in de database tot een directe blootstelling in platte tekst zodra de database wordt gecompromitteerd.

Een ander belangrijk punt:

  • Wachtwoorden
  • De server hoeft de oorspronkelijke tekst niet te kennen → gebruik een hash (BCrypt, Argon2).
  • API‑sleutels / geheimen / tokens
  • De server moet de oorspronkelijke tekst opnieuw gebruiken → gebruik een versleutelbare encryptie.

Samenvatting

  • Gevoelige gegevens die opnieuw gebruikt moeten wordenversleutelen en opslaan
  • Gegevens die niet opnieuw gebruikt hoeven te worden (zoals wachtwoorden) → hashen en opslaan

2. Wat is Fernet?

cryptography.fernet.Fernet is een hoog‑niveau API die alles in één keer levert:

  • Intern gebruikt het een veilige algoritmecombinatie (AES + HMAC, etc.).
  • Het verzorgt een willekeurige IV, een timestamp en een integriteitscontrole (HMAC).
  • De ontwikkelaar hoeft alleen een sleutel veilig op te slaan en kan dan:
  • encrypt(plain_text) → token‑string
  • decrypt(token) → plain_text
from cryptography.fernet import Fernet

key = Fernet.generate_key()      # sleutel genereren
f = Fernet(key)

token = f.encrypt(b"hello")      # versleutelen
plain = f.decrypt(token)         # ontsleutelen

Met deze basiskennis kun je al veel doen.

Wat we moeten doen:

  1. Sleutelbeheer (omgevingsvariabele, Secret Manager, etc.)
  2. In Django automatisch encrypten/decrypten bij schrijven/lees.

3. Fernet‑sleutel genereren en Django instellen



3‑1. Fernet‑sleutel genereren

# Genereer één Fernet‑sleutel (Base64‑string)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Voorbeeldoutput:

twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=

Plaats deze string in een omgevingsvariabele op de server.

export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="

3‑2. settings.py voorbereiden

# 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"))

Nu kun je overal in Django from django.conf import settings gebruiken en settings.FERNET.encrypt(...) of settings.FERNET.decrypt(...) aanroepen.


4. Een Fernet‑versleuteld veld toevoegen aan een Django‑model

In plaats van een ingewikkelde blok‑encryptie kun je een eenvoudig aangepast veld maken.

4‑1. Ontwerp‑punten

  • In de database sla je alleen de versleutelde token‑string op.
  • In het model, de view of de service zie je altijd de ontsleutelde plain text.
  • Gebruik een _secret + secret patroon om de database‑kolom en de werkelijke betekenis te scheiden.

4‑2. EncryptedTextField implementatie

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


class EncryptedTextField(models.TextField):
    """
    Fernet‑gebaseerd versleuteld TextField
    - Opslaan: plain text (str) → Fernet‑token (str)
    - Ophalen: Fernet‑token (str) → plain text (str)
    """

    description = "Fernet encrypted text field"

    def get_prep_value(self, value):
        """Python object → waarde die in de DB wordt opgeslagen"""
        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‑waarde → Python object"""
        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‑object"""
        return value

Praktische tip

  • Als er in het begin al platte tekst in de DB staat, kun je in from_db_value een fallback laten teruggeven en geleidelijk de waarden opnieuw opslaan als versleuteld.

5. Het veld in een model gebruiken

# 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()   # echte DB‑kolom (Fernet‑token)

    @property
    def secret(self):
        """Toegang tot de plain text"""
        return self._secret

    @secret.setter
    def secret(self, value: str):
        """Automatisch versleutelen bij opslaan"""
        self._secret = value
  • _secret is de echte kolomnaam in de database.
  • secret is het logische veld dat je in je code gebruikt.

6. Voorbeelden

6‑1. Django shell

>>> from myapp.models import MyModel

# Nieuwe instantie
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key"   # plain text toekennen
>>> obj.save()

# In de DB is de kolom _secret versleuteld
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'

# In het model is het plain text
>>> obj.secret
'my-very-secret-key'

6‑2. In een API gebruiken

In de praktijk geef je de secret meestal niet aan de client, maar hier een voorbeeld:

# 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 de praktijk gebruik je obj.secret vaak alleen voor interne API‑aanroepen en exposeer je het nooit rechtstreeks.


7. Wat je moet weten bij het gebruik van Fernet

7‑1. Beperkingen bij zoeken/filteren

Fernet produceert bij elke encryptie een verschillende output.

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

Beide zijn "abc", maar de opgeslagen _secret‑waarden zijn volledig verschillend.

Daarom werken de volgende queries niet:

MyModel.objects.filter(_secret="abc")       # zinloos
MyModel.objects.filter(_secret=obj._secret) # token vergelijken, geen plain text zoeken

Als je op basis van de plain text wilt zoeken of sorteren, overweeg dan:

  • een extra zoek‑kolom (hash, prefix, etc.)
  • of herontwerp de encryptie‑strategie.

7‑2. Sleutelrotatie

Gebruik niet één sleutel voor altijd. Een goede aanpak is:

  • Begin met één sleutel (DJANGO_FERNET_KEY).
  • Later kun je OLD_KEY en NEW_KEY gebruiken:
  • Versleutelen: alleen met NEW_KEY.
  • Ontsleutelen: probeer eerst met OLD_KEY, dan met NEW_KEY.
  • Naarmate data opnieuw wordt opgeslagen, kun je OLD_KEY verwijderen.

8. Beveiligings‑checklist (Fernet‑versie)

Item Checklist
Sleutelbeheer DJANGO_FERNET_KEY niet in Git, maar via AWS Secrets Manager, Parameter Store, Vault, etc.
Toegangscontrole Sleutel alleen injecteren op productie, andere sleutels voor ontwikkeling/locaal.
Encryptie‑doel Alleen echte gevoelige waarden (API‑sleutels, tokens) versleutelen; overige kolommen onversleuteld.
Logging Nooit plain text, token of sleutel loggen (vooral in DEBUG).
Backups Zonder sleutel is een backup waardeloos; zorg dat backups veilig zijn.
Testing Unit‑tests voor encrypt → decrypt en migratie‑scenario’s.

9. Afsluiting

  • Het implementeren van een lage‑niveau AES‑versleuteling is foutgevoelig en complex.
  • Met Fernet krijg je:
  • Een veilige algoritmecombinatie
  • Willekeurige IV, timestamp en integriteitscontrole
  • Een eenvoudige API

  • In Django kun je met een EncryptedTextField en het _secret/secret‑patroon de database altijd versleuteld houden, terwijl de code alleen met plain text werkt.

Conclusie

  • Gevoelige data = altijd versleuteld opslaan
  • Complexe cryptografie = aan een library overlaten – zo verhoog je de beveiliging van je Django‑applicatie aanzienlijk. 🔐

image