Безопасное развертывание приложения Django с помощью Gunicorn, Nginx и HTTPS

Переход приложения Django от разработки к производству - это сложный, но полезный процесс. Этот учебник проведет вас через этот процесс шаг за шагом, предоставляя подробное руководство, которое начнется с самого начала с приложения Django, не требующего особых усилий, и добавит в него Gunicorn, Nginx, регистрацию доменов и заголовки безопасности HTTP. После изучения этого руководства вы будете лучше подготовлены к тому, чтобы запустить ваше приложение Django в производство и предоставить его миру.

В этом учебнике вы узнаете:

  • Как вы можете довести ваше приложение Django от разработки до производства
  • .
  • Как вы можете разместить свое приложение на реальном общественном домене
  • Как внедрить Gunicorn и Nginx в цепочку запросов и ответов
  • Как HTTP-заголовки могут укрепить безопасность HTTPS вашего сайта

Чтобы извлечь максимальную пользу из этого учебника, вы должны иметь ознакомительный уровень понимания Python, Django и высокоуровневой механики HTTP-запросов.

Вы можете скачать проект Django, используемый в этом учебнике, по следующей ссылке:

Get Source Code: Click here to get the companion Django project used in this tutorial.

Начало работы с Django и WSGIServer

Вы будете использовать Django как фреймворк в основе вашего веб-приложения, используя его для маршрутизации URL, рендеринга HTML, аутентификации, администрирования и логики бэкенда. В этом учебнике вы дополните компонент Django двумя другими слоями, Gunicorn и Nginx, чтобы обслуживать приложение масштабируемо. Но прежде чем это сделать, вам нужно настроить окружение и запустить само приложение Django.

Установка облачной виртуальной машины (VM)

Сначала необходимо запустить и настроить виртуальную машину (ВМ), на которой будет работать веб-приложение. Вам следует ознакомиться хотя бы с одним провайдером облачных услуг infrastructure as a service (IaaS), чтобы создать виртуальную машину. В этом разделе мы рассмотрим процесс на высоком уровне, но не будем подробно описывать каждый шаг.

Использование виртуальной машины для обслуживания веб-приложений - это пример IaaS, когда вы полностью контролируете серверное программное обеспечение. Существуют и другие варианты, помимо IaaS:

  • Архитектура serverless позволяет компоновать только приложение Django и позволить отдельному фреймворку или облачному провайдеру заниматься инфраструктурой.
  • Подход containerized позволяет нескольким приложениям работать независимо на одной операционной системе.

Для этого руководства, однако, вы будете использовать проверенный и верный путь обслуживания Nginx и Django непосредственно на IaaS.

Два популярных варианта виртуальных машин - Azure VMs и Amazon EC2. Для получения дополнительной помощи по запуску экземпляра следует обратиться к документации вашего облачного провайдера:

Проект Django и все остальное, задействованное в этом руководстве, находится на экземпляре Amazon EC2 под управлением Ubuntu Server 20.04.t2.micro

Одним из важных компонентов настройки виртуальной машины являются правила безопасности входящего трафика. Это тонкие правила, которые контролируют входящий трафик на ваш экземпляр. Для начальной разработки создайте следующие правила безопасности входящего трафика, которые вы измените в процессе эксплуатации:

Ссылка Тип Протокол Диапазон портов Источник
1 Custom TCP 8000 my-laptop-ip-address/32
2 Заказной Все Все security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

Теперь вы будете проходить их по очереди:

  1. Правило 1 разрешает TCP через порт 8000 с IPv4-адреса вашего персонального компьютера, что позволяет вам отправлять запросы в ваше приложение Django, когда вы обслуживаете его в разработке, через порт 8000.
  2. Правило 2 разрешает входящий трафик от сетевых интерфейсов и экземпляров, назначенных одной и той же группе безопасности, с использованием идентификатора группы безопасности в качестве источника. Это правило включено в группу безопасности AWS по умолчанию, которую вы должны привязать к своему экземпляру.
  3. Правило 3 позволяет вам получить доступ к вашей виртуальной машине через SSH с вашего персонального компьютера.

Вы также захотите добавить исходящее правило, чтобы разрешить исходящий трафик для выполнения таких действий, как установка пакетов:

Тип Протокол Диапазон портов Источник
Настройка Все Все 0.0.0.0/0

Связывая все это вместе, ваш начальный набор правил безопасности AWS может состоять из трех входящих правил и одного исходящего правила. Они, в свою очередь, исходят из трех отдельных групп безопасности - группы по умолчанию, группы для HTTP доступа и группы для SSH доступа:

Initial security ruleset for Django app.
Инициальный набор правил группы безопасности

С вашего локального компьютера вы можете SSH войти в экземпляр:

$ ssh -i ~/.ssh/<privkey>.pem ubuntu@<instance-public-ip-address>

Эта команда регистрирует вас на вашей ВМ как пользователя ubuntu. Здесь ~/.ssh/<privkey>.pem - это путь к закрытому ключу , который является частью набора учетных данных безопасности, которые вы привязали к ВМ. ВМ - это место, где будет располагаться код приложения Django.

После этого вы можете приступать к созданию приложения.

Создание Django-приложения по шаблону

В этом учебнике вам не нужно создавать модный проект Django со сложной маршрутизацией URL или расширенными возможностями базы данных. Вместо этого вам нужно что-то простое, небольшое и понятное, позволяющее быстро проверить, работает ли ваша инфраструктура.

Для этого вы можете выполнить следующие шаги по настройке вашего приложения.

Сначала войдите по SSH в свою виртуальную машину и убедитесь, что у вас установлены последние версии патчей Python 3.8 и SQLite3:

$ sudo apt-get update -y
$ sudo apt-get install -y python3.8 python3.8-venv sqlite3
$ python3 -V
Python 3.8.10

Здесь Python 3.8 - это системный Python, или python3 версия, поставляемая с Ubuntu 20.04 (Focal). Обновление дистрибутива гарантирует получение исправлений ошибок и безопасности из последнего выпуска Python 3.8.x. Как вариант, вы можете установить другую версию Python - например, python3.9 - вместе с общесистемным интерпретатором, который вам нужно будет вызывать явно как python3.9.

Далее, создайте и активируйте виртуальную среду :

$ cd  # Change directory to home directory
$ python3 -m venv env
$ source env/bin/activate

Теперь установите Django 3.2:

$ python -m pip install -U pip 'django==3.2.*'

Теперь вы можете загружать проект Django и приложение, используя команды управления Django:

$ mkdir django-gunicorn-nginx/
$ django-admin startproject project django-gunicorn-nginx/
$ cd django-gunicorn-nginx/
$ django-admin startapp myapp
$ python manage.py migrate
$ mkdir -pv myapp/templates/myapp/

Это создает приложение Django myapp рядом с проектом с именем project:

/home/ubuntu/
│
├── django-gunicorn-nginx/
│    │
│    ├── myapp/
│    │   ├── admin.py
│    │   ├── apps.py
│    │   ├── __init__.py
│    │   ├── migrations/
│    │   │   └── __init__.py
│    │   ├── models.py
│    │   ├── templates/
│    │   │   └── myapp/
│    │   ├── tests.py
│    │   └── views.py
│    │
│    ├── project/
│    │   ├── asgi.py
│    │   ├── __init__.py
│    │   ├── settings.py
│    │   ├── urls.py
│    │   └── wsgi.py
|    |
│    ├── db.sqlite3
│    └── manage.py
│
└── env/  ← Virtual environment

Используя редактор терминала, такой как Vim или GNU nano, откройте project/settings.py и добавьте ваше приложение в INSTALLED_APPS:

# project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "myapp",
]

Далее откройте myapp/templates/myapp/home.html и создайте короткую и милую HTML-страницу:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

После этого отредактируйте myapp/views.py, чтобы отобразить эту HTML-страницу:

from django.shortcuts import render

def index(request):
    return render(request, "myapp/home.html")

Теперь создайте и откройте myapp/urls.py, чтобы связать ваше представление с шаблоном URL:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

После этого отредактируйте project/urls.py соответствующим образом:

from django.urls import include, path

urlpatterns = [
    path("myapp/", include("myapp.urls")),
    path("", include("myapp.urls")),
]

Вы можете сделать еще одну вещь, пока вы здесь, а именно убедиться, что Django secret key, используемый для криптографической подписи, не закодирован в settings.py, что Git, вероятно, будет отслеживать. Удалите следующую строку из project/settings.py:

SECRET_KEY = "django-insecure-o6w@a46mx..."  # Remove this line

