Безопасное хранение секретных ключей в модели Django (версия Fernet)

Безопасность – ответственность разработчика – хранить открытый текст в БД нельзя.

1. Зачем нужна шифрация?



  • API Key / Secret / OAuth токен – при утечке это как открытая дверь для всей службы.
  • Хардкод в settings.py → Git‑репозиторий, артефакты развертывания, ноутбук разработчика – всё это может раскрыть данные.
  • Даже если хранить в переменных окружения, запись в БД без шифрации приводит к открытию в момент утечки БД.

Важно различать:

  • Пароль – серверу не нужен открытый текст → хеш (BCrypt, Argon2).
  • API Key / Secret / токен – серверу нужен открытый текст → шифрование с возможностью расшифровки.

Итог

  • Данные, которые нужно использовать позжешифруем и сохраняем.
  • Данные, которые не нужно использовать (пароли) → хешируем.

2. Что такое Fernet?

cryptography.fernet.Fernet – высокоуровневый API, который всё делает за вас:

  • Внутренне использует надёжные алгоритмы (AES + HMAC).
  • Автоматически генерирует случайный IV, таймстамп, проверку целостности.
  • Разработчик просто хранит один ключ и вызывает:
  • encrypt(plaintext) → токен‑строка
  • decrypt(token) → открытый текст
from cryptography.fernet import Fernet

key = Fernet.generate_key()      # генерация ключа
f = Fernet(key)

token = f.encrypt(b"hello")      # шифрование
plain = f.decrypt(token)         # расшифровка

Что нужно сделать:

  1. Управление ключом (переменные окружения, Secret Manager и т.д.)
  2. В Django автоматически шифровать/расшифровывать при чтении/записи.

3. Генерация ключа Fernet и настройка Django



3‑1. Генерация ключа

# Генерация одного ключа Fernet (Base64‑строка)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Пример вывода:

twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=

Сохраняем в переменную окружения сервера.

export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="

3‑2. Подготовка экземпляра Fernet в settings.py

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

DJANGO_FERNET_KEY = os.environ["DJANGO_FERNET_KEY"]  # строка
FERNET = Fernet(DJANGO_FERNET_KEY.encode("utf-8"))

Теперь в любом месте проекта можно импортировать settings и вызывать settings.FERNET.encrypt(...) / settings.FERNET.decrypt(...).


4. Добавление поля Fernet‑шифрования в модель Django

Нужно только один простой пользовательский тип поля.

4‑1. Основные идеи

  • В БД хранится токен‑строка.
  • В коде всегда виден открытый текст.
  • Паттерн _secret + secret разделяет имя колонки и логическое поле.

4‑2. Реализация EncryptedTextField

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


class EncryptedTextField(models.TextField):
    """Fernet‑шифрованное поле TextField."""

    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

Практический совет

Если в БД уже есть открытые данные, from_db_value может вернуть их как есть, а при последующем чтении‑записи они будут автоматически зашифрованы.


5. Применение в модели

# 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()   # реальная колонка в БД (токен)

    @property
    def secret(self):
        return self._secret

    @secret.setter
    def secret(self, value: str):
        self._secret = value
  • _secret – имя колонки в БД, хранит токен.
  • secret – логическое поле, всегда открытый текст.

6. Примеры использования

6‑1. Django shell

>>> from myapp.models import MyModel

# Создание объекта
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key"
>>> obj.save()

# В БД хранится токен
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-'

# В коде виден открытый текст
>>> obj.secret
'my-very-secret-key'

6‑2. В 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})

В реальной работе обычно obj.secret используется только внутри сервера, а клиенту не передаётся.


7. Что важно знать о Fernet

7‑1. Ограничения поиска/фильтрации

Fernet генерирует разные токены для одинакового текста, поэтому:

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

_secret в БД будет разными, и запросы по открытым значениям невозможны.

Если нужна поиск/сортировка по открытым данным, добавьте отдельный хеш‑или‑префикс‑поле или пересмотрите архитектуру.

7‑2. Ротация ключей

Не держите один ключ навсегда. Стратегия:

  1. DJANGO_FERNET_KEY – основной ключ.
  2. При смене ключа создайте OLD_KEY и NEW_KEY.
  3. При шифровании используйте только NEW_KEY.
  4. При расшифровке пробуйте OLD_KEY и NEW_KEY.
  5. Постепенно обновляйте данные до NEW_KEY и удаляйте OLD_KEY.

8. Чеклист безопасности (Fernet)

Пункт Проверка
Управление ключом Не хранить в Git, загружать из Secrets Manager, Parameter Store, Vault и т.д.
Разделение прав Ключ доступен только на продакшн‑сервере, в dev/локале – другой ключ
Шифрование только нужных данных API‑ключи, токены – шифруем; пароли – хешируем
Логи Никогда не выводить открытый текст, токены, ключи в логи (особенно DEBUG)
Резервные копии Без ключа резервные копии БД бесполезны
Тесты encrypt -> decrypt совпадает, миграции с открытыми данными работают

9. Итоги

Низкоуровневый AES‑код писать сложно и рискованно. Используя Fernet:

  • Вы получаете надёжный набор алгоритмов.
  • Автоматически обрабатываются IV, таймстамп, HMAC.
  • API прост и безопасен.

В Django достаточно EncryptedTextField + паттерна _secret/secret, чтобы разработчики работали только с открытым текстом, а в БД всегда хранились зашифрованные данные.

Вывод

  • Секретные данные всегда шифруются.
  • Сложные алгоритмы доверяйте библиотекам. Это повышает уровень безопасности вашего Django‑сервиса.

image