Django上級 #7 デプロイのセキュリティ — settings 分離、ALLOWED_HOSTS、CSRF、secret 管理
Django 上級シリーズの最後。中級 #5 メッセージ / セッション / クッキー のクッキーセキュリティオプションの上に、運用デプロイ前に必ず点検 すべき項目を整理します。settings 分離から HSTS、secret 管理、自動点検コマンドまで。
Django は 開発フレンドリなデフォルト値 で始まります。それがそのまま運用に乗ると危険な箇所が多いです。この記事の目的はそのギャップを埋めることです。
全体像 #
運用セキュリティの軸を 3 つにまとめると:
| 軸 | キーワード |
|---|---|
| 環境分離 | settings 分離、env vars、secret 管理 |
| 通信セキュリティ | HTTPS、HSTS、secure cookies、CSRF、proxy ヘッダー |
| 情報露出 | DEBUG、ALLOWED_HOSTS、エラーページ、ログ |
settings 分離 — 2 つの道 #
パターン 1: ファイル分割 #
myproject/
├── settings/
│ ├── __init__.py
│ ├── base.py # 共通
│ ├── dev.py # 開発
│ ├── test.py # テスト
│ └── prod.py # 運用import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
...
"myapp",
]
MIDDLEWARE = [...]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME"),
...
}
}
# ⚠ ここには絶対に秘密情報を置かないfrom .base import *
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
SECRET_KEY = "dev-only-do-not-use-in-prod"
INSTALLED_APPS += ["debug_toolbar"]
INTERNAL_IPS = ["127.0.0.1"]from .base import *
import os
DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"] # なければ KeyError → fail fast
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
# ... 運用専用設定たち (以下のセクションで)DJANGO_SETTINGS_MODULE=myproject.settings.prod gunicorn myproject.wsgi:applicationまたは manage.py のデフォルトを環境変数で:
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE",
"myproject.settings.dev", # デフォルトは dev、運用は環境で上書き
)パターン 2: 単一ファイル + 環境変数による分岐 #
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
ENVIRONMENT = os.environ.get("DJANGO_ENV", "development")
PRODUCTION = ENVIRONMENT == "production"
DEBUG = not PRODUCTION
if PRODUCTION:
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ["DB_HOST"],
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
else:
SECRET_KEY = "dev-secret"
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}| ファイル分割 | 単一ファイル | |
|---|---|---|
| 可読性 | 明確 (どこに何が) | 1 か所で比較 |
| 設定差分の追跡 | ファイル diff が難しい | 直感的 |
| 新環境追加 | 新ファイル | if 1 行 |
| 大きなプロジェクト | 良い | すぐ複雑に |
| 小さなプロジェクト | 過剰 | 十分 |
小〜中規模は 単一ファイル + 環境変数、大きなプロジェクトは ファイル分割 が普通。
環境変数の検証 — django-environ / pydantic-settings #
生の os.environ は漏れ / 型変換ミスが多いです。検証ツールを使います:
django-environ #
pip install django-environimport environ
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, []),
DATABASE_URL=(str, ""),
)
environ.Env.read_env() # .env ファイルをロード (あれば)
DEBUG = env("DEBUG")
SECRET_KEY = env("SECRET_KEY")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
# DATABASE_URL 1 行で DB 設定
DATABASES = {"default": env.db()}
# CACHE_URL も 1 行
CACHES = {"default": env.cache()}.env ファイル (絶対に git にコミットしない):
DEBUG=False
SECRET_KEY=...
ALLOWED_HOSTS=myapp.com,www.myapp.com
DATABASE_URL=postgres://user:pass@db:5432/mydb
CACHE_URL=redis://cache:6379/1DATABASE_URL 1 つが ENGINE/NAME/USER/PASSWORD/HOST/PORT をすべて埋めてくれるので、12factor スタイルのデプロイによく似合います。
pydantic-settings #
Django でも モダン Python 実践 で見たそれを使えます。
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
debug: bool = False
secret_key: str
allowed_hosts: list[str] = []
database_url: str
model_config = SettingsConfigDict(env_file=".env", case_sensitive=False)
settings = Settings() # 漏れ変数があれば startup の時点で ValidationErrorfrom myproject.config import settings as cfg
DEBUG = cfg.debug
SECRET_KEY = cfg.secret_key
ALLOWED_HOSTS = cfg.allowed_hosts起動時点で検証 されるのが大きな長所 — ランタイムで KeyError が出ません。
DEBUG=False の意味
#
DEBUG=True が運用に漏れると起こること:
- エラーページに settings、環境変数、スタックトレース、SQL クエリ がすべて露出 → SECRET_KEY まで見えるかもしれない
- 静的ファイルを Django が直接配信 (遅い、負荷)
ALLOWED_HOSTSが無視される (host ヘッダー検証なし)
DEBUG=False で:
- 404、500 ページが単純化 — ディテールなし
ALLOWED_HOSTSが有効 (合わなければ 400)- 静的ファイルは直接配信しない (
collectstatic+ nginx/CDN が必要)
カスタムエラーページ #
def custom_404(request, exception):
return render(request, "404.html", status=404)
def custom_500(request):
return render(request, "500.html", status=500)handler404 = "myapp.views.custom_404"
handler500 = "myapp.views.custom_500"運用デプロイ前に 2 ページを作っておけば、画像 / スタイルが適用された親切なエラー画面になります。
ALLOWED_HOSTS
#
DEBUG=False で有効。リクエストの Host: ヘッダーがこのリストになければ 400 Bad Request。
ALLOWED_HOSTS = ["myapp.com", "www.myapp.com"]ワイルドカードの落とし穴 #
ALLOWED_HOSTS = ["*"]すべてのホストを許可 — DNS rebinding、host header 攻撃 を無防備に受けます。絶対に運用に置いてはいけません。
ALLOWED_HOSTS = [".myapp.com"] # ドットで開始 — *.myapp.com の意味ヘルスチェック IP #
ロードバランサーのヘルスチェックが IP で直接来ると host ヘッダーが IP になります。それも明示:
ALLOWED_HOSTS = ["myapp.com", ".myapp.com", "10.0.0.0/8"] # CIDR も可能 (5.x)またはヘルスチェックの path だけ別途処理するミドルウェアを作ることもあります。
SECRET_KEY 管理
#
Django がセッション、CSRF、パスワードリセットトークンなどを署名するのに使う。漏洩すればセッション偽造が可能。
ルール:
- 絶対に git に置かない —
.env、secrets manager (AWS SM、Vault、GCP Secret Manager) - 環境ごとに違う値
- ログに絶対に出さない
- ドキュメント / README に絶対に書かない
生成 #
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())または:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"Rotation #
キーを変更しなければならないとき (漏洩、定期交換):
Django 4.1+ から SECRET_KEY_FALLBACKS があります。新しいキーで署名しつつ、古いキーで署名されたセッションも一時的に受け付けます。
SECRET_KEY = "new-key"
SECRET_KEY_FALLBACKS = ["old-key"] # 一時的に保持数日後、古いセッションがすべて期限切れになったら fallback を削除。
CSRF — Cross-Site Request Forgery #
Django は CSRF トークン検証をデフォルトで有効にしています。POST/PUT/DELETE には csrfmiddlewaretoken または X-CSRFToken ヘッダーが必要です。
CSRF_TRUSTED_ORIGINS — 4.0+ で必須
#
別ドメインから自サイトへ form を POST する場面 (サブドメイン、決済コールバックなど) があるなら:
CSRF_TRUSTED_ORIGINS = [
"https://myapp.com",
"https://www.myapp.com",
"https://payments.example.com",
]スキーム (https://) の含有が必須。 4.0 から義務化されました。
クッキーオプション #
CSRF_COOKIE_SECURE = True # HTTPS でのみ送信
SESSION_COOKIE_SECURE = True # セッションも同じく
CSRF_COOKIE_HTTPONLY = False # JS がトークンを読んでヘッダーに入れる必要あり
SESSION_COOKIE_HTTPONLY = True # セッションは JS アクセスを遮断
CSRF_COOKIE_SAMESITE = "Lax" # CSRF の基本防御
SESSION_COOKIE_SAMESITE = "Lax"| クッキーオプション | 意味 |
|---|---|
Secure | HTTPS でのみ送信 |
HttpOnly | JS (document.cookie) のアクセスを遮断 |
SameSite=Lax | 別サイトが送ったリクエストには自動添付しない (top-level GET のみ OK) |
SameSite=Strict | 別サイトのリクエストには絶対添付しない |
SameSite=None | すべてのリクエストに添付 (Secure 必須) |
中級 #5 のオプションが運用で一気に集まります。
HTTPS / HSTS #
SECURE_SSL_REDIRECT
#
SECURE_SSL_REDIRECT = TrueDjango が HTTP リクエストを受けたら 301 で HTTPS に送ります。普通は nginx/ALB 側で処理しますが、Django 側にも一度置けば安全。
SECURE_PROXY_SSL_HEADER — プロキシ後ろのとき
#
リバースプロキシ (nginx、ALB、Cloudflare) が SSL 終端をするなら、Django から見ると HTTP のように見えます。プロキシが送ってくれるヘッダーで元は HTTPS だったことを伝える必要があります。
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")⚠ 信頼できるプロキシの後ろでなければ ヘッダー偽造 の危険。プロキシが常にこのヘッダーを上書きするよう設定された環境でのみ。
HSTS — Strict-Transport-Security #
ブラウザに「このドメインは HTTPS のみ使う」と伝えるヘッダー。
SECURE_HSTS_SECONDS = 31536000 # 1 年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True⚠ 最初は小さな値で始めましょう (例: 60 秒)。HSTS が一度キャッシュされると その期間中は HTTP アクセス自体が遮断 — 証明書の問題が起きるとユーザーがサイトに入れなくなります。安定確認後に少しずつ増やします。
preload list #
SECURE_HSTS_PRELOAD = True と一緒に hstspreload.org に登録すると ブラウザビルトインの HTTPS 強制リスト に含まれます。初訪問でも HTTP の試み自体が起きません。登録は慎重に — 外すのに数か月かかります。
その他のセキュリティヘッダー #
X_FRAME_OPTIONS
#
X_FRAME_OPTIONS = "DENY"Django のデフォルトは SAMEORIGIN。iframe に自サイトが入るのを防いで clickjacking 攻撃を遮断。決済、ログインページに特に重要。
Content Security Policy (CSP) #
XSS などの最後の防御線。django-csp が標準。
pip install django-cspMIDDLEWARE = [..., "csp.middleware.CSPMiddleware"]
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "https://cdn.jsdelivr.net")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_CONNECT_SRC = ("'self'", "https://api.myapp.com")最初は report-only モードで始めましょう — 違反時に遮断ではなくレポートのみ:
CSP_REPORT_ONLY = True
CSP_REPORT_URI = "/csp-report/"数日モニタリングして違反をつかみ、ポリシー調整後に enforce モードへ。
SECURE_CONTENT_TYPE_NOSNIFF、SECURE_REFERRER_POLICY
#
SECURE_CONTENT_TYPE_NOSNIFF = True # デフォルト True (5.x)
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"パスワード — ハッシャー、検証器 #
PASSWORD_HASHERS — 強いハッシュ
#
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
]デフォルトは PBKDF2 — 十分に安全です。Argon2 が現代の標準 (2015 年のパスワードハッシュコンペ優勝)。追加パッケージが必要:
pip install argon2-cffiハッシャーを追加して最初に置けば 新パスワードは Argon2、既存の PBKDF2 パスワードはユーザーが次回ログインしたときに自動で Argon2 で再ハッシュされます。
AUTH_PASSWORD_VALIDATORS
#
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 12}, # 8 → 12 推奨
},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]NIST ガイドライン基準で最低 12 文字推奨。複雑度ルール (特殊文字を強制) よりも長さ の方が効果的というのが現代の推奨事項です。
ロギング #
運用ロギングのパターン #
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {name} {process:d} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": {
"django.request": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
},
"django.security": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
},
}ルール:
- stdout/stderr へ — コンテナ / systemd が収集
- ファイルロギングはしない (ローテーション、権限、ディスクすべてが面倒)
- ERROR は別途通知 (Sentry など)
Sentry 統合 #
pip install "sentry-sdk[django]"import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"],
integrations=[DjangoIntegration()],
traces_sample_rate=0.1,
send_default_pii=False, # 個人情報の自動収集なし
)運用エラー追跡の事実上の標準。
manage.py check --deploy
#
Django が提供する デプロイ点検の自動化コマンド。セキュリティ推奨事項をチェックして警告を出します。
DJANGO_SETTINGS_MODULE=myproject.settings.prod python manage.py check --deploy出力例:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting...
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True.
?: (security.W012) SESSION_COOKIE_SECURE is not set to True.
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True.
?: (security.W018) You should not have DEBUG set to True in deployment.
?: (security.W019) You have 'django.middleware.clickjacking.XFrameOptionsMiddleware' in your MIDDLEWARE, but X_FRAME_OPTIONS is not set to 'DENY'.
?: (security.W020) ALLOWED_HOSTS must not be empty in deployment.CI に入れて通らなければデプロイ中断 — 運用前の必須ゲートとして置くことを推奨。
- name: Django deploy check
run: |
DJANGO_SETTINGS_MODULE=myproject.settings.prod \
python manage.py check --deploy --fail-level WARNINGsecret 管理 — どこに置くか #
1) .env + サーバーディスク
#
小さなプロジェクト。.env を git から除外してサーバーに直接配置。シンプルだが rotation、多重環境、権限 に弱い。
2) Cloud secrets manager #
- AWS Secrets Manager / AWS Systems Manager Parameter Store
- GCP Secret Manager
- Azure Key Vault
ランタイムに IAM で認証して取得。rotation 自動化、監査ログ まで。
import boto3, json
if PRODUCTION:
client = boto3.client("secretsmanager")
secret = json.loads(client.get_secret_value(SecretId="myapp/prod")["SecretString"])
SECRET_KEY = secret["django_secret"]
...3) HashiCorp Vault #
セルフホスト / マルチクラウド環境。
4) Kubernetes Secrets / Sealed Secrets #
Kubernetes の上なら自然な選択肢。Sealed Secrets で git に暗号化された形で保存。
絶対にしないこと #
- コード / git に平文
- README、Wiki、Slack
- 環境変数に平文の git 上の docker-compose.yml
よく出会う落とし穴 #
1) DEBUG=True が運用へ
#
最も多くて最も致命的。CI で遮断:
python -c "import django; from django.conf import settings; assert not settings.DEBUG"または manage.py check --deploy --fail-level WARNING。
2) SECRET_KEY の git コミット履歴
#
コードに一度入って抜けた SECRET_KEY も git history に永遠に 残ります。そのキーは即座に rotation。git filter-branch / BFG で history の掃除も一緒に。
3) 静的ファイルの漏れ #
DEBUG=False では Django は静的ファイルを配信しません。必ず collectstatic をデプロイ段階に入れて、nginx/CDN/whitenoise のうち 1 つで配信。
python manage.py collectstatic --noinputwhitenoise は Django 自体で静的ファイルを配信できるようにするミドルウェア — シンプルなサイトには便利。
4) ALLOWED_HOSTS の漏れで空のレスポンス
#
デプロイしたのに全リクエストが 400 Bad Request — 十中八九 ALLOWED_HOSTS の漏れ。Django のログに明確に出ます。
5) ALB/Cloudflare の X-Forwarded-* の信頼
#
SECURE_PROXY_SSL_HEADER を有効にしたのに、肝心のプロキシ前から入ってくるリクエストがヘッダーを偽造できる環境 — すべてのリクエストが元 IP ではなく X-Forwarded-For の偽 IP として認識されます。プロキシが常にヘッダーを上書きする トポロジーでのみ。
6) Admin の露出 #
/admin/ URL をそのままにすると bot 攻撃の 1 番のターゲット。推奨:
- URL 変更 (
/secret-admin-path-12345/) - IP 制限 (nginx 側で)
- 2FA (
django-otp) - VPN 内からのみアクセス
点検チェックリスト #
デプロイ前に一度:
-
DEBUG = False -
SECRET_KEYが環境から来ていて git にない -
ALLOWED_HOSTSを明示 (*禁止) - HTTPS 強制 (
SECURE_SSL_REDIRECT) - HSTS 設定 (徐々に増やす)
-
SESSION_COOKIE_SECURE、CSRF_COOKIE_SECURE= True -
CSRF_TRUSTED_ORIGINSを明示 -
X_FRAME_OPTIONS = "DENY" - CSP 設定 (まず report-only)
- パスワード検証器の長さ 12 文字以上
- Argon2 または PBKDF2
-
manage.py check --deploy通過 - 静的ファイル collectstatic + 配信
- エラー追跡 (Sentry など)
- バックアップ自動化 (DB)
- Admin 保護
まとめ #
今回つかんだもの:
- settings 分離: ファイル分割または環境変数による分岐 — 小さなプロジェクトは後者
- env 検証:
django-environ(DATABASE_URL1 行)、pydantic-settings DEBUG=Falseがトリガするもの — ALLOWED_HOSTS 有効化、静的ファイル非配信、エラー詳細の非公開ALLOWED_HOSTSのワイルドカードの落とし穴、ヘルスチェック IP 処理SECRET_KEY— git 禁止、secrets manager、SECRET_KEY_FALLBACKSで rotation- CSRF:
CSRF_TRUSTED_ORIGINS(4.0+ 必須)、クッキーオプション (Secure/HttpOnly/SameSite) - HTTPS:
SECURE_SSL_REDIRECT、SECURE_PROXY_SSL_HEADER、HSTS (徐々に)、preload - ヘッダー:
X_FRAME_OPTIONS=DENY、django-csp(report-only から) - パスワード: Argon2、長さ 12+
- ロギング: stdout、ERROR は Sentry
manage.py check --deploy— CI ゲート- secret 管理: secrets manager 推奨、git/README は絶対 NG
- 落とし穴: DEBUG 漏れ、SECRET_KEY history、collectstatic 漏れ、X-Forwarded 偽造、/admin/ の露出
- 点検チェックリストでデプロイ前に確認
シリーズを締めくくって #
基礎 7 編 → 中級 7 編 → 上級 7 編で 21 編の Django 道具箱を 1 か所にまとめました。
これからその上に API を 1 つのプロジェクト に積む段階が残っています。次のシリーズ Django 実践 — DRF #1 では、同じ Django の上に REST API を本格的に 作ります。DRF の ViewSet/Serializer/Permission、JWT、ページネーション、OpenAPI 自動ドキュメント、Celery 非同期作業、テストとデプロイまで — 6 編で運用可能な Django API プロジェクトを一気に組み立てる流れです。