Django.fun

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

Оглавление

  • Начиная с Django и WSGIServer
    • Настройка облачной виртуальной машины (ВМ)
    • Создание приложения Django для вырезания файлов cookie
    • Использование Django WSGIServer в разработке
  • Размещение вашего сайта в Интернете с помощью Django, Gunicorn и Nginx
    • Установка статического общедоступного IP-адреса
    • Ссылка на домен
    • Замена WSGIServer на Gunicorn
    • Включение Nginx
    • Обслуживание статических файлов напрямую с Nginx
  • Подготовка вашего сайта к работе с помощью HTTPS
    • Включение HTTPS
    • Перенаправление HTTP на HTTPS
    • Еще один шаг вперед с HSTS
    • Установка заголовка политики реферера
    • Добавление заголовка Content-Security- Policy (CSP)
    • Заключительные шаги для развертывания в производственной среде
    • Проверка безопасности HTTPS вашего сайта
  • Заключение
  • Дальнейшее чтение

In this tutorial, you’ll learn:

  • How you can take your Django app from development to production
  • How you can host your app on a real- world public domain
  • How to introduce Gunicorn and Nginx into the request and response chain
  • How HTTP headers can fortify your site’s HTTPS security

Starting Django and WSGIServer

You will use Django as the framework at the core of your web application, applying it to URL routing, HTML rendering, authentication, administration, and backend logic. In this tutorial, you'll supplement the Django component with two other layers, Gunicorn and Nginx, to provide scalability for your application. But before you do this, you need to set up the environment and start the Django application itself.

Installing a cloud-based virtual machine (VM)

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

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

<

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

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

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

.

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

Reference Type Protocol Port Range Source
1 Custom TCP 8000 my-laptop-ip-address/32
2 Custom All All security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