Замените его следующим:

import os

# ...

try:
    SECRET_KEY = os.environ["SECRET_KEY"]
except KeyError as e:
    raise RuntimeError("Could not find a SECRET_KEY in environment") from e

Это указывает Django искать SECRET_KEY в вашем окружении, а не включать его в исходный код вашего приложения.

Примечание: Для больших проектов ознакомьтесь с django-environ для настройки вашего приложения Django с помощью переменных среды.

Наконец, установите ключ в вашей среде. Вот как это можно сделать в Ubuntu Linux, используя OpenSSL для установки ключа в виде восьмидесятисимвольной строки:

$ echo "export SECRET_KEY='$(openssl rand -hex 40)'" > .DJANGO_SECRET_KEY
$ source .DJANGO_SECRET_KEY

Вы можете cat содержимое .DJANGO_SECRET_KEY, чтобы увидеть, что openssl сгенерировал криптографически безопасный шестнадцатеричный ключ:

$ cat .DJANGO_SECRET_KEY
export SECRET_KEY='26a2d2ccaf9ef850...'

Итак, все готово. Это все, что вам нужно для минимально функционирующего приложения Django.

Использование WSGIServer в разработке Django

В этом разделе вы протестируете веб-сервер разработки Django, используя httpie, удивительный HTTP-клиент командной строки для тестирования запросов к вашему веб-приложению из консоли:

$ pwd
/home/ubuntu
$ source env/bin/activate
$ python -m pip install httpie

Вы можете создать alias, который позволит вам отправить GET запрос, используя httpie вашему приложению:

$ # Send GET request and follow 30x Location redirects
$ alias GET='http --follow --timeout 6'

Это псевдоним GET на вызов http с некоторыми флагами по умолчанию. Теперь вы можете использовать GET docs.python.org для просмотра заголовков и тела ответа с домашней страницы документации Python.

Перед запуском сервера разработки Django, вы можете проверить ваш проект Django на наличие потенциальных проблем:

$ cd django-gunicorn-nginx/
$ python manage.py check
System check identified no issues (0 silenced).

Если ваша проверка не выявила никаких проблем, то скажите встроенному серверу приложений Django начать прослушивание на localhost, используя порт по умолчанию 8000:

$ # Listen on 127.0.0.1:8000 in the background
$ nohup python manage.py runserver &
$ jobs -l
[1]+ 43689 Running                 nohup python manage.py runserver &

Использование nohup <command> & выполняет command в фоновом режиме, чтобы вы могли продолжать использовать вашу оболочку. Вы можете использовать jobs -l, чтобы увидеть идентификатор процесса (PID), который позволит вам вывести процесс на передний план или завершить его. nohup перенаправит стандартный вывод (stdout) и стандартную ошибку (stderr) в файл nohup.out.

Примечание: Если окажется, что nohup завис и остался без курсора, нажмите Enter, чтобы вернуть курсор терминала и приглашение оболочки.

Команда runserver в Django, в свою очередь, использует следующий синтаксис:

$ python manage.py runserver [address:port]

Если вы оставите аргумент address:port неуказанным, как было сделано выше, Django по умолчанию будет слушать на localhost:8000. Вы также можете использовать команду lsof для более прямой проверки того, что команда python была вызвана для прослушивания порта 8000:

$ sudo lsof -n -P -i TCP:8000 -s TCP:LISTEN
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python  43689 ubuntu    4u  IPv4  45944      0t0  TCP 127.0.0.1:8000 (LISTEN)

На данном этапе обучения ваше приложение прослушивает только localhost, то есть адрес 127.0.0.1. Оно еще не доступно из браузера, но вы все еще можете дать ему первого посетителя, отправив ему запрос GET из командной строки в самой виртуальной машине:

$ GET :8000/myapp/
HTTP/1.1 200 OK
Content-Length: 182
Content-Type: text/html; charset=utf-8
Date: Sat, 25 Sep 2021 00:11:38 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.10
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

Заголовок Server: WSGIServer/0.2 CPython/3.8.10 описывает программное обеспечение, сгенерировавшее ответ. В данном случае это версия 0.2 от WSGIServer наряду с CPython 3.8.10.

WSGIServer - это не более чем класс Python , определенный Django, который реализует протокол Python WSGI. Это означает, что он придерживается Web Server Gateway Interface (WSGI), который является стандартом, определяющим способ взаимодействия web server software и web applications

В нашем примере проект django-gunicorn-nginx/ - это веб-приложение. Поскольку вы обслуживаете приложение в процессе разработки, отдельного веб-сервера фактически не существует. Django использует модуль simple_server, который реализует легкий HTTP-сервер, и объединяет концепцию веб-сервера и сервера приложений в одну команду runserver.

Далее вы увидите, как начать знакомить свое приложение с большим временем, связав его с реальным доменом.

Размещение сайта в Интернете с помощью Django, Gunicorn и Nginx

На этом этапе ваш сайт доступен локально на вашей виртуальной машине. Если вы хотите, чтобы ваш сайт был доступен по реальному URL, вам нужно заявить доменное имя и привязать его к веб-серверу. Это также необходимо для включения HTTPS, поскольку некоторые центры сертификации не выдадут сертификат на пустой IP-адрес или поддомен, который вам не принадлежит. В этом разделе вы увидите, как зарегистрировать и настроить домен.

Установка статического публичного IP-адреса

Идеально, если вы можете указать конфигурацию вашего домена на публичный IP-адрес, который гарантированно не изменится. Одним из неоптимальных свойств облачных виртуальных машин является то, что их публичный IP-адрес может измениться, если экземпляр будет переведен в состояние остановки. Кроме того, если вам по какой-то причине потребуется заменить существующую ВМ на новый экземпляр, изменение IP-адреса будет проблематичным.

Решением этой дилеммы является привязка статического IP-адреса к экземпляру:

Следуйте документации поставщика облачных услуг, чтобы связать статический IP-адрес с вашей облачной виртуальной машиной. В среде AWS, использованной для примера в этом руководстве, с экземпляром EC2 был связан эластичный IP-адрес 50.19.125.152.

Примечание: Помните, что это означает, что вам нужно будет изменить целевой IP-адрес ssh для того, чтобы подключиться к вашей виртуальной машине по SSH:

$ ssh [args] my-new-static-public-ip

После обновления целевого IP вы сможете подключиться к вашей облачной виртуальной машине.

Получив более стабильный публичный IP перед вашей виртуальной машиной, вы готовы связать ее с доменом.

Ссылка на домен

В этом разделе вы узнаете, как приобрести, настроить и связать доменное имя с существующим приложением.

В этих примерах используется Namecheap, но, пожалуйста, не воспринимайте это как однозначное одобрение. Существует более чем достаточно других вариантов, таких как domain.com, GoDaddy и Google Domains. Что касается предвзятости, Namecheap заплатил ровно $0 за то, что был представлен в качестве регистратора доменов в этом руководстве.

Предупреждение: Если вы хотите обслуживать ваш сайт в разработке на публичном домене с DEBUG, установленным на True, вам необходимо создать пользовательские входящие правила безопасности, чтобы разрешить только IP-адреса вашего персонального компьютера и виртуальной машины. Вы должны не открывать любые входящие правила HTTP или HTTPS для 0.0.0.0 до тех пор, пока вы не отключите DEBUG как минимум.

Вот как вы можете начать:

  1. Создайте учетную запись на Namecheap, обязательно настроив двухфакторную аутентификацию (2FA).
  2. На главной странице начните поиск доменного имени, подходящего вашему бюджету. Вы обнаружите, что цены могут сильно различаться как в зависимости от домена верхнего уровня (TLD), так и имени хоста.
  3. Купите домен, когда будете довольны выбором.

В этом учебнике используется домен supersecure.codes, но у вас будет свой собственный.

Примечание: По мере прохождения этого руководства помните, что supersecure.codes - это всего лишь пример домена, который не поддерживается активно.

При выборе домена имейте в виду, что выбор более эзотерического названия сайта и домена верхнего уровня (TLD) обычно приводит к более низкой цене покупки домена. Это может быть особенно полезно для целей тестирования.

После того, как вы получите свой домен, вы захотите включить защиту WithheldForPrivacy, которая официально называется WhoisGuard. Это позволит скрыть вашу личную информацию, когда кто-то запускает whois поиск на вашем домене. Вот как это сделать:

  1. Select Account → Domain List.
  2. Select Manage next to your domain.
  3. Enable WithheldForPrivacy protection.

