セキュリティは開発者の責任 – データベースに原文をそのまま保存してはいけません。
1. なぜ暗号化が必要なのか?
- API Key / Secret / OAuthトークン のような値は、 一度漏れたら サービス全体が突破されたようなもの です。
settings.pyにハードコーディングすると、 → Gitリポジトリ、デプロイアーティファクト、開発者のノートパソコンなどで そのまま露出 します。- 環境変数に入れたとしても、 → その値をそのまま DB に保存すると DB が漏れた瞬間に平文が露出 します。
もう一つ重要なポイント:
- パスワード
- サーバーは「原文」を知る必要がない → ハッシュ(BCrypt, Argon2) を使用
- API Key / Secret / トークン
- サーバーが原文を再利用する必要がある → 復号可能な暗号化 が必要
まとめ
- 再利用が必要な機密データ → 暗号化して保存
- 再利用が不要なデータ(パスワード等) → ハッシュして保存
2. Fernet とは?
cryptography.fernet.Fernet は、以下を 一度に提供 する高レベル API です。
- 内部で安全なアルゴリズム(AES + HMAC 等)を使用
- ランダム IV、タイムスタンプ、整合性検証(HMAC) を自動で処理
- 開発者は 「キーを一つしっかり保管」 し、
encrypt(平文)→ トークン文字列decrypt(トークン)→ 平文 を呼び出すだけ
つまり、AES-CBC/IV/パディング/HMAC を直接扱う代わりに、
from cryptography.fernet import Fernet
key = Fernet.generate_key() # キー生成
f = Fernet(key)
token = f.encrypt(b"hello") # 暗号化
plain = f.decrypt(token) # 復号化
この程度を理解していれば十分に使えます。
取り組むべきことは二つだけ
- キー管理(環境変数、Secret Manager 等)
- Django で 書き/読み時に自動で encrypt/decrypt する仕組みを作る
3. Fernet キー生成と Django 設定
3-1. Fernet キー生成
# Fernet キー 1 つ生成(Base64 文字列で出力)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
出力例:
twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=
この文字列をサーバーの環境変数に入れます。
export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="
3-2. settings.py に Fernet インスタンスを準備
# 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"))
これで Django のどこからでも from django.conf import settings 後に
settings.FERNET.encrypt(...), settings.FERNET.decrypt(...) を使えます。
4. Django モデルに Fernet 暗号化フィールドを追加
複雑なブロック暗号実装の代わりに、非常にシンプルなカスタムフィールド だけで十分です。
4-1. 設計ポイント
- 実際の DB には 暗号化されたトークン文字列 だけを保存
- モデル/ビュー/サービスコードでは常に 復号された平文 だけを扱う
_secret+secretパターンで DB カラムと実際の意味 を分離
4-2. EncryptedTextField 実装
# myapp/utils/encrypted_field.py
from django.db import models
from django.conf import settings
class EncryptedTextField(models.TextField):
"""
Fernet ベースの暗号化 TextField
- 保存時: 平文(str) → Fernet トークン(str) に暗号化
- 取得時: Fernet トークン(str) → 平文(str) に復号化
"""
description = "Fernet encrypted text field"
def get_prep_value(self, value):
"""
Python オブジェクト → DB に保存される値
"""
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 値 → 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):
"""
ORM が Python オブジェクトとして扱う値
- from_db_value の後にもう一度呼ばれることがある
- ここでは特別な処理はせずそのまま返す
"""
return value
実務のヒント
- 既に平文データが DB に入っている場合、
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() # 実際の DB カラム(Fernet トークン)
@property
def secret(self):
"""
外部で使用する際は常に平文でアクセス
"""
return self._secret
@secret.setter
def secret(self, value: str):
"""
平文を割り当てると保存時に自動で暗号化される
"""
self._secret = value
_secret- 実際の DB カラム名
- DB には Fernet トークン(暗号化文字列)が保存される
secret- コードで使用する「論理的」フィールド
- 常に 平文 だけを扱う
このパターンにより、後でモデルを見る人も
obj.secret # 平文
obj._secret # 暗号化された値が入っているカラム
関係を明確に理解できます。
6. 使用例
6-1. Django shell
>>> from myapp.models import MyModel
# 新しいオブジェクトを作成
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key" # 平文で割り当て
>>> obj.save()
# DB に保存された実際のカラムは暗号化されたトークン
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-' # 例
# モデルで使用する際は平文
>>> obj.secret
'my-very-secret-key'
6-2. API で使用
実際には クライアントに secret をそのまま返すより、 バックエンド内部でのみ使用するケースが多いです。例としては以下のようにします。
# 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)
# obj.secret は平文(既に復号済み)
return Response({"secret": obj.secret})
実務では通常、
obj.secretを 外部 API 呼び出しに使用 するだけで- クライアントには 絶対にそのまま露出しない パターンが多いです。
7. Fernet を使う際に知っておくべきこと
7-1. 検索/フィルタリングの制約
Fernet は毎回暗号化結果が 変わる 構造です。
obj1.secret = "abc"
obj2.secret = "abc"
両方とも "abc" ですが、DB に保存された _secret は 完全に別のトークン です。
そのため、次のようなクエリは動作しません。
MyModel.objects.filter(_secret="abc") # 意味なし
MyModel.objects.filter(_secret=obj._secret) # トークンそのまま比較は可能だが、平文ベースの検索は不可
つまり、平文ベースの検索/ソート が必要な値なら:
- 別の 検索用カラム(ハッシュ、プレフィックス等) を用意する
- もしくは 暗号化対象の設計を再検討 する
7-2. キー変更(ローテーション)戦略
一つのキーをずっと使い続けるより、定期的に変更するのが理想です。
- シンプルなスタート:
DJANGO_FERNET_KEYだけで始める- 後で高度化:
OLD_KEY,NEW_KEYの二つを持ち、- 暗号化は NEW_KEY でのみ
- 復号は OLD/NEW 両方を試す
- データが NEW_KEY で再暗号化されると、OLD_KEY を削除
この部分は記事の分量が足りないので、概念だけ触れました。🙂
8. セキュリティチェックリスト(Fernet バージョン)
| 項目 | チェック |
|---|---|
| キー管理 | DJANGO_FERNET_KEY は Git に載せず、AWS Secrets Manager / Parameter Store / Vault 等からロード |
| 権限分離 | 本番サーバーでのみキーを注入し、開発/ローカル環境は別キーを使用 |
| 暗号化対象 | 本当に必要な値(API Key, Secret, トークン等)のみ暗号化し、残りは通常カラム |
| ログ | 平文/トークン/キーをログに出力しない(特に DEBUG ログは注意) |
| バックアップ | DB バックアップを取得しても、キーが無ければ意味のないデータ になるように |
| テスト | 単体テストで encrypt -> decrypt が一致、マイグレーション(既存平文)処理まで検証 |
9. まとめ
- 低レベルの AES 実装は 説明が難しく、ミスが多い。
- 代わりに Fernet を使えば:
- 安全なアルゴリズムの組み合わせ
- ランダム IV、タイムスタンプ、整合性検証
- シンプルな API を一度に取得
- Django では
EncryptedTextFieldなどのカスタムフィールド +_secret/secretプロパティパターンを使えば 開発者は 平文のみを扱うコード を書きつつ、 DB には 常に暗号化された値 を保存できる。
結論
- 「機密データは常に暗号化して保存」
- 「複雑な暗号アルゴリズムはライブラリに任せる」 だけを守れば、Django サービスのセキュリティレベルは大幅に向上します。 🔐

コメントはありません。