Django 모델에 비밀키를 안전하게 저장하기 (Fernet 버전)
보안은 개발자의 책임 – DB에 원문 그대로 저장하면 안 됩니다.
1. 왜 암호화가 필요한가?
- API Key / Secret / OAuth 토큰 같은 값은 한 번 새어나가면 서비스 전체가 뚫린 것과 마찬가지입니다.
settings.py에 하드코딩하면 → Git 저장소, 배포 아티팩트, 개발자 노트북 등에서 그대로 노출될 수 있습니다.- 환경변수에 넣더라도 → 그 값을 그대로 DB에 저장하면 DB가 털리는 순간 그대로 평문 노출입니다.
또 한 가지 꼭 짚고 가야 할 점:
-
비밀번호(password)
-
서버가 “원문”을 알 필요가 없음 → 해시(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
# 마이그레이션 등으로 인해 평문이 섞여 있을 수 있으므로
# decrypt 실패 시에는 평문으로 간주하고 그대로 반환
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에서 decrypt 실패 시 평문으로 반환하게 해두고 점진적으로 읽기 → 다시 저장될 때 암호화된 값으로 변환되도록 할 수 있습니다.
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) # 토큰 그대로 비교는 가능하지만,
# 평문 기준 검색이 안 됨
즉, 평문 기준 검색/정렬이 필요한 값이라면:
- 별도의 검색용 컬럼(해시, prefix 등) 을 두거나
- 애초에 암호화 대상 설계를 다시 고민해야 합니다.
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 서비스의 보안 레벨이 크게 올라갑니다. 🔐

댓글이 없습니다.