Далее необходимо настроить таблицу записей DNS для вашего сайта. Каждая запись DNS станет строкой в базе данных, которая сообщает браузеру, на какой базовый IP-адрес указывает полностью определенное доменное имя (FQDN). В данном случае мы хотим, чтобы supersecure.codes указывало на 50.19.125.152, публичный IPv4-адрес, по которому можно связаться с виртуальной машиной:

  1. Select Account → Domain List.
  2. Select Manage next to your domain.
  3. Select Advanced DNS.
  4. Under Host Records, add two A Records for your domain.

Добавьте записи A следующим образом, заменив 50.19.125.152 на публичный IPv4 адрес вашего экземпляра:

Тип Хост Значение TTL
Запись @ 50.19.125.152 Автоматическая
Запись www 50.19.125.152 Автоматическая

Запись A позволяет связать доменное имя или поддомен с IPv4-адресом веб-сервера, на котором обслуживается ваше приложение. Выше, в поле Value должен использоваться публичный IPv4-адрес вашего экземпляра виртуальной машины.

Вы видите, что есть два варианта для поля Host:

  1. Использование @ указывает на корневой домен, в данном случае supersecure.codes.
  2. Использование www означает, что www.supersecure.codes будет указывать на то же место, что и supersecure.codes. Технически www — это поддомен, который может отправлять пользователей в то же место, что и более короткий supersecure.codes.

После того, как вы установили таблицу записей хостов DNS, вам нужно подождать до тридцати минут, чтобы маршруты вступили в силу. Теперь вы можете завершить существующий процесс runserver:

$ jobs -l
[1]+ 43689 Running                 nohup python manage.py runserver &
$ kill 43689
[1]+  Done                    nohup python manage.py runserver

Вы можете подтвердить, что процесс завершен с помощью pgrep или снова проверить активные задания:

$ pgrep runserver  # Empty
$ jobs -l  # Empty or 'Done'
$ sudo lsof -n -P -i TCP:8000 -s TCP:LISTEN  # Empty
$ rm nohup.out

Вместе с этим, вам также нужно настроить параметры Django, ALLOWED_HOSTS, которые представляют собой набор доменных имен, которые вы позволяете обслуживать вашему приложению Django:

# project/settings.py
# Replace 'supersecure.codes' with your domain
ALLOWED_HOSTS = [".supersecure.codes"]

Ведущая точка (.) - это подстановочный знак поддомена, позволяющий использовать как www.supersecure.codes, так и supersecure.codes. Держите этот список в строгом порядке для предотвращения атак на заголовки хостов HTTP ..

Теперь вы можете перезапустить WSGIServer с одним небольшим изменением:

$ nohup python manage.py runserver '0.0.0.0:8000' &

Обратите внимание, что аргумент address:port теперь 0.0.0.0:8000, тогда как ранее он не был указан:

  • Указание нет address:port подразумевает обслуживание приложения на localhost:8000. Это означает, что приложение было доступно только из самой виртуальной машины. Вы могли общаться с ним, вызывая httpie с того же IP-адреса, но вы не могли связаться с вашим приложением из внешнего мира.

  • Указание address:port значения '0.0.0.0:8000' делает ваш сервер видимым для внешнего мира, хотя по умолчанию он все еще находится на порту 8000. 0.0.0.0 является сокращением для "привязки ко всем IP-адресам, которые поддерживает этот компьютер". В случае готовой облачной виртуальной машины с одним контроллером сетевого интерфейса (NIC) с именем eth0, использование 0.0.0.0 выступает в качестве замены публичного IPv4 адреса машины.

Далее, включите вывод nohup.out для просмотра всех входящих журналов с WSGIServer'а Django:

$ tail -f nohup.out

Теперь настал момент истины. Пришло время дать вашему сайту первого посетителя. С вашей персональной машины введите в веб-браузере следующий URL:

http://www.supersecure.codes:8000/myapp/

Замените доменное имя, указанное выше, на свое собственное. Вы должны увидеть быстрый отклик страницы во всей ее красе:

Now this is some sweet HTML!

Этот URL доступен для вас - но не для других - из-за правила безопасности для входящих, которое вы создали ранее.

Теперь вернитесь в оболочку вашей виртуальной машины. В непрерывном выводе tail -f nohup.out вы должны увидеть что-то вроде этой строки:

[<date>] "GET /myapp/ HTTP/1.1" 200 182

Поздравляем, вы только что сделали первый монументальный шаг к созданию собственного сайта! Однако здесь следует сделать паузу и обратить внимание на пару серьезных проблем, заложенных в URL http://www.supersecure.codes:8000/myapp/:

  • Сайт обслуживается только по протоколу HTTP. Без включения HTTPS ваш сайт фундаментально небезопасен, если вы хотите передать какие-либо конфиденциальные данные от клиента к серверу или наоборот. Использование HTTP означает, что запросы и ответы отправляются открытым текстом. Вы скоро это исправите.

  • URL использует нестандартный порт 8000 вместо стандартного HTTP-порта по умолчанию 80. Это нестандартно и немного бросается в глаза, но вы пока не можете использовать 80. Это потому, что порт 80 является привилегированным, и пользователь, не являющийся пользователем root, не может - и не должен - привязываться к нему. Позже вы внедрите инструмент, который позволит вашему приложению быть доступным на порту 80.

Если вы проверите в своем браузере, вы увидите строку URL вашего браузера, намекающую на это. Если вы используете Firefox, появится красный значок замка, указывающий на то, что соединение осуществляется по протоколу HTTP, а не HTTPS:

HTTP page emphasizing insecure icon

В дальнейшем вы хотите узаконить операцию. Вы можете начать обслуживание через стандартный порт 80 для HTTP. Еще лучше начать обслуживать HTTPS (443) и перенаправлять туда HTTP-запросы. Скоро вы увидите, как выполнить эти шаги.

Замена WSGIServer на Gunicorn

Вы хотите начать продвигать свое приложение к состоянию, когда оно готово к внешнему миру? Если да, то вам следует заменить встроенный в Django WSGIServer, который является сервером приложений, используемым manage.py runserver, на отдельный выделенный сервер приложений. Но подождите минутку: WSGIServer, казалось, работает просто отлично. Зачем его заменять?

Чтобы ответить на этот вопрос, вы можете прочитать, что говорит документация Django:

НЕ ИСПОЛЬЗУЙТЕ ЭТОТ СЕРВЕР В ПРОИЗВОДСТВЕННЫХ УСЛОВИЯХ. Он не проходил аудита безопасности или тестов производительности. (И так оно и останется. Мы занимаемся созданием веб-фреймворков, а не веб-серверов, поэтому улучшение этого сервера для работы в производственной среде выходит за рамки Django). (Source)

Django - это web framework, а не веб-сервер, и его сопровождающие хотят сделать это различие ясным. В этом разделе вы замените команду Django runserver на Gunicorn. Gunicorn - это, прежде всего, сервер приложений WSGI на языке Python, причем проверенный в боях:

  • Он быстр, оптимизирован и предназначен для производства.
  • Это дает вам более тонкий контроль над самим сервером приложений.
  • У него более полное и настраиваемое протоколирование.
  • Это хорошо протестировано, в частности, на функциональность сервера приложений.

Вы можете установить Gunicorn через pip в вашу виртуальную среду:

$ pwd
/home/ubuntu
$ source env/bin/activate
$ python -m pip install 'gunicorn==20.1.*'

Далее необходимо выполнить некоторый уровень конфигурации. Самое замечательное в конфигурационном файле Gunicorn то, что он должен быть просто корректным кодом Python, с именами переменных, соответствующих аргументам. Вы можете хранить несколько конфигурационных файлов Gunicorn в подкаталоге проекта:

$ cd ~/django-gunicorn-nginx
$ mkdir -pv config/gunicorn/
mkdir: created directory 'config'
mkdir: created directory 'config/gunicorn/'

Далее, откройте файл конфигурации разработки, config/gunicorn/dev.py, и добавьте следующее:

"""Gunicorn *development* config file"""

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "project.wsgi:application"
# The granularity of Error log outputs
loglevel = "debug"
# The number of worker processes for handling requests
workers = 2
# The socket to bind
bind = "0.0.0.0:8000"
# Restart workers when code changes (development only!)
reload = True
# Write access and error info to /var/log
accesslog = errorlog = "/var/log/gunicorn/dev.log"
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
pidfile = "/var/run/gunicorn/dev.pid"
# Daemonize the Gunicorn process (detach & enter background)
daemon = True

Перед запуском Gunicorn необходимо остановить процесс runserver. Используйте jobs, чтобы найти его, и kill, чтобы остановить его:

