Djangoでsettings.pyの機密情報を管理する方法

はじめに

Djangoではプロジェクトの設定情報を settings.py で管理します。
このファイルにはデータベースの接続情報などを設定しますが、これら機密情報の管理には以下の課題があります。

  • セキュリティ上の問題となるため、リポジトリ上で公開すべきではない
  • 実行環境ごとに変わる情報であるためバージョン管理外としたい

そこで本エントリでは、機密情報をバージョン管理外のファイルで管理する手順をまとめます。
なお、説明を簡単にするため django-admin startproject が生成するプロジェクトディレクトリ直下の settings.py を対象に話を進めますが、当該ファイルを別のパスで管理している場合には適宜説明を読み替えてください。

プロジェクトの作成

my_project という名前のプロジェクトで説明します。

$ django-admin startproject my_project
$ cd my_project

プロジェクトディレクトmy_project 以下に作成される settings.py の機密情報を管理する方法を考えていきます。

$ ls
manage.py   my_project
$ ls my_project
__init__.py settings.py urls.py     wsgi.py

settings.pyの設定内容

ここでは settings.py に以下のような設定があるものとします。
データベースは標準でSQLite3ですが、ここではPostgreSQLに変更しています。

SECRET_KEY = 'noeh%g##*pneyx@stz_1b*cl6liudiw(dvsa#9xh44h0x^@gjb'
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'my_db',
        'USER': 'my_user',
        'PASSWORD': 'maeh7Aik',
        'HOST': 'localhost',
        'PORT': '',
        'ATOMIC_REQUESTS': True,
    }
}

ここで問題となるのは以下の4項目です。各項目の用途はリンク先を参照してください。

機密情報をJSONファイルで管理

以下の手順で機密情報の管理方法を改めます。

  1. 機密情報を管理するJSONファイル secrets.json の生成スクリプトを用意
  2. settings.pysecrets.json の設定値を参照するように変更
  3. 実行環境ごとに 1.スクリプトsecrets.json を作成し、機密情報を入力

1. 機密情報を管理するJSONファイル secrets.json の生成スクリプトを用意

my_project 以下にスクリプト gen_secrets.py を作成します。
このスクリプトは機密情報を管理するJSONの雛形をプロジェクトのルートに作成します。
SECRET_KEY については django.core.management.utils.get_random_secret_key で自動生成していますが、データベース関連の項目は空文字を出力します。
プロジェクトで取り扱う機密情報に応じて secrets = {...} の辞書定義は修正してください。

import json
import os

from django.core.management.utils import get_random_secret_key

BASE_DIR = os.path.dirname(
    os.path.dirname(
        os.path.abspath(__file__)
    )
)

secrets = {
    'SECRET_KEY': get_random_secret_key(),
    'DB_NAME': '',
    'DB_USER': '',
    'DB_PASSWORD': '',
}

with open(os.path.join(BASE_DIR, 'secrets.json'), 'w') as outfile:
    json.dump(secrets, outfile)

secrets.json はバージョン管理する必要のないファイルなので、忘れずに .gitignore に指定しておきましょう。

secrets.json

2. settings.pysecrets.json の設定値を参照するように変更

以下の通り settings.py を修正します。
SECRET_KEY 等の設定値が get_secret() の戻り値となっていることを確認してください。
get_secret() は引数の文字列をキーに secrets.json から値を取得しています。
これで機密情報の管理を settings.py から secrets.json に追い出すことができました。

import json
import os

...
if os.path.exists(os.path.join(BASE_DIR, 'secrets.json')):
    with open(os.path.join(BASE_DIR, 'secrets.json')) as secrets_file:
        secrets = json.load(secrets_file)
else:
    print('secrets.json could not be found.')
    quit()


def get_secret(setting, secrets=secrets, is_optional=False): #  設定が見つからない場合にNoneを返したい場合にはis_optionalにTrueを設定
    try:
        secret = secrets[setting]
        if secret:
            return secret
        else:
            if is_optional:
                return None
            else:
                print(f'Please set {setting} in secrets.json')
                quit()
    except KeyError:
        print(f' Please set {setting} in secrets.json')
        quit()


SECRET_KEY = get_secret('SECRET_KEY')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': get_secret('DB_NAME'),
        'USER': get_secret('DB_USER'),
        'PASSWORD': get_secret('DB_PASSWORD'),
        'HOST': 'localhost',
        'PORT': '',
        'ATOMIC_REQUESTS': True,
    }
}

3. 実行環境ごとに 1.スクリプトsecrets.json を作成し、機密情報を入力

secrets.jsonの作成

以下のコマンドで secrets.json (雛形)をプロジェクトのルートに作成します。
SECRET_KEY の値は実行の都度変化します。

$ python my_project/gen_secrets.py 
$ cat secrets.json
{"SECRET_KEY": "#_odl(6s%+h_1=l95ow%5d=*=f!uzvt%*++y_7ne1%5_f(#l2(", "DB_NAME": "", "DB_USER": "", "DB_PASSWORD": ""}

secrets.jsonの編集

元々 settings.py で管理していた値で DB_NAMEDB_USERDB_PASSWORD を更新しました。

$ cat secrets.json 
{"SECRET_KEY": "#_odl(6s%+h_1=l95ow%5d=*=f!uzvt%*++y_7ne1%5_f(#l2(", "DB_NAME": "my_db", "DB_USER": "my_user", "DB_PASSWORD": "maeh7Aik"}

動作確認

データベースの用意

secrets.json の設定値でデータベース・ユーザを作成します。

$ initdb -D postgres_data
$ pg_ctl -D postgres_data -l logfile start
$ createuser -s -d -P my_user
Enter password for new role: maeh7Aik
Enter it again: maeh7Aik
$ createdb my_db -O my_user

マイグレーション

マイグレーションを実行します。これが成功すれば正しく設定できています。

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

サーバ起動

サーバも問題なく起動できました。

$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
July 14, 2020 - 14:32:06
Django version 2.1.7, using settings 'my_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

さいごに

本エントリでは、Djangoアプリの機密情報を管理する方法をまとめました。
今回紹介した内容は一つの例に過ぎないので、ご自身のプロジェクトに適した方式を検討することをおすすめします。
django-environ等を採用するのも良いと思います。