Almacenar claves secretas de forma segura en modelos de Django (versión Fernet)
La seguridad es responsabilidad del desarrollador – no se debe guardar el texto plano en la base de datos.
1. ¿Por qué se necesita cifrado?
- API Key / Secret / OAuth token son valores que, una vez filtrados, equivalen a una brecha total del servicio.
- Si se hardcodean en
settings.py, pueden quedar expuestos en el repositorio Git, artefactos de despliegue o en la máquina del desarrollador. - Incluso si se guardan en variables de entorno, al almacenarlos tal cual en la BD, el momento en que la BD se vea comprometida, el texto plano queda expuesto.
Otro punto clave:
- Contraseñas
- El servidor no necesita conocer el texto plano → hash (BCrypt, Argon2)
- API Key / Secret / token
- El servidor necesita usar el texto plano → cifrado reversible
Resumen
- Datos sensibles que se vuelven a usar → cifrar antes de guardar
- Datos que no se vuelven a usar (contraseñas, etc.) → hashear antes de guardar
2. ¿Qué es Fernet?
cryptography.fernet.Fernet es una API de alto nivel que proporciona todo de una vez.
- Usa algoritmos seguros internamente (AES + HMAC, etc.)
- Gestiona IV aleatorio, timestamp y verificación de integridad (HMAC) automáticamente
- El desarrollador solo necesita mantener una clave segura y
encrypt(plaintext)→ token stringdecrypt(token)→ plaintext
Ejemplo rápido:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # generar clave
f = Fernet(key)
token = f.encrypt(b"hello") # cifrar
plain = f.decrypt(token) # descifrar
Con esto basta para usarlo.
Lo que debemos cuidar:
- Gestión de claves (variables de entorno, Secret Manager, etc.)
- Hacer que Django cifre/descifre automáticamente al leer/escribir
3. Generar clave Fernet y configurar Django
3-1. Generar clave Fernet
# Generar una clave Fernet (se muestra como cadena Base64)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Ejemplo de salida:
twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=
Coloca esta cadena en una variable de entorno del servidor.
export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="
3-2. Preparar instancia Fernet en settings.py
# settings.py
import os
from cryptography.fernet import Fernet
DJANGO_FERNET_KEY = os.environ["DJANGO_FERNET_KEY"] # cadena
FERNET = Fernet(DJANGO_FERNET_KEY.encode("utf-8"))
Ahora en cualquier parte de Django puedes hacer:
from django.conf import settings
settings.FERNET.encrypt(...)
settings.FERNET.decrypt(...)
4. Añadir campo cifrado Fernet a un modelo de Django
En lugar de implementar un cifrado complejo, basta con un solo campo personalizado.
4-1. Puntos de diseño
- En la BD solo se guarda el token cifrado
- En el código siempre se trabaja con el texto plano descifrado
- Se separa la columna real (
_secret) del significado lógico (secret)
4-2. Implementación de EncryptedTextField
# myapp/utils/encrypted_field.py
from django.db import models
from django.conf import settings
class EncryptedTextField(models.TextField):
"""Campo de texto cifrado con Fernet."""
description = "Fernet encrypted text field"
def get_prep_value(self, value):
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):
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
Consejo práctico
Si la BD ya contiene datos en texto plano,
from_db_valuepuede devolver el valor tal cual y, con cada lectura, volver a guardarlo cifrado.
5. Aplicar el campo al modelo
# 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() # columna real (token Fernet)
@property
def secret(self):
"""Acceso en texto plano."""
return self._secret
@secret.setter
def secret(self, value: str):
self._secret = value
_secret→ nombre real de la columna en la BD (token cifrado)secret→ campo lógico que siempre devuelve el texto plano
Con este patrón, quien vea el modelo puede distinguir claramente:
obj.secret # texto plano
obj._secret # token cifrado
6. Ejemplos de uso
6-1. Django shell
>>> from myapp.models import MyModel
# Crear objeto
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key" # asignar texto plano
>>> obj.save()
# En la BD se guarda el token cifrado
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'
# En el modelo se ve el texto plano
>>> obj.secret
'my-very-secret-key'
6-2. Uso en una API
# 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})
En producción, normalmente se no expone el secret al cliente, sino que se usa internamente.
7. Cosas a tener en cuenta con Fernet
7-1. Limitaciones de búsqueda/filtrado
Fernet produce un token diferente cada vez, incluso con el mismo texto plano.
obj1.secret = "abc"
obj2.secret = "abc"
Aunque ambos son "abc", los tokens en _secret son totalmente distintos.
Por eso, consultas como:
MyModel.objects.filter(_secret="abc")
MyModel.objects.filter(_secret=obj._secret)
no funcionan como se esperaría. Si necesitas buscar por el valor en texto plano, considera:
- una columna de búsqueda (hash, prefijo, etc.)
- o re-evaluar si ese campo debe cifrarse.
7-2. Rotación de claves
Es recomendable no usar la misma clave indefinidamente.
- Inicio sencillo: solo
DJANGO_FERNET_KEY - Avanzado: mantener
OLD_KEYyNEW_KEY - cifrar con
NEW_KEY - descifrar intentando con ambas
- gradualmente migrar datos a
NEW_KEY
8. Checklist de seguridad (versión Fernet)
| Elemento | Verificación |
|---|---|
| Gestión de claves | No subir DJANGO_FERNET_KEY a Git; usar Secrets Manager, Parameter Store, Vault, etc. |
| Separación de permisos | Solo el servidor de producción recibe la clave; entornos de desarrollo usan claves distintas |
| Objetos cifrados | Solo cifrar API Key, Secret, token; el resto permanece en columnas normales |
| Logs | Nunca registrar texto plano, token ni clave (especialmente en DEBUG) |
| Backups | Los backups son inútiles sin la clave; asegúrate de proteger la clave |
| Pruebas | Verificar que encrypt -> decrypt sea consistente y que migraciones de datos en texto plano funcionen |
9. Conclusión
Implementar cifrado de bajo nivel con AES es complejo y propenso a errores. Con Fernet obtienes:
- Algoritmos seguros combinados
- IV aleatorio, timestamp y verificación de integridad
- API sencilla
En Django, usar un campo personalizado como EncryptedTextField junto con la convención _secret / secret permite que el código trabaje siempre con texto plano, mientras la BD almacena únicamente valores cifrados.
Resumen
- Datos sensibles = siempre cifrar
- Algoritmos complejos = delegar a bibliotecas Así, la seguridad de tu servicio Django mejora significativamente. 🔐

No hay comentarios.