$ jobs -l
[1]+ 26374 Running                 nohup python manage.py runserver &
$ kill 26374
[1]+  Done                    nohup python manage.py runserver

Далее, убедитесь, что каталоги log и PID существуют для значений, установленных в конфигурационном файле Gunicorn выше:

$ sudo mkdir -pv /var/{log,run}/gunicorn/
mkdir: created directory '/var/log/gunicorn/'
mkdir: created directory '/var/run/gunicorn/'
$ sudo chown -cR ubuntu:ubuntu /var/{log,run}/gunicorn/
changed ownership of '/var/log/gunicorn/' from root:root to ubuntu:ubuntu
changed ownership of '/var/run/gunicorn/' from root:root to ubuntu:ubuntu

С помощью этих команд вы убедились, что необходимые каталоги PID и log существуют для Gunicorn, и что они доступны для записи пользователю ubuntu.

После этого вы можете запустить Gunicorn, используя флаг -c для указания на файл конфигурации из корня проекта:

$ pwd
/home/ubuntu/django-gunicorn-nginx
$ source .DJANGO_SECRET_KEY
$ gunicorn -c config/gunicorn/dev.py

Это запускается gunicorn в фоновом режиме с файлом конфигурации разработки dev.py, который вы указали выше. Как и раньше, вы можете следить за выходным файлом, чтобы увидеть вывод, регистрируемый Gunicorn:

$ tail -f /var/log/gunicorn/dev.log
[2021-09-27 01:29:50 +0000] [49457] [INFO] Starting gunicorn 20.1.0
[2021-09-27 01:29:50 +0000] [49457] [DEBUG] Arbiter booted
[2021-09-27 01:29:50 +0000] [49457] [INFO] Listening at: http://0.0.0.0:8000 (49457)
[2021-09-27 01:29:50 +0000] [49457] [INFO] Using worker: sync
[2021-09-27 01:29:50 +0000] [49459] [INFO] Booting worker with pid: 49459
[2021-09-27 01:29:50 +0000] [49460] [INFO] Booting worker with pid: 49460
[2021-09-27 01:29:50 +0000] [49457] [DEBUG] 2 workers

Теперь снова зайдите на URL вашего сайта в браузере. Вам все еще понадобится порт 8000:

http://www.supersecure.codes:8000/myapp/

Снова проверьте свой терминал VM. Вы должны увидеть одну или несколько строк, подобных следующей, из файла журнала Gunicorn:

67.xx.xx.xx - - [27/Sep/2021:01:30:46 +0000] "GET /myapp/ HTTP/1.1" 200 182

Эти строки являются журналами доступа, которые рассказывают о входящих запросах:

Компонент Смысл
67.xx.xx.xx IP-адрес пользователя
27/Sep/2021:01:30:46 +0000 Временная метка запроса
GET Метод запроса
/myapp/ Путь к URL
HTTP/1.1 Протокол
200 Код статуса ответа
182 Длина содержимого ответа

Для краткости выше исключен user agent, который также может появиться в вашем журнале. Вот пример из браузера Firefox на macOS:

Mozilla/5.0 (Macintosh; Intel Mac OS X ...) Gecko/20100101 Firefox/92.0

После того, как Gunicorn работает и слушается, пришло время ввести в уравнение и легитимный веб-сервер.

Встраивание Nginx

На данном этапе вы отказались от команды runserver Django в пользу gunicorn в качестве сервера приложений. Осталось добавить в цепочку запросов еще одного игрока: веб-сервер , например Nginx.

Постойте - вы уже добавили Гуникорна! Зачем вам нужно добавлять что-то новое? Причина в том, что Nginx и Gunicorn - это две разные вещи, и они сосуществуют и работают как одна команда.

Nginx определяет себя как высокопроизводительный веб-сервер и обратный прокси-сервер. Стоит разбить это на части, потому что это помогает объяснить связь Nginx с Gunicorn и Django.

Во-первых, Nginx - это веб-сервер, который может предоставлять файлы веб-пользователю или клиенту. Файлы - это буквальные документы: HTML, CSS, PNG, PDF - называйте как хотите. В старые времена, до появления таких фреймворков, как Django, было принято считать, что веб-сайт функционирует, по сути, как прямой доступ к файловой системе. В пути URL косые черты обозначали каталоги в ограниченной части файловой системы сервера, которые вы могли запросить для просмотра.

Обратите внимание на тонкую разницу в терминологии:

  • Django - это веб-фреймворк. Он позволяет вам создать основное веб-приложение, которое обеспечивает работу с фактическим содержимым сайта. Он обрабатывает рендеринг HTML, аутентификацию, администрирование и логику бэкенда.

  • Gunicorn - это сервер приложений. Он переводит HTTP-запросы в то, что может понять Python. Gunicorn реализует Web Server Gateway Interface (WSGI), который является стандартным интерфейсом между программным обеспечением веб-сервера и веб-приложениями.

  • Nginx - это веб-сервер. Это публичный обработчик, более формально называемый обратным прокси, для входящих запросов и масштабируется до тысяч одновременных соединений.

Частью роли Nginx как веб-сервера является то, что он может более эффективно обслуживать статические файлы. Это означает, что для запросов на статическое содержимое, например, изображения, вы можете отказаться от посредника в лице Django и поручить Nginx напрямую отрисовывать файлы. Мы перейдем к этому важному шагу позже в учебнике.

Nginx также является обратным прокси-сервером, поскольку он стоит между внешним миром и вашим приложением Gunicorn/Django. Точно так же, как вы можете использовать прокси для отправки исходящих запросов, вы можете использовать такой прокси, как Nginx, для их получения:

Finalized configuration of Nginx and Gunicorn.
 

Чтобы начать использовать Nginx, установите его и проверьте его версию:

$ sudo apt-get install -y 'nginx=1.18.*'
$ nginx -v  # Display version info
nginx version: nginx/1.18.0 (Ubuntu)

Затем вам следует изменить правила inbound-allow, которые вы установили для порта 8000 на порт 80. Замените входящее правило для TCP:8000 следующим:

Тип Протокол Диапазон портов Источник
HTTP TCP 80 my-laptop-ip-address/32

Другие правила, например, правила доступа к SSH, должны остаться неизменными.

Теперь запустите службу nginx и убедитесь, что ее статус running:

$ sudo systemctl start nginx
$ sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; ...
   Active: active (running) since Mon 2021-09-27 01:37:04 UTC; 2min 49s ago
...

Теперь вы можете сделать запрос к знакомому URL:

http://supersecure.codes/

Это большая разница по сравнению с тем, что было раньше. Вам больше не нужен порт 8000 в URL. Вместо этого по умолчанию используется порт 80, что выглядит гораздо более нормально:

Welcome to nginx!

Это дружественная функция Nginx. Если вы запустите Nginx с нулевой конфигурацией, он выдаст вам страницу, показывающую, что он прослушивает. Теперь попробуйте запустить страницу /myapp по следующему URL:

http://supersecure.codes/myapp/

Не забудьте заменить supersecure.codes на ваше собственное доменное имя.

Вы должны увидеть ответ 404, и это нормально:

Nginx 404 page

Это происходит потому, что вы запрашиваете путь /myapp через порт 80, который прослушивает Nginx, а не Gunicorn. На данный момент у вас следующая установка:

  • Nginx прослушивает порт 80.
  • Gunicorn прослушивает, отдельно, порт 8000.

Между ними нет никакой связи или привязки, пока вы ее не укажете. Nginx не знает, что у Gunicorn и Django есть какой-то замечательный HTML, который они хотят показать миру. Вот почему он возвращает ответ 404 Not Found. Вы еще не настроили его на прокси запросы к Gunicorn и Django:

Nginx disconnected from Gunicorn.

Вам нужно задать Nginx некоторую базовую конфигурацию, чтобы он направлял запросы в Gunicorn, который затем передаст их в Django. Откройте /etc/nginx/sites-available/supersecure и добавьте следующее содержимое:

server_tokens               off;
access_log                  /var/log/nginx/supersecure.access.log;
error_log                   /var/log/nginx/supersecure.error.log;

# This configuration will be changed to redirect to HTTPS later
server {
  server_name               .supersecure.codes;
  listen                    80;
  location / {
    proxy_pass              http://localhost:8000;
    proxy_set_header        Host $host;
  }
}

Помните, что вам нужно заменить supersecure в имени файла на имя хоста вашего сайта, и обязательно заменить server_name значение .supersecure.codes на ваш собственный домен с префиксом в виде точки.

