Stocker en toute sécurité les clés secrètes dans un modèle Django (version Fernet)

La sécurité est la responsabilité du développeur – il ne faut pas stocker les valeurs en clair dans la base de données.

1. Pourquoi le chiffrement est-il nécessaire ?



  • API Key / Secret / OAuth token : une fuite unique équivaut à ouvrir la porte à l’ensemble du service.
  • Si vous les hardcodez dans settings.py, ils peuvent être exposés dans le dépôt Git, les artefacts de déploiement ou sur votre machine de développement.
  • Même si vous les placez dans des variables d’environnement, les enregistrer tel quel dans la base de données signifie que dès que la BDD est compromise, les secrets apparaissent en clair.

Un point important à retenir :

  • Mot de passe
  • Le serveur n’a pas besoin de connaître le texte en clair → hash (BCrypt, Argon2)
  • API Key / Secret / Token
  • Le serveur doit pouvoir les réutiliser → chiffrement déchiffrable

Résumé

  • Données sensibles à réutiliserchiffrer avant de stocker
  • Données à ne pas réutiliser (mot de passe, etc.)hasher avant de stocker

2. Qu’est‑ce que Fernet ?

cryptography.fernet.Fernet est une API de haut niveau qui regroupe tout en une seule fois.

  • Utilise des algorithmes sûrs en interne (AES + HMAC, etc.)
  • Gère automatiquement IV aléatoire, horodatage, vérification d’intégrité (HMAC)
  • Le développeur n’a qu’à conserver une clé
  • encrypt(texte) → chaîne de token
  • decrypt(token) → texte clair

En d’autres termes, vous n’avez pas à vous soucier d’AES‑CBC/IV/padding/HMAC :

from cryptography.fernet import Fernet

key = Fernet.generate_key()      # Génération de la clé
f = Fernet(key)

token = f.encrypt(b"hello")      # Chiffrement
plain = f.decrypt(token)         # Déchiffrement

Comprendre ces quelques lignes suffit pour l’utiliser.

Ce qu’il faut retenir

  1. Gestion de la clé (variables d’environnement, Secret Manager, etc.)
  2. Faire en sorte que Django chiffre/déchiffre automatiquement lors des écritures/lectures

3. Génération de la clé Fernet et configuration Django



3‑1. Générer la clé Fernet

# Générer une clé Fernet (sous forme de chaîne Base64)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Exemple de sortie :

twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=

Placez cette chaîne dans une variable d’environnement de votre serveur.

export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="

3‑2. Préparer une instance Fernet dans settings.py

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

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

Vous pouvez désormais appeler settings.FERNET.encrypt(...) ou settings.FERNET.decrypt(...) depuis n’importe quel module Django.


4. Ajouter un champ chiffré Fernet à un modèle Django

Au lieu d’implémenter un chiffrement complexe, il suffit de créer un champ personnalisé très simple.

4‑1. Points de conception

  • La base de données ne stocke que le token chiffré
  • Le code modèle, vue ou service ne voit que le texte clair
  • Le motif _secret + secret sépare la colonne de base de données de la logique métier

4‑2. Implémentation de EncryptedTextField

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


class EncryptedTextField(models.TextField):
    """Champ TextField chiffré basé sur Fernet."""

    description = "Fernet encrypted text field"

    def get_prep_value(self, value):
        """Python -> valeur stockée en BDD."""
        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):
        """Valeur BDD -> Python."""
        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):
        return value

Astuce de production

Si votre base contient déjà des données en clair, vous pouvez laisser from_db_value renvoyer la valeur brute en cas d’échec de déchiffrement, puis les réécrire chiffrées lors de la prochaine sauvegarde.


5. Appliquer le champ au modèle

# 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()   # Colonne réelle (token Fernet)

    @property
    def secret(self):
        """Accès en clair depuis l’extérieur."""
        return self._secret

    @secret.setter
    def secret(self, value: str):
        """Assigne un texte clair, qui sera chiffré automatiquement."""
        self._secret = value
  • _secret : nom réel de la colonne BDD, contenant le token chiffré.
  • secret : champ logique utilisé dans le code, toujours en clair.

Ainsi, un développeur peut faire :

obj.secret  # texte clair
obj._secret # valeur chiffrée stockée

6. Exemples d’utilisation

6‑1. Dans la console Django

>>> from myapp.models import MyModel

# Créer un nouvel objet
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key"   # assignation en clair
>>> obj.save()

# La colonne réelle contient un token chiffré
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'

# Accès en clair via le modèle
>>> obj.secret
'my-very-secret-key'

6‑2. Dans une API

En pratique, on ne renvoie pas le secret au client, mais voici un exemple simplifié :

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

Dans un environnement réel, on utiliserait obj.secret uniquement pour appeler d’autres services internes, sans jamais l’exposer au client.


7. Points à connaître lorsqu’on utilise Fernet

7‑1. Recherche / filtrage

Fernet produit un résultat différent à chaque chiffrement. Ainsi, même si deux secrets sont identiques, leurs tokens chiffrés seront différents.

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

Les requêtes suivantes ne fonctionneront pas :

MyModel.objects.filter(_secret="abc")       # sans sens
MyModel.objects.filter(_secret=obj._secret) # comparaison de tokens, pas de texte clair

Si vous avez besoin de rechercher ou trier par texte clair, envisagez :

  • une colonne de recherche (hash, préfixe, etc.)
  • ou repensez la conception pour éviter de chiffrer ces champs.

7‑2. Rotation de clé

Il est préférable de ne pas garder la même clé indéfiniment.

  • Commencez avec une seule clé DJANGO_FERNET_KEY.
  • Plus tard, introduisez OLD_KEY et NEW_KEY :
  • Chiffrement : toujours avec NEW_KEY.
  • Déchiffrement : essayer OLD_KEY puis NEW_KEY.
  • Au fil du temps, les données seront ré‑chiffrées avec NEW_KEY et OLD_KEY pourra être supprimée.

8. Checklist de sécurité (version Fernet)

Élément Vérification
Gestion de la clé Ne pas versionner DJANGO_FERNET_KEY; charger depuis Secrets Manager, Parameter Store, Vault, etc.
Séparation des environnements Injecter la clé uniquement sur les serveurs de production; utiliser une clé différente en dev/local
Chiffrement ciblé Chiffrer uniquement les valeurs réellement sensibles (API Key, Secret, Token) ; laisser le reste en clair
Logs Ne jamais enregistrer le texte clair, le token ou la clé dans les logs (surtout en DEBUG)
Sauvegardes Même si la BDD est sauvegardée, sans la clé les données restent inutilisables
Tests Vérifier que encrypt -> decrypt donne le même résultat ; tester la migration de données en clair

9. Conclusion

Implémenter un chiffrement bas niveau est à la fois complexe et source d’erreurs. En utilisant Fernet, vous bénéficiez :

  • d’une combinaison d’algorithmes sécurisés
  • d’un IV aléatoire, d’un horodatage et d’une vérification d’intégrité
  • d’une API simple

Dans Django, un champ personnalisé comme EncryptedTextField couplé au motif _secret / secret permet aux développeurs de travailler uniquement avec du texte clair, tout en garantissant que la base de données ne contient que des valeurs chiffrées.

En résumé

  • Tout secret sensible doit être chiffré avant d’être stocké.
  • Laissez la bibliothèque gérer le chiffrement. En suivant ces deux principes, la sécurité de votre service Django s’améliorera considérablement. 🔐

image