Safely Storing Secret Keys in Django Models (Fernet Edition)
Security is a developer’s responsibility – never store raw secrets in the database.
1. Why Encryption Matters
- API keys, secrets, OAuth tokens are critical. If one leaks, the entire service is compromised.
- Hard‑coding them in
settings.pyexposes them in Git, deployment artifacts, and developers’ laptops. - Even if you keep them in environment variables, storing the plain value in the database means the secret is exposed as soon as the DB is compromised.
A key point to remember:
- Passwords – the server never needs the plain text. Use a hash (BCrypt, Argon2).
- API keys, secrets, tokens – the server must use the original value. Use reversible encryption.
Summary
- Data that will be used again → Encrypt before storing
- Data that won’t be reused (e.g., passwords) → Hash before storing
2. What is Fernet?
cryptography.fernet.Fernet is a high‑level API that bundles several security primitives:
- Uses a secure algorithm internally (AES + HMAC, etc.)
- Handles random IV, timestamps, and integrity checks automatically
- The developer only needs to keep a single key
from cryptography.fernet import Fernet
key = Fernet.generate_key() # generate a key
f = Fernet(key)
token = f.encrypt(b"hello") # encrypt
plain = f.decrypt(token) # decrypt
With this understanding, you’re ready to use it.
What we need to manage:
- Key management (environment variables, Secret Manager, etc.)
- Automatic encrypt/decrypt in Django when reading/writing
3. Generating a Fernet Key and Configuring Django
3‑1. Generate a Fernet Key
# Generate one key (Base64 string)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Example output:
twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=
Store this string in a server environment variable.
export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="
3‑2. Prepare a Fernet instance in settings.py
# 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"))
Now anywhere in Django you can do:
from django.conf import settings
settings.FERNET.encrypt(...)
settings.FERNET.decrypt(...)
4. Adding a Fernet‑Encrypted Field to a Django Model
Instead of building a full block‑cipher implementation, a single custom field is enough.
4‑1. Design Considerations
- Store only the encrypted token string in the database.
- The application code always sees the decrypted plain text.
- Use a
_secretcolumn for the raw token and asecretproperty for the logical field.
4‑2. Implementing EncryptedTextField
# myapp/utils/encrypted_field.py
from django.db import models
from django.conf import settings
class EncryptedTextField(models.TextField):
"""Fernet‑based encrypted TextField.
- On save: plain text → Fernet token
- On load: Fernet token → plain text
"""
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
Practical tip
If you have legacy plain text in the DB, let
from_db_valuereturn the plain text on decryption failure and gradually re‑encrypt on subsequent saves.
5. Applying the Field to a Model
# 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() # actual DB column (Fernet token)
@property
def secret(self):
"""Always expose the plain text."""
return self._secret
@secret.setter
def secret(self, value: str):
"""Assign plain text; it will be encrypted on save."""
self._secret = value
_secret– the real DB column storing the encrypted token.secret– the logical field used in code, always plain text.
This pattern makes the intent clear to anyone reading the model.
6. Usage Examples
6‑1. Django Shell
>>> from myapp.models import MyModel
# Create a new instance
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key" # plain text assignment
>>> obj.save()
# The actual DB column contains an encrypted token
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'
# In code, you get the plain text
>>> obj.secret
'my-very-secret-key'
6‑2. API Usage
In practice, you rarely expose the secret to the client. Here’s a minimal example:
# 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 production, you’d typically use obj.secret only for internal calls and never return it to the client.
7. Things to Keep in Mind When Using Fernet
7‑1. Search / Filtering Limitations
Fernet produces a different ciphertext each time, even for the same plaintext.
obj1.secret = "abc"
obj2.secret = "abc"
Both store distinct _secret values, so queries like filter(_secret="abc") are meaningless. If you need to search or sort by the plain value, consider:
- A separate searchable column (hash, prefix, etc.)
- Re‑evaluate whether the field should be encrypted.
7‑2. Key Rotation Strategy
Don’t rely on a single key forever. A simple approach:
- Start with one key (
DJANGO_FERNET_KEY). - Later, introduce
OLD_KEYandNEW_KEY. * Encrypt withNEW_KEYonly. * Decrypt with either key. * Gradually re‑encrypt data withNEW_KEYand dropOLD_KEY.
This concept is enough for most use cases.
8. Security Checklist (Fernet Edition)
| Item | Check |
|---|---|
| Key Management | Keep DJANGO_FERNET_KEY out of Git; load from Secrets Manager, Parameter Store, Vault, etc. |
| Access Separation | Inject the key only on production; use a different key locally. |
| Encryption Scope | Encrypt only truly sensitive values (API keys, tokens). Keep other data in plain columns. |
| Logging | Never log plain text, tokens, or keys (especially in DEBUG mode). |
| Backups | Backups are useless without the key; ensure the key is protected. |
| Testing | Verify encrypt → decrypt round‑trip and migration of legacy plain data. |
9. Wrap‑Up
Low‑level AES implementations are error‑prone and hard to maintain. Using Fernet gives you:
- A proven, secure algorithm combination
- Automatic IV, timestamp, and integrity checks
- A simple API
In Django, a custom EncryptedTextField plus the _secret/secret pattern lets developers work with plain text while the database stores only encrypted values.
Bottom line
- Always encrypt sensitive data before storing.
- Leave the heavy lifting to well‑tested libraries. Following these two principles significantly raises your Django service’s security posture. 🔐
There are no comments.