Примечание: Вам, вероятно, понадобится sudo для открытия файлов под /etc.

Этот файл является "Hello World" конфигурации обратного прокси-сервера Nginx. Он указывает Nginx, как себя вести:

  • Слушайте порт 80 для запросов, которые используют хост для supersecure.codes и его поддоменов.
  • Передайте эти запросы на порт http://localhost:8000, который прослушивает Gunicorn.

Поле proxy_set_header является важным. Оно гарантирует, что Nginx передаст Gunicorn и Django заголовок запроса HTTP Host, отправленный конечным пользователем. В противном случае Nginx будет использовать Host: localhost по умолчанию, игнорируя поле заголовка Host, посылаемое браузером конечного пользователя.

Вы можете проверить свой конфигурационный файл, используя nginx configtest:

$ sudo service nginx configtest /etc/nginx/sites-available/supersecure
 * Testing nginx configuration                                  [ OK ]

Вывод [ OK ] указывает на то, что файл конфигурации действителен и может быть разобран.

Теперь вам нужно symlink этот файл в каталог sites-enabled, заменив supersecure на домен вашего сайта:

$ cd /etc/nginx/sites-enabled
$ # Note: replace 'supersecure' with your domain
$ sudo ln -s ../sites-available/supersecure .
$ sudo systemctl restart nginx

Перед тем, как сделать запрос к вашему сайту с помощью httpie, вам нужно добавить еще одно входящее правило безопасности. Добавьте следующее входящее правило:

Тип Протокол Диапазон портов Источник
HTTP TCP 80 vm-static-ip-address/32

Это правило безопасности разрешает входящий HTTP трафик с публичного (эластичного) IP адреса самой виртуальной машины. Сначала это может показаться излишеством, но это необходимо сделать, поскольку запросы теперь будут направляться через публичный интернет, а значит, самореферентного правила, использующего ID группы безопасности, будет уже недостаточно.

Теперь, когда он использует Nginx в качестве фронтенда веб-сервера, повторно отправьте запрос на сайт:

$ GET http://supersecure.codes/myapp/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Mon, 27 Sep 2021 19:54:19 GMT
Referrer-Policy: same-origin
Server: nginx
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

Теперь, когда Nginx сидит перед Django и Gunicorn, здесь есть несколько интересных результатов:

  • Nginx теперь возвращает заголовок Server в виде Server: nginx, указывая, что Nginx является новым внешним веб-сервером. Установка server_tokens в значение off указывает Nginx не выдавать свою точную версию, например nginx/x.y.z (Ubuntu). С точки зрения безопасности это было бы раскрытием ненужной информации.
  • Nginx использует chunked для заголовка Transfer-Encoding вместо рекламы Content-Length.
  • Nginx также просит держать открытым сетевое соединение с помощью Connection: keep-alive.

Далее вы будете использовать Nginx для одной из его основных функций: возможности быстро и эффективно обслуживать статические файлы.

Отдача статических файлов напрямую с помощью Nginx

Теперь у вас есть Nginx, проксирующий запросы к вашему приложению Django. Важно отметить, что вы также можете использовать Nginx для обслуживания статических файлов напрямую. Если у вас DEBUG = True в project/settings.py, то Django будет рендерить файлы, но это крайне неэффективно и, вероятно, небезопасно. Вместо этого вы можете поручить вашему веб-серверу рендерить их напрямую.

Обычные примеры статических файлов включают локальный JavaScript, изображения и CSS - все, где Django не нужен в качестве части уравнения для динамического отображения содержимого ответа.

Для начала в каталоге вашего проекта создайте место для хранения и отслеживания статических файлов JavaScript в процессе разработки:

$ pwd
/home/ubuntu/django-gunicorn-nginx
$ mkdir -p static/js

Теперь откройте новый файл static/js/greenlight.js и добавьте следующий JavaScript:

// Enlarge the #changeme element in green when hovered over
(function () {
    "use strict";
    function enlarge() {
        document.getElementById("changeme").style.color = "green";
        document.getElementById("changeme").style.fontSize = "xx-large";
        return false;
    }
    document.getElementById("changeme").addEventListener("mouseover", enlarge);
}());

Этот JavaScript сделает блок текста большим зеленым шрифтом при наведении на него курсора. Да, это передовой фронтенд!

Далее, добавьте следующую конфигурацию в project/settings.py, обновляя STATIC_ROOT своим доменным именем:

STATIC_URL = "/static/"
# Note: Replace 'supersecure.codes' with your domain
STATIC_ROOT = "/var/www/supersecure.codes/static"
STATICFILES_DIRS = [BASE_DIR / "static"]

Вы указываете команде Django collectstatic где искать и размещать результирующие статические файлы, которые собираются из нескольких приложений Django, включая собственные встроенные приложения Django, такие как admin.

В последнюю очередь измените HTML в myapp/templates/myapp/home.html, чтобы включить JavaScript, который вы только что создали:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

Включив сценарий /static/js/greenlight.js, элемент <span id="changeme"> будет иметь прикрепленный к нему слушатель событий.

Примечание: Чтобы сохранить этот пример простым, вы жестко кодируете путь URL в greenlight.js вместо того, чтобы использовать тег шаблона Django static. Вы захотите воспользоваться этой возможностью в более крупном проекте.

Следующим шагом будет создание пути к каталогу, в котором будет храниться статическое содержимое вашего проекта для обслуживания Nginx:

$ sudo mkdir -pv /var/www/supersecure.codes/static/
mkdir: created directory '/var/www/supersecure.codes'
mkdir: created directory '/var/www/supersecure.codes/static/'
$ sudo chown -cR ubuntu:ubuntu /var/www/supersecure.codes/
changed ownership of '/var/www/supersecure.codes/static' ... to ubuntu:ubuntu
changed ownership of '/var/www/supersecure.codes/' ... to ubuntu:ubuntu

Теперь запустите collectstatic от имени пользователя, не являющегося пользователем root, из каталога вашего проекта:

$ pwd
/home/ubuntu/django-gunicorn-nginx
$ python manage.py collectstatic
129 static files copied to '/var/www/supersecure.codes/static'.

Наконец, добавьте переменную location для /static в /etc/nginx/sites-available/supersecure, файл конфигурации вашего сайта для Nginx:

server {
  location / {
    proxy_pass          http://localhost:8000;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-Proto $scheme;
  }

  location /static {
    autoindex on;
    alias /var/www/supersecure.codes/static/;
  }
}

Помните, что ваш домен, вероятно, не supersecure.codes, поэтому вам нужно будет адаптировать эти шаги для работы с вашим собственным проектом.

Теперь вы должны выключить режим DEBUG в вашем проекте в project/settings.py:

# project/settings.py
DEBUG = False

Gunicorn подхватит это изменение, поскольку вы указали reload = True в config/gunicorn/dev.py.

Затем перезапустите Nginx:

$ sudo systemctl restart nginx

Теперь снова обновите страницу сайта и наведите курсор на текст страницы:

Result of JavaScript enlarge being called on mouseover

Это явное свидетельство того, что сработала функция JavaScript enlarge(). Чтобы получить этот результат, браузер должен был запросить /static/js/greenlight.js. Ключевым здесь является то, что браузер получил этот файл непосредственно из Nginx без того, чтобы Nginx запрашивал его у Django.

Заметьте кое-что другое в приведенном выше процессе: нигде вы не добавили новый маршрут URL Django или представление для доставки файла JavaScript. Это потому, что после выполнения collectstatic, Django больше не отвечает за определение того, как сопоставить URL со сложным представлением и отрисовать это представление. Nginx может просто передать файл непосредственно браузеру.

Фактически, если вы перейдете к эквиваленту вашего домена https://supersecure.codes/static/js/, вы увидите традиционное представление дерева файловой системы /static, созданное Nginx. Это означает более быструю и эффективную доставку статических файлов.

На данный момент у вас есть отличная основа для создания масштабируемого сайта с помощью Django, Gunicorn и Nginx. Еще один огромный шаг вперед - включить HTTPS для вашего сайта, что вы и сделаете далее.

Сделаем ваш сайт готовым к производству с помощью HTTPS

Вы можете повысить уровень безопасности вашего сайта с хорошего до отличного, сделав еще несколько шагов, включая включение HTTPS и добавление набора заголовков, которые помогают веб-браузерам работать с вашим сайтом в более безопасном режиме. Включение HTTPS повышает надежность вашего сайта, и это необходимо, если ваш сайт использует аутентификацию или обменивается конфиденциальными данными с пользователями.