Сейчас вы пройдете по ним по очереди:

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

    You’ll also want to add an outbound rule to allow outbound traffic to do things such as install packages:

    Type Protocol Port Range Source
    Custom All All 0.0.0.0/0

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

    Initial security ruleset for Django app
    Initial security group rule set

    From your local computer, you can then SSH into the instance:

    $ 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.

    .

    Next, create and activate a virtual environment:

    $ 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/
    

    This creates the Django app myapp alongside the project named 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
    

    Using a terminal editor such as Vim or GNU nano, open project/settings.py and append your app to 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",
    ]
    

    Next, open myapp/templates/myapp/home.html and create a short and sweet HTML page:

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

    After that, edit myapp/views.py to render that HTML page:

    from django.shortcuts import render
    
    def index(request):
        return render(request, "myapp/home.html")
    

    Now create and open myapp/urls.py to associate your view with a URL pattern:

    from django.urls import path
    from . import views
    
    urlpatterns = [
        path("", views.index, name="index"),
    ]
    

    After that, edit project/urls.py accordingly:

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

    Вы можете сделать еще одну вещь, пока вы здесь, а именно убедиться, что Django секретный ключ, используемый для криптографической подписи, не закодирован в 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 в вашем окружении, а не включать его в исходный код вашего приложения.

    Note: For larger projects, check out django-environ to configure your Django application with environment variables.

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

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

    You can cat the contents of .DJANGO_SECRET_KEY to see that openssl has generated a cryptographically secure hex string key:

    $ 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
    

    You can create an alias that will let you send a GET request using httpie to your application:

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

    This aliases GET to an http call with some default flags. You can now use GET docs.python.org to see the response headers and body from the Python documentation’s homepage.

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

    Note: If it appears that nohup hangs and leaves you without a cursor, press Enter to get your terminal cursor and shell prompt back.

    Django’s runserver command, in turn, uses the following syntax:

    $ python manage.py runserver [address:port]
    

    If you leave the address:port argument unspecified as done above, Django will default to listening on localhost:8000. You can also use the lsof command to verify more directly that a python command was invoked to listen on port 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>
    

    The header Server: WSGIServer/0.2 CPython/3.8.10 describes the software that generated the response. In this case, it’s version 0.2 of WSGIServer alongside CPython 3.8.10.

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

    В нашем примере проект 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.

    Note: Remember that this means you’ll need to change the target IP of ssh in order to SSH into your VM:

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

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

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

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

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

    <

    Warning: If you want to serve your site in development on a public domain with DEBUG set to True, you need to have created custom inbound security rules to only allow your personal computer’s and VM’s IP addresses. You should not open up any HTTP or HTTPS inbound rules to 0.0.0.0 until you’ve turned off DEBUG at the very least.

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

    1. Create an account on Namecheap, making sure to set up two-factor authentication (2FA).
    2. From the homepage, start searching for a domain name that suits your budget. You’ll find that prices can vary drastically with both the top-level domain (TLD) and hostname.
    3. Purchase the domain when you’re happy with a choice.

    This tutorial uses the domain supersecure.codes, but you’ll have your own.

    Note: As you go through this tutorial, remember that supersecure.codes is just an example domain and isn’t actively maintained.

    < <
    1. Select Account → Domain List.
    2. Select Manage next to your domain.
    3. Enable WithheldForPrivacy protection.
    <
    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.

    Add the A Records as follows, replacing 50.19.125.152 with your instance’s public IPv4 address:

    Type Host Value TTL
    A Record @ 50.19.125.152 Automatic
    A Record www 50.19.125.152 Automatic
    <

    You can see that there are two variations for the Host field:

    1. Using @ points to the root domain, supersecure.codes in this case.
    2. Using www means that www.supersecure.codes will point to the same place as just supersecure.codes. The www is technically a subdomain that can send users to the same place as the shorter supersecure.codes.

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

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

    You can confirm that the process is gone with pgrep or by checking active jobs again:

    $ 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"]
    

    The leading dot (.) is a subdomain wildcard, allowing both www.supersecure.codes and supersecure.codes. Keep this list tight to prevent HTTP host header attacks.

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

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

    Notice the address:port argument is now 0.0.0.0:8000, while none was previously specified:

    • Specifying no address:port implies serving the app on localhost:8000. This means that the application was only accessible from within the VM itself. You could talk to it by calling httpie from the same IP address, but you couldn’t reach your application from the outside world.

    • Specifying an address:port of '0.0.0.0:8000' makes your server viewable to the outside world, though still on port 8000 by default. The 0.0.0.0 is shorthand for “bind to all IP addresses this computer supports.” In the case of an out-of-the-box cloud VM with one network interface controller (NIC) named eth0, using 0.0.0.0 acts as a stand-in for the public IPv4 address of the machine.

    Next, turn on output from nohup.out to view any incoming logs from Django’s WSGIServer:

    $ 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). (Источник)

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

    • It’s fast, optimized, and designed for production.
    • It gives you more fine-grained control over the application server itself.
    • It has more complete and configurable logging.
    • It’s well-tested, specifically for its functionality as an application server.

    You can install Gunicorn through pip into your virtual environment:

    $ 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/'
    

    Next, open a development configuration file, config/gunicorn/dev.py, and add the following:

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

    Before starting Gunicorn, you should halt the runserver process. Use jobs to find it and kill to stop it:

    $ 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/
    

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

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

    These lines are access logs that tell you about incoming requests:

    Component Meaning
    67.xx.xx.xx User IP address
    27/Sep/2021:01:30:46 +0000 Timestamp of request
    GET Request method
    /myapp/ URL path
    HTTP/1.1 Protocol
    200 Response status code
    182 Response content length

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

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

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

    Включение Nginx

    At this point, you’ve swapped out Django’s runserver command in favor of gunicorn as the application server. There’s one more player to add to the request chain: a web server like Nginx.

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

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

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

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

    • Django is a web framework. It lets you build the core web application that powers the actual content on the site. It handles HTML rendering, authentication, administration, and backend logic.

    • Gunicorn is an application server. It translates HTTP requests into something Python can understand. Gunicorn implements the Web Server Gateway Interface (WSGI), which is a standard interface between web server software and web applications.

    • Nginx is a web server. It’s the public handler, more formally called the reverse proxy, for incoming requests and scales to thousands of simultaneous connections.

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

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

    Finalized configuration of Nginx and Gunicorn
    Image: Real Python

    Чтобы начать использовать 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 следующим:

    Type Protocol Port Range Source
    HTTP TCP 80 my-laptop-ip-address/32

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

    Now, start the nginx service and confirm that its status is 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/
    

    Remember to replace supersecure.codes with your own domain name.

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

    Nginx 404 page

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

    • Nginx is listening on port 80.
    • Gunicorn is listening, separately, on port 8000.

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

    Nginx disconnected from Gunicorn
    Image: Real Python

    Вам нужно задать 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;
      }
    }
    

    Remember that you need to replace supersecure in the file name with your site’s hostname, and make sure to replace the server_name value of .supersecure.codes with your own domain, prefixed by a dot.

    Note: You will likely need sudo to open files under /etc.

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

    • Listen on port 80 for requests that use a host for supersecure.codes and its subdomains.
    • Pass those requests on to http://localhost:8000, which is where Gunicorn is listening.

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

    You can validate your configuration file using nginx configtest:

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

    The [ OK ] output indicates that the configuration file is valid and can be parsed.

    Now you need to symlink this file to the sites-enabled directory, replacing supersecure with your site domain:

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

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

    Type Protocol Port Range Source
    HTTP TCP 80 vm-static-ip-address/32

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

    Теперь, когда он использует 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 now returns the Server header as Server: nginx, indicating that Nginx is the new front-end web server. Setting server_tokens to a value of off tells Nginx not to emit its exact version, such as nginx/x.y.z (Ubuntu). From a security perspective, that would be disclosing unnecessary information.
    • Nginx uses chunked for the Transfer-Encoding header instead of advertising Content-Length.
    • Nginx also asks to keep open the network connection with 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
    

    Now open a new file static/js/greenlight.js and add the following 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 заставит блок текста расплываться большим зеленым шрифтом при наведении на него курсора. Да, это передовой фронтенд!

    Next, add the following configuration to project/settings.py, updating STATIC_ROOT with your domain name:

    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>
    

    By including the /static/js/greenlight.js script, the <span id="changeme"> element will have an event listener attached to it.

    Примечание: Чтобы этот пример был простым, вы жестко кодируете путь 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
    

    Now run collectstatic as your non-root user from within your project’s directory:

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

    Finally, add a location variable for /static in /etc/nginx/sites-available/supersecure, your site configuration file for 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, поэтому вам нужно будет адаптировать эти шаги для работы с вашим собственным проектом.

    You should now turn off DEBUG mode in your project in project/settings.py:

    # project/settings.py
    DEBUG = False
    

    Gunicorn will pick up this change since you specified reload = True in 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;
    

    You can use nginx -t to confirm that your Nginx supports version 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
    

    Now you’re ready to install and use Certbot. On Ubuntu Focal (20.04), you can use snap to install Certbot:

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

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

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

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

    Reference Type Protocol Port Range Source
    1 HTTP TCP 80 0.0.0.0/0
    2 Custom All All security-group-id
    3 SSH TCP 22 my-laptop-ip-address/32

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

    You can then issue one more command, certbot, to install the certificate:

    $ 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
    

    If you cat out the configuration file at your equivalent of /etc/nginx/sites-available/supersecure, you’ll see that certbot has automatically added a group of lines related to 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. Измените свои правила безопасности для входящего трафика следующим образом:

    Reference Type Protocol Port Range Source
    1 HTTPS TCP 443 0.0.0.0/0
    2 HTTP TCP 80 0.0.0.0/0
    2 Custom All All security-group-id
    3 SSH TCP 22 my-laptop-ip-address/32

    Каждое из этих правил имеет конкретную цель:

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

    Now, re-navigate to your site in a browser, but with one key difference. Rather than http, specify https as the protocol:

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

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

    .
    Connecting to your Django app over HTTPS

    Если вы используете Firefox и нажимаете на значок замка, вы можете просмотреть более подробную информацию о сертификате, участвующем в защите соединения:

    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 ]
    

    Then, tell nginx to reload the configuration:

    $ sudo systemctl reload nginx
    

    Then send a GET request with the --all flag to your app’s HTTP URL to display any redirect chains:

    $ 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. The initial request receives a 307 status code response redirecting to the HTTPS version.
    2. The second request is made to the same URI but with an HTTPS scheme rather than HTTP. This time, it receives the page content that it was looking for with a 200 OK response.

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

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

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

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

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

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

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

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

    .

    To remedy this, you can tell Django to set the Strict-Transport-Security header. Add these lines to your project’s 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, прочитайте в Django объяснение строгой транспортной безопасности HTTP. Прежде чем устанавливать большое значение временного окна 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. (Источник)

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

    1. В браузере откройте инструменты разработчика. Перейдите на вкладку, показывающую сетевую активность. В Firefox это Правый клик → Inspect Element → Network.

    2. Обновите страницу. Сначала вы должны увидеть ответ 307 Temporary Redirect как часть цепочки ответов. Это первый раз, когда ваш браузер видит заголовок Strict-Transport-Security.

    3. Измените URL в браузере на версию HTTP и запросите страницу снова. Если вы работаете в Chrome, вы должны увидеть 307 Internal Redirect. В Firefox вы должны увидеть ответ 200 OK, потому что ваш браузер автоматически перешел на запрос HTTPS, даже когда вы пытались указать ему использовать 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 в течение этого периода времени.

    Setting the Referrer-Policy Header

    Django 3.x also added the ability to control the Referrer-Policy header. You can specify SECURE_REFERRER_POLICY in project/settings.py:

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

    How does this setting work? When you follow a link from page A to page B, your request to page B contains the URL of page A under the header Referer. A server that sets the Referrer-Policy header, which you can set in Django through SECURE_REFERRER_POLICY, controls when and how much information is forwarded on to the target site. SECURE_REFERRER_POLICY can take a number of recognized values, which you can read about in detail in the Mozilla docs.

    As an example, if you use "strict-origin-when-cross-origin" and a user’s current page is https://example.com/page, the Referer header is constrained in the following ways:

    Target Site Referer Header
    https://example.com/otherpage https://example.com/page
    https://mozilla.org https://example.com/
    http://example.org (HTTP target) [None]

    Here’s what happens case by case, assuming the current user’s page is https://example.com/page:

    • If the user follows a link to https://example.com/otherpage, Referer will include the full path of the current page.
    • If the user follows a link to the separate domain of https://mozilla. org, Referer will exclude the path of the current page.
    • If the user follows a link to http://example.org with an http:// protocol, Referer will be blank.

    If you add this line to project/settings.py and re-request your app’s homepage, then you’ll see a new entrant:

    $ 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) и атакам с использованием инъекций данных.

    Adding a Content-Security-Policy (CSP) Header

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

    .
    $ python -m pip install django-csp
    

    To turn on the header with its default value, add this single line to project/settings.py under the existing MIDDLEWARE definition:

    # 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. Вот почему он не загружается:

    • Your project/settings.py includes CSPMiddleware in Django’s MIDDLEWARE. Including CSPMiddleware sets the header to the default Content-Security-Policy value, which is default-src 'self', where 'self' means your site’s own domain. In this tutorial, that’s supersecure.codes.
    • Your browser obeys this rule and forbids cdn.jsdelivr.net from loading. CSP is a default deny policy.

    Вы должны принять решение и явно разрешить браузеру клиента загружать определенные ссылки, встроенные в ответы с вашего сайта. Чтобы исправить это, добавьте следующую настройку в 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

    The style-src directive is one of many directives that can be part of Content-Security-Policy. There are many others, such as img-src, which specifies valid sources of images and favicons, and script-src, which defines valid sources for JavaScript.

    Each of these has a corresponding setting for django-csp. For example, img-src and script-src are set by CSP_IMG_SRC and CSP_SCRIPT_SRC, respectively. You can check out the django-csp docs for the complete list.

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

    Последние шаги для развертывания производства

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

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

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

    Следующим шагом перезапустите Gunicorn с конфигурационным файлом production. Добавьте следующее содержимое в 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
    

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

    • You turned off the reload feature used in development.
    • You made the number of workers a function of the VM’s CPU count instead of hard-coding it.
    • You allowed loglevel to default to "info" rather than the more verbose "debug".

    Сейчас вы можете остановить текущий процесс 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;
    }
    

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

    Type Protocol Port Range Source
    HTTPS TCP 443 0.0.0.0/0
    HTTP TCP 80 0.0.0.0/0
    Custom All All security-group-id
    SSH TCP 22 my-laptop-ip-address/32

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

    Final security ruleset for Django app
    Final security group rule set

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

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

    Finalized configuration of Nginx and Gunicorn
    Image: Real Python

    Сейчас, когда вы собрали все компоненты вместе, ваше приложение доступно через 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 могут объединиться, чтобы помочь вам безопасно обслуживать ваш сайт.

    In this tutorial, you’ve learned how to:

    • Take your Django app from development to production
    • Host your app on a real-world public domain
    • Introduce Gunicorn and Nginx into the request and response chain
    • Work with HTTP headers to increase your site’s HTTPS security

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

    Поделитесь с другими:

  • Рекомендуемые записи по теме

    Докеризация Django с помощью Postgres, Gunicorn и Nginx