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        # 運用
settings/base.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"),
        ...
    }
}

# ⚠ ここには絶対に秘密情報を置かない
settings/dev.py
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"]
settings/prod.py
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 のデフォルトを環境変数で:

manage.py
os.environ.setdefault(
    "DJANGO_SETTINGS_MODULE",
    "myproject.settings.dev",   # デフォルトは dev、運用は環境で上書き
)

パターン 2: 単一ファイル + 環境変数による分岐 #

settings.py — 単一ファイル
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-environ
settings.py
import 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 にコミットしない):

.env
DEBUG=False
SECRET_KEY=...
ALLOWED_HOSTS=myapp.com,www.myapp.com
DATABASE_URL=postgres://user:pass@db:5432/mydb
CACHE_URL=redis://cache:6379/1

DATABASE_URL 1 つが ENGINE/NAME/USER/PASSWORD/HOST/PORT をすべて埋めてくれるので、12factor スタイルのデプロイによく似合います。

pydantic-settings #

Django でも モダン Python 実践 で見たそれを使えます。

myproject/config.py
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 の時点で ValidationError
settings.py
from 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 が必要)

カスタムエラーページ #

myapp/views.py
def custom_404(request, exception):
    return render(request, "404.html", status=404)

def custom_500(request):
    return render(request, "500.html", status=500)
urls.py
handler404 = "myapp.views.custom_404"
handler500 = "myapp.views.custom_500"

運用デプロイ前に 2 ページを作っておけば、画像 / スタイルが適用された親切なエラー画面になります。

ALLOWED_HOSTS #

DEBUG=False で有効。リクエストの Host: ヘッダーがこのリストになければ 400 Bad Request

prod
ALLOWED_HOSTS = ["myapp.com", "www.myapp.com"]

ワイルドカードの落とし穴 #

🚫 危険
ALLOWED_HOSTS = ["*"]

すべてのホストを許可 — DNS rebinding、host header 攻撃 を無防備に受けます。絶対に運用に置いてはいけません。

✅ サブドメインワイルドカード
ALLOWED_HOSTS = [".myapp.com"]   # ドットで開始 — *.myapp.com の意味

ヘルスチェック IP #

ロードバランサーのヘルスチェックが IP で直接来ると host ヘッダーが IP になります。それも明示:

ALB / k8s ヘルスチェック
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 があります。新しいキーで署名しつつ、古いキーで署名されたセッションも一時的に受け付けます。

rotation 手順
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 する場面 (サブドメイン、決済コールバックなど) があるなら:

信頼 origin
CSRF_TRUSTED_ORIGINS = [
    "https://myapp.com",
    "https://www.myapp.com",
    "https://payments.example.com",
]

スキーム (https://) の含有が必須。 4.0 から義務化されました。

クッキーオプション #

prod
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"
クッキーオプション意味
SecureHTTPS でのみ送信
HttpOnlyJS (document.cookie) のアクセスを遮断
SameSite=Lax別サイトが送ったリクエストには自動添付しない (top-level GET のみ OK)
SameSite=Strict別サイトのリクエストには絶対添付しない
SameSite=Noneすべてのリクエストに添付 (Secure 必須)

中級 #5 のオプションが運用で一気に集まります。

HTTPS / HSTS #

SECURE_SSL_REDIRECT #

HTTP → HTTPS 自動リダイレクト
SECURE_SSL_REDIRECT = True

Django が HTTP リクエストを受けたら 301 で HTTPS に送ります。普通は nginx/ALB 側で処理しますが、Django 側にも一度置けば安全。

SECURE_PROXY_SSL_HEADER — プロキシ後ろのとき #

リバースプロキシ (nginx、ALB、Cloudflare) が SSL 終端をするなら、Django から見ると HTTP のように見えます。プロキシが送ってくれるヘッダーで元は HTTPS だったことを伝える必要があります。

proxy 後ろ
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

⚠ 信頼できるプロキシの後ろでなければ ヘッダー偽造 の危険。プロキシが常にこのヘッダーを上書きするよう設定された環境でのみ。

HSTS — Strict-Transport-Security #

ブラウザに「このドメインは HTTPS のみ使う」と伝えるヘッダー。

HSTS
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 #

clickjacking 防御
X_FRAME_OPTIONS = "DENY"

Django のデフォルトは SAMEORIGIN。iframe に自サイトが入るのを防いで clickjacking 攻撃を遮断。決済、ログインページに特に重要。

Content Security Policy (CSP) #

XSS などの最後の防御線。django-csp が標準。

インストール
pip install django-csp
settings.py
MIDDLEWARE = [..., "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 モードで始めましょう — 違反時に遮断ではなくレポートのみ:

report-only
CSP_REPORT_ONLY = True
CSP_REPORT_URI = "/csp-report/"

数日モニタリングして違反をつかみ、ポリシー調整後に enforce モードへ。

SECURE_CONTENT_TYPE_NOSNIFFSECURE_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 年のパスワードハッシュコンペ優勝)。追加パッケージが必要:

argon2
pip install argon2-cffi

ハッシャーを追加して最初に置けば 新パスワードは Argon2、既存の PBKDF2 パスワードはユーザーが次回ログインしたときに自動で Argon2 で再ハッシュされます。

AUTH_PASSWORD_VALIDATORS #

検証器 — デフォルト + alpha
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 文字推奨。複雑度ルール (特殊文字を強制) よりも長さ の方が効果的というのが現代の推奨事項です。

ロギング #

運用ロギングのパターン #

settings/prod.py
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]"
settings/prod.py
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 に入れて通らなければデプロイ中断 — 運用前の必須ゲートとして置くことを推奨。

.github/workflows/deploy.yml — 一部
- name: Django deploy check
  run: |
    DJANGO_SETTINGS_MODULE=myproject.settings.prod \
    python manage.py check --deploy --fail-level WARNING

secret 管理 — どこに置くか #

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 自動化監査ログ まで。

settings.py — boto3 例
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 で遮断:

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 --noinput

whitenoise は 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_SECURECSRF_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_URL 1 行)、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_REDIRECTSECURE_PROXY_SSL_HEADER、HSTS (徐々に)、preload
  • ヘッダー: X_FRAME_OPTIONS=DENYdjango-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 プロジェクトを一気に組み立てる流れです。

X