The recording for this lesson has not been published yet.
Open the settings.py you deployed at the end of the tutorial. It contains,
in plain text, in a file that is committed to git:
SECRET_KEY = "django-insecure-8s%3v!k2$0m@..."
DEBUG = True
ALLOWED_HOSTS = ["*"]
Each of those three lines is a separate incident waiting to happen.
SECRET_KEY signs session cookies, password-reset tokens
and CSRF tokens. Anyone who knows it can forge a session and log in as you.DEBUG = True in production renders a full traceback -
including your settings, your environment variables and fragments of your database -
to whoever triggers an error.ALLOWED_HOSTS = ["*"] disables host-header validation
and opens the door to cache poisoning and poisoned password-reset links.The principle comes from the Twelve-Factor App: anything that differs between your laptop, the staging server and production is configuration, and configuration lives in the environment.
pip install python-decouple
pip freeze > requirements.txt
Create a .env file next to manage.py:
# .env - NEVER commit this file
DJANGO_SECRET_KEY=replace-me-with-50-random-characters
DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DATABASE_URL=sqlite:///db.sqlite3
EMAIL_HOST_PASSWORD=
And rewrite the top of your settings:
# core/settings.py
from pathlib import Path
from decouple import Csv, config
BASE_DIR = Path(__file__).resolve().parent.parent
# No default -> the project refuses to start without a key. That is intentional.
SECRET_KEY = config("DJANGO_SECRET_KEY")
# Safe default: if nobody says otherwise, we are in production mode.
DEBUG = config("DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config("DJANGO_ALLOWED_HOSTS", default="127.0.0.1", cast=Csv())
EMAIL_PORT = config("EMAIL_PORT", default=587, cast=int)
Three details are worth memorising:
SECRET_KEY should
crash the process immediately, not silently fall back to a value an attacker knows.DEBUG defaults to
False, never to True.cast=bool, DEBUG would be the string
"False" - which Python considers true.bool("False") is True. This single line has taken more production sites down
than any exotic bug. cast=bool from decouple understands
True/False, 1/0, yes/no and on/off.
.env is private, but the list of variables is documentation. Commit a
template with empty or fake values so a new contributor knows what to fill in:
# .env.example - committed to git
DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
DATABASE_URL=sqlite:///db.sqlite3
EMAIL_HOST=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_PORT=587
And make absolutely sure the real one can never be staged:
# .gitignore
.env
.env.*
!.env.example
*.sqlite3
__pycache__/
/staticfiles/
/media/
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
Put the result into .env on your machine and into the environment of your
server - and use a different value in each place. If the old key was ever pushed
to GitHub, rotate it now; every session and every pending password-reset link will be
invalidated, which is exactly what you want.
On a PaaS you set the variables in the dashboard. On your own server you have options:
# /etc/systemd/system/myblog.service
[Service]
EnvironmentFile=/srv/myblog/.env
ExecStart=/srv/myblog/.venv/bin/gunicorn core.wsgi:application
# docker-compose.yml
services:
web:
build: .
env_file: .env
ports:
- "8000:8000"
The application code is identical in all three cases. That is the whole point: one artifact, many environments.
git ls-files | grep -c "^\.env$" # must print 0
python manage.py check --deploy # read every warning
python -c "from decouple import config; print(config('DEBUG', cast=bool))"
SECRET_KEY, DEBUG and ALLOWED_HOSTS out of
settings.py into .env..env to .gitignore and commit
.env.example..env temporarily and confirm the project refuses to start with a
clear error message.