Включение HTTPS

Чтобы посетители могли получить доступ к вашему сайту через HTTPS, вам понадобится сертификат SSL/TLS, который находится на вашем веб-сервере. Сертификаты выдаются центром сертификации (ЦС). В этом руководстве вы будете использовать бесплатный центр сертификации под названием Let's Encrypt. Чтобы установить сертификат, вы можете использовать клиент Certbot, который предоставляет вам совершенно безболезненную пошаговую серию подсказок.

Перед началом работы с Certbot вы можете заранее указать Nginx на отключение TLS версий 1.0 и 1.1 в пользу версий 1.2 и 1.3. TLS 1.0 устарел, а TLS 1.1 содержал несколько уязвимостей, которые были устранены в TLS 1.2. Для этого откройте файл /etc/nginx/nginx.conf. Найдите следующую строку:

# File: /etc/nginx/nginx.conf
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

Замените его более современными реализациями:

# File: /etc/nginx/nginx.conf
ssl_protocols TLSv1.2 TLSv1.3;

Вы можете использовать nginx -t для подтверждения того, что ваш Nginx поддерживает версию 1.3:

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Теперь вы готовы к установке и использованию Certbot. На Ubuntu Focal (20.04) вы можете использовать snap для установки Certbot:

$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

Обратитесь к руководству Certbot instructions guide, чтобы посмотреть шаги по установке для различных операционных систем и веб-серверов.

Прежде чем вы сможете получить и установить сертификаты HTTPS с помощью certbot, необходимо внести еще одно изменение в правила группы безопасности вашей виртуальной машины. Поскольку Let's Encrypt требует подключения к Интернету для проверки, вам нужно сделать важный шаг - открыть ваш сайт для публичного доступа в Интернет.

Измените правила безопасности входящих соединений так, чтобы они соответствовали следующим:

Ссылка Тип Протокол Диапазон портов Источник
1 HTTP TCP 80 0.0.0.0/0
2 Обычный Все Все security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

Ключевым изменением здесь является первое правило, которое разрешает HTTP-трафик через порт 80 из всех источников. Вы можете удалить входящее правило для TCP:80, в котором был белый список публичного IP-адреса вашей виртуальной машины, поскольку оно теперь является избыточным. Два других правила остаются неизменными.

Затем вы можете выполнить еще одну команду certbot для установки сертификата:

$ sudo certbot --nginx --rsa-key-size 4096 --no-redirect
Saving debug log to /var/log/letsencrypt/letsencrypt.log
...

Это создает сертификат с размером ключа RSA 4096 байт. Опция --no-redirect указывает certbot не применять автоматически конфигурацию, связанную с автоматическим перенаправлением с HTTP на HTTPS. В качестве иллюстрации вы увидите, как добавить это самостоятельно.

Вы пройдете ряд шагов по настройке, большинство из которых не требует пояснений, например, ввод адреса электронной почты. Когда появится запрос на ввод доменных имен, введите домен и www поддомен, разделенные запятой:

www.supersecure.codes,supersecure.codes

После того, как вы выполнили все шаги, вы должны увидеть сообщение об успехе, подобное следующему:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/supersecure.codes/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/supersecure.codes/privkey.pem
This certificate expires on 2021-12-26.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this
  certificate in the background.

Deploying certificate
Successfully deployed certificate for supersecure.codes
  to /etc/nginx/sites-enabled/supersecure
Successfully deployed certificate for www.supersecure.codes
  to /etc/nginx/sites-enabled/supersecure
Congratulations! You have successfully enabled HTTPS
  on https://supersecure.codes and https://www.supersecure.codes

Если вы cat откроете конфигурационный файл в вашем эквиваленте /etc/nginx/sites-available/supersecure, вы увидите, что certbot автоматически добавил группу строк, связанных с SSL:

