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 worden → versleutelen 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‑stringdecrypt(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:
- Sleutelbeheer (omgevingsvariabele, Secret Manager, etc.)
- 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+secretpatroon 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_valueeen 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
_secretis de echte kolomnaam in de database.secretis 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_KEYenNEW_KEYgebruiken: - Versleutelen: alleen met
NEW_KEY. - Ontsleutelen: probeer eerst met
OLD_KEY, dan metNEW_KEY. - Naarmate data opnieuw wordt opgeslagen, kun je
OLD_KEYverwijderen.
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
EncryptedTextFielden 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. 🔐

댓글이 없습니다.