# Nginx configuration: /etc/nginx/sites-available/supersecure
server {
  server_name               .supersecure.codes;
  listen                    80;
  location / {
    proxy_pass              http://localhost:8000;
    proxy_set_header        Host $host;
  }

  location /static {
    autoindex on;
    alias /var/www/supersecure.codes/static/;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.supersecure.codes/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.supersecure.codes/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

Убедитесь, что Nginx подхватил эти изменения:

$ sudo systemctl reload nginx

Для доступа к вашему сайту через HTTPS вам понадобится последнее дополнение к правилам безопасности. Вам нужно разрешить трафик через TCP:443, где 443 - порт по умолчанию для HTTPS. Измените ваши входящие правила безопасности так, чтобы они соответствовали следующим:

Ссылка Тип Протокол Диапазон портов Источник
1 HTTPS TCP 443 0.0.0.0/0
2 HTTP TCP 80 0.0.0.0/0
2 Заказной Все Все security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

Каждое из этих правил имеет конкретное назначение:

  1. Правило 1 разрешает трафик HTTPS через порт 443 из всех источников.
  2. Правило 2 разрешает HTTP-трафик через порт 80 из всех источников.
  3. Правило 3 разрешает входящий трафик от сетевых интерфейсов и экземпляров, назначенных одной и той же группе безопасности, с использованием идентификатора группы безопасности в качестве источника. Это правило включено в группу безопасности AWS по умолчанию, которую вы должны привязать к своему экземпляру.
  4. Правило 4 позволяет вам получить доступ к вашей виртуальной машине через SSH с вашего персонального компьютера.

Теперь снова перейдите на свой сайт в браузере, но с одним ключевым отличием. Вместо http укажите https в качестве протокола:

https://www.supersecure.codes/myapp/

Если все хорошо, вы должны увидеть одно из прекрасных сокровищ жизни - доставку вашего сайта по HTTPS:

Connecting to your Django app over HTTPS

 

You are securely connected to this site

Вы на один шаг ближе к безопасному веб-сайту. На данный момент сайт все еще доступен как по HTTP, так и по HTTPS. Это лучше, чем раньше, но все еще не идеально.

Перенаправление HTTP на HTTPS

Теперь ваш сайт доступен как по HTTP, так и по HTTPS. Когда HTTPS работает, вы можете практически полностью отключить HTTP - или, по крайней мере, приблизиться к этому на практике. Вы можете добавить несколько функций для автоматического перенаправления посетителей, пытающихся получить доступ к вашему сайту по HTTP, на версию HTTPS. Отредактируйте ваш эквивалент /etc/nginx/sites-available/supersecure:

# Nginx configuration: /etc/nginx/sites-available/supersecure
server {
  server_name               .supersecure.codes;
  listen                    80;
  return                    307 https://$host$request_uri;
}

server {
  location / {
    proxy_pass              http://localhost:8000;
    proxy_set_header        Host $host;
  }

  location /static {
    autoindex on;
    alias /var/www/supersecure.codes/static/;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.supersecure.codes/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.supersecure.codes/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

Добавленный блок указывает серверу перенаправлять браузер или клиента на HTTPS-версию любого HTTP URL. Вы можете убедиться, что эта конфигурация действительна:

$ sudo service nginx configtest /etc/nginx/sites-available/supersecure
 * Testing nginx configuration                                  [ OK ]

Затем скажите nginx, чтобы перезагрузить конфигурацию:

$ sudo systemctl reload nginx

Затем отправьте запрос GET с флагом --all на HTTP URL вашего приложения, чтобы отобразить все цепочки перенаправления:

$ GET --all http://supersecure.codes/myapp/
HTTP/1.1 307 Temporary Redirect
Connection: keep-alive
Content-Length: 164
Content-Type: text/html
Date: Tue, 28 Sep 2021 02:16:30 GMT
Location: https://supersecure.codes/myapp/
Server: nginx

<html>
<head><title>307 Temporary Redirect</title></head>
<body bgcolor="white">
<center><h1>307 Temporary Redirect</h1></center>
<hr><center>nginx</center>
</body>
</html>

HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:16:30 GMT
Referrer-Policy: same-origin
Server: nginx
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

Вы видите, что на самом деле здесь есть два ответа:

  1. Первоначальный запрос получает ответ с кодом состояния 307, перенаправляющий на версию HTTPS.
  2. Второй запрос выполняется на тот же URI, но по схеме HTTPS, а не HTTP. На этот раз он получает содержимое страницы, которое искал, с ответом 200 OK.

Далее вы увидите, как выйти на один шаг за рамки настройки перенаправления, помогая браузерам запомнить этот выбор.

Сделайте еще один шаг вперед с HSTS

В этой настройке редиректа есть небольшая уязвимость при изолированном использовании:

Когда пользователь вводит веб-домен вручную (указывая имя домена без префикса http:// или https://) или переходит по обычной ссылке http://, первый запрос на веб-сайт отправляется в незашифрованном виде, используя обычный HTTP.

Большинство защищенных веб-сайтов сразу же отправляют перенаправление, чтобы перевести пользователя на HTTPS-соединение, но хорошо расположенный злоумышленник может организовать атаку "человек посередине" (MITM), чтобы перехватить первый HTTP-запрос и с этого момента контролировать сессию пользователя. (Источник)

Чтобы облегчить эту проблему, можно добавить политику HSTS, чтобы указать браузерам предпочесть HTTPS, даже если пользователь пытается использовать HTTP. Вот тонкая разница между использованием только редиректа и добавлением заголовка HSTS в дополнение к нему:

Чтобы исправить это, вы можете сказать Django установить заголовок Strict-Transport-Security. Добавьте эти строки в settings.py:

  • При обычном перенаправлении с HTTP на HTTPS сервер отвечает браузеру словами: "Попробуй еще раз, но с HTTPS". Если браузер сделает 1 000 HTTP-запросов, ему будет сказано 1 000 раз повторить попытку с HTTPS.

  • При использовании заголовка HSTS браузер выполняет предварительную работу по эффективной замене HTTP на HTTPS после первого запроса. Перенаправления не происходит. Во втором сценарии браузер можно рассматривать как улучшающее соединение. Когда пользователь просит свой браузер посетить HTTP-версию вашего сайта, браузер отрывисто отвечает: "Нет, я веду вас на HTTPS-версию".

Чтобы исправить это, вы можете указать Django установить заголовок Strict-Transport-Security. Добавьте эти строки в файл settings.py вашего проекта:

# Add to project/settings.py
SECURE_HSTS_SECONDS = 30  # Unit is seconds; *USE A SMALL VALUE FOR TESTING!*
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Обратите внимание, что значение SECURE_HSTS_SECONDS недолговечно - 30 секунд. В данном примере это сделано намеренно. Когда вы перейдете к реальному производству, вам следует увеличить это значение. На сайте Security Headers рекомендуется минимальное значение 2,592,000, равное 30 дням.

Предупреждение: Прежде чем увеличивать значение SECURE_HSTS_SECONDS, прочитайте объяснение строгой транспортной безопасности HTTP в Django. Прежде чем устанавливать большое значение временного окна HSTS, вы должны сначала убедиться, что HTTPS работает на вашем сайте. Увидев заголовок, браузеры не легко позволят вам отменить это решение и будут настаивать на использовании HTTPS вместо HTTP.

Некоторые браузеры, такие как Chrome, могут позволить вам отменить это поведение и редактировать списки политик HSTS, но вы не должны полагаться на этот трюк. Это будет не очень удобным для пользователя. Вместо этого сохраняйте небольшое значение для SECURE_HSTS_SECONDS до тех пор, пока вы не убедитесь, что ваш сайт не испытывает никаких регрессий при обслуживании по HTTPS.

Когда вы будете готовы приступить к работе, вам нужно будет добавить еще одну строку конфигурации Nginx. Отредактируйте ваш эквивалент /etc/nginx/sites-available/supersecure, чтобы добавить директиву proxy_set_header:

  location / {
    proxy_pass          http://localhost:8000;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-Proto $scheme;
  }

Затем скажите Nginx перезагрузить обновленную конфигурацию:

$ sudo systemctl reload nginx

В результате этого добавления proxy_set_header Nginx отправляет Django следующий заголовок, включенный в промежуточные запросы, которые изначально отправляются на веб-сервер через HTTPS на порт 443:

X-Forwarded-Proto: https

Это напрямую связано со значением SECURE_PROXY_SSL_HEADER, которое вы добавили выше в project/settings.py. Это необходимо, потому что Nginx фактически посылает обычные HTTP запросы в Gunicorn/Django, поэтому у Django нет другого способа узнать, был ли исходный запрос HTTPS. Поскольку блок location из конфигурационного файла Nginx выше предназначен для порта 443 (HTTPS), все запросы, проходящие через этот порт, должны дать Django знать, что они действительно HTTPS.

Документация Django объясняет это достаточно хорошо:

Однако, если ваше приложение Django находится за прокси-сервером, прокси-сервер может «проглатывать» независимо от того, использует ли исходный запрос HTTPS или нет. Если между прокси-сервером и Django существует не-HTTPS-соединение, то is_secure() всегда будет возвращать False — даже для запросов, сделанных через HTTPS конечным пользователем. Напротив, если между прокси-сервером и Django есть HTTPS-соединение, is_secure() всегда будет возвращать True — даже для запросов, которые изначально были сделаны через HTTP.

Как проверить, что этот заголовок работает? Вот элегантный способ, который позволяет вам оставаться в пределах браузера:

Если вы следите за работой Firefox, вы должны увидеть что-то вроде следующего:

Immediate 200 OK response with HSTS header

Последнее, вы также можете проверить наличие заголовка с помощью запроса из консоли:

$ GET -ph https://supersecure.codes/myapp/
...
Strict-Transport-Security: max-age=30; includeSubDomains; preload

Это свидетельство того, что вы эффективно установили заголовок Strict-Transport-Security, используя соответствующие значения в project/settings.py. Как только вы будете готовы, вы можете увеличить значение max-age, но помните, что это необратимо скажет браузеру обновить HTTP на такой-то период времени.

Установка заголовка Referrer-Policy

Django 3.x также добавил возможность управлять заголовком Referrer-Policy. Вы можете указать SECURE_REFERRER_POLICY в project/settings.py:

# Add to project/settings.py
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"

Как работает эта настройка? Когда вы переходите по ссылке со страницы A на страницу B, ваш запрос к странице B содержит URL страницы A под заголовком Referer. Сервер, устанавливающий заголовок Referrer-Policy, который вы можете установить в Django с помощью SECURE_REFERRER_POLICY, контролирует, когда и сколько информации передается на целевой сайт. SECURE_REFERRER_POLICY может принимать ряд распознаваемых значений, о которых вы можете подробно прочитать в Mozilla docs.

В качестве примера, если вы используете "strict-origin-when-cross-origin" и текущей страницей пользователя является https://example.com/page, заголовок Referer ограничивается следующим образом:

Целевой сайт RefererЗаголовок
https://example.com/otherpage https://example.com/page
https://mozilla.org https://example.com/
http://example.org (цель HTTP) [None]

Вот что происходит в каждом конкретном случае, если предположить, что страница текущего пользователя имеет вид https://example.com/page:

Если вы добавите эту строку в project/settings.py и повторно запросите домашнюю страницу вашего приложения, то вы увидите нового участника:

$ GET -ph https://supersecure.codes/myapp/  # -ph: Show response headers only
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:31:36 GMT
Referrer-Policy: strict-origin-when-cross-origin
Server: nginx
Strict-Transport-Security: max-age=30; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

В этом разделе вы сделали еще один шаг к защите конфиденциальности ваших пользователей. Далее вы увидите, как снизить уязвимость вашего сайта к межсайтовому скриптингу (XSS) и атакам с использованием инъекций данных.

Добавление заголовка Content-Security-Policy (CSP)

Еще один важный заголовок HTTP ответа, который вы можете добавить на свой сайт - это заголовок Content-Security-Policy (CSP), который помогает предотвратить межсайтовый скриптинг (XSS) и атаки внедрения данных. Django не поддерживает эту функцию изначально, но вы можете установить django-csp, небольшое промежуточное расширение, разработанное Mozilla:

$ python -m pip install django-csp

Чтобы включить заголовок со значением по умолчанию, добавьте эту единственную строку в project/settings.py под существующим определением MIDDLEWARE:

# project/settings.py
MIDDLEWARE += ["csp.middleware.CSPMiddleware"]

Как вы можете проверить это на практике? Ну, вы можете включить ссылку в одну из ваших HTML-страниц и посмотреть, позволит ли браузер загрузить ее вместе с остальной частью страницы.

Отредактируйте шаблон по адресу myapp/templates/myapp/home.html, чтобы включить ссылку на файл Normalize.css, который представляет собой CSS-файл, помогающий браузерам отображать все элементы более последовательно и в соответствии с современными стандартами:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
    <link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css"
    >
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

Теперь запросите страницу в браузере с включенными инструментами разработчика. В консоли вы увидите ошибку, подобную следующей:

The page's settings blocked the loading of a resource

О-о-о. Вы упускаете возможности нормализации, потому что ваш браузер не загружает normalize.css. Вот почему он не загружается:

  • Ваш проект/settings.py включает CSPMiddleware в MIDDLEWARE Django. Включение CSPMiddleware устанавливает для заголовка значение Content-Security-Policy по умолчанию, которое равно default-src 'self', где 'self' означает собственный домен вашего сайта. В этом руководстве это supersecure.codes.
  • Ваш браузер подчиняется этому правилу и запрещает загрузку cdn.jsdelivr.net. CSP — это политика отказа по умолчанию.

Вы должны разрешить браузеру клиента загружать определенные ссылки, встроенные в ответы с вашего сайта. Чтобы исправить это, добавьте следующую настройку в project/settings.py:

# project/settings.py
# Allow browsers to load normalize.css from cdn.jsdelivr.net
CSP_STYLE_SRC = ["'self'", "cdn.jsdelivr.net"]

Затем попробуйте снова запросить страницу вашего сайта:

$ GET -ph https://supersecure.codes/myapp/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Security-Policy: default-src 'self'; style-src 'self' cdn.jsdelivr.net
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:37:19 GMT
Referrer-Policy: strict-origin-when-cross-origin
Server: nginx
Strict-Transport-Security: max-age=30; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

Обратите внимание, что style-src указывает 'self' cdn.jsdelivr.net как часть значения для заголовка Content-Security-Policy. Это означает, что браузер должен разрешить таблицы стилей только из двух доменов:

  1. supersecure.codes ('self')
  2. cdn.jsdelivr.net

style-src является одной из многих директив, которые могут быть частью Content-Security-Policy. Существует множество других, таких как img-src, которая определяет допустимые источники изображений и фавиконов, и script-src, которая определяет допустимые источники для JavaScript.

Каждое из них имеет соответствующую настройку для django-csp. Например, img-src и script-src задаются соответственно CSP_IMG_SRC и CSP_SCRIPT_SRC. Полный список можно посмотреть в django-csp docs

Вот последний совет по заголовку CSP: Установите его как можно раньше! Когда позже что-то сломается, будет легче определить причину, так как вы сможете легче изолировать добавленную вами функцию или ссылку, которая не загружается из-за отсутствия соответствующей директивы CSP.

Заключительные шаги для развертывания производства

Теперь вы пройдете через несколько последних шагов, которые вы можете предпринять, готовясь к развертыванию вашего приложения.

Во-первых, убедитесь, что вы установили DEBUG = False в settings.py вашего проекта, если вы еще этого не сделали. Это гарантирует, что отладочная информация на стороне сервера не будет утечка в случае ошибки 5xx на стороне сервера.

Во-вторых, отредактируйте SECURE_HSTS_SECONDS в settings.py вашего проекта, чтобы увеличить время истечения срока действия заголовка Strict-Transport-Security с 30 секунд до рекомендуемых 30 дней, что эквивалентно 2,592,000 секунд:

# Add to project/settings.py
SECURE_HSTS_SECONDS = 2_592_000  # 30 days

Далее перезапустите Gunicorn с производственным конфигурационным файлом. Добавьте следующее содержимое в config/gunicorn/prod.py:

"""Gunicorn *production* config file"""

import multiprocessing

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "project.wsgi:application"
# The number of worker processes for handling requests
workers = multiprocessing.cpu_count() * 2 + 1
# The socket to bind
bind = "0.0.0.0:8000"
# Write access and error info to /var/log
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
pidfile = "/var/run/gunicorn/prod.pid"
# Daemonize the Gunicorn process (detach & enter background)
daemon = True

Здесь вы внесли несколько изменений:

Теперь вы можете остановить текущий процесс Gunicorn и начать новый, заменив конфигурационный файл разработки на его производственный аналог:

$ # Stop existing Gunicorn dev server if it is running
$ sudo killall gunicorn

$ # Restart Gunicorn with production config file
$ gunicorn -c config/gunicorn/prod.py

После внесения этих изменений вам не нужно перезапускать Nginx, поскольку он просто передает запросы тому же address:host и никаких видимых изменений быть не должно. Тем не менее, запуск Gunicorn с настройками, ориентированными на производство, полезнее в долгосрочной перспективе, когда приложение расширяется.

Наконец, убедитесь, что вы полностью собрали файл Nginx. Вот файл полностью, включая все компоненты, которые вы добавили до сих пор, а также несколько дополнительных значений:

# File: /etc/nginx/sites-available/supersecure
# This file inherits from the http directive of /etc/nginx/nginx.conf

# Disable emitting nginx version in the "Server" response header field
server_tokens             off;

# Use site-specific access and error logs
access_log                /var/log/nginx/supersecure.access.log;
error_log                 /var/log/nginx/supersecure.error.log;

# Return 444 status code & close connection if no Host header present
server {
  listen                  80 default_server;
  return                  444;
}

# Redirect HTTP to HTTPS
server {
  server_name             .supersecure.codes;
  listen                  80;
  return                  307 https://$host$request_uri;
}

server {

  # Pass on requests to Gunicorn listening at http://localhost:8000
  location / {
    proxy_pass            http://localhost:8000;
    proxy_set_header      Host $host;
    proxy_set_header      X-Forwarded-Proto $scheme;
    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_redirect        off;
  }

  # Serve static files directly
  location /static {
    autoindex             on;
    alias                 /var/www/supersecure.codes/static/;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.supersecure.codes/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.supersecure.codes/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

В качестве напоминания, входящие правила безопасности, привязанные к вашей виртуальной машине, должны иметь определенную настройку:

Тип Протокол Диапазон портов Источник
HTTPS TCP 443 0.0.0.0/0
HTTP TCP 80 0.0.0.0/0
Заказной Все Все security-group-id
SSH TCP 22 my-laptop-ip-address/32

Подводя итог, ваш окончательный набор правил безопасности AWS состоит из четырех входящих правил и одного исходящего правила:

Final security ruleset for Django app.

Окончательный набор правил группы безопасности

Сравните приведенные выше данные с вашим первоначальным набором правил безопасности. Обратите внимание, что вы закрыли доступ через TCP:8000, где обслуживалась версия Django для разработки, и открыли доступ к Интернету через HTTP и HTTPS на портах 80 и 443, соответственно.

Ваш сайт теперь готов к показу:

Finalized configuration of Nginx and Gunicorn.

Теперь, когда вы собрали все компоненты вместе, ваше приложение доступно через Nginx по HTTPS через порт 443. HTTP-запросы на порт 80 перенаправляются на HTTPS. Сами компоненты Django и Gunicorn не выходят в открытый интернет, а находятся за обратным прокси Nginx.

Проверка безопасности HTTPS вашего сайта

Теперь ваш сайт значительно более безопасен, чем когда вы начинали это руководство, но не верьте мне на слово. Существует несколько инструментов, которые дадут вам объективную оценку связанных с безопасностью характеристик вашего сайта, уделяя особое внимание заголовкам ответов и HTTPS.

Первое - это приложение Security Headers, которое оценивает качество заголовков ответов HTTP, возвращающихся с вашего сайта. Если вы следили за этим, ваш сайт должен быть готов получить оценку A или выше.

Второй - SSL Labs, который проведет глубокий анализ конфигурации вашего веб-сервера в отношении SSL/TLS. Введите домен вашего сайта, и SSL Labs выдаст оценку, основанную на силе различных факторов, связанных с SSL/TLS. Если вы вызвали certbot с --rsa-key-size 4096 и отключили TLS 1.0 и 1.1 в пользу 1.2 и 1.3, вы должны быть хорошо настроены, чтобы получить оценку A+ от SSL Labs.

В качестве проверки, вы также можете запросить HTTPS URL вашего сайта из командной строки, чтобы увидеть полный обзор изменений, которые вы добавили на протяжении этого руководства:

$ GET https://supersecure.codes/myapp/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Security-Policy: style-src 'self' cdn.jsdelivr.net; default-src 'self'
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:37:19 GMT
Referrer-Policy: no-referrer-when-downgrade
Server: nginx
Strict-Transport-Security: max-age=2592000; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
    <link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css"
    >
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

Это действительно хороший HTML.

Заключение

Если вы следовали этому руководству, то ваш сайт достиг значительного прогресса по сравнению с тем, что было раньше, когда он был самостоятельным Django-приложением для разработки. Вы увидели, как Django, Gunicorn и Nginx могут объединиться, чтобы помочь вам безопасно обслуживать ваш сайт.

В этом руководстве вы узнали, как:

Теперь у вас есть воспроизводимый набор шагов для развертывания вашего готового к производству веб-приложения Django.

Вернуться на верх