Защита контейнеризированного приложения Django с помощью Let's Encrypt

Как установить SSL-сертификат для приложения Django?

В этом руководстве мы рассмотрим, как защитить контейнерное приложение Django, работающее за HTTPS-прокси Nginx, с помощью SSL-сертификатов Let's Encrypt.

Данный учебник построен на основе Докеризация Django с Postgres, Gunicorn и Nginx. Предполагается, что вы понимаете, как контейнеризировать приложение Django вместе с Postgres, Nginx и Gunicorn.

В настоящее время просто невозможно вывести на рынок приложение, работающее по протоколу HTTP. Без HTTPS ваш сайт становится менее безопасным и надежным. С Let's Encrypt, который упрощает процесс получения и установки SSL-сертификатов, больше нет причин не иметь HTTPS.

Django на Docker Series:

  1. Докеризация Django с Postgres, Gunicorn и Nginx
  2. Защита контейнеризированного Django-приложения с помощью Let's Encrypt (этот учебник!)
  3. Развертывание Django на AWS с помощью Docker и Let's Encrypt

Пререквизиты

Для выполнения данного урока вам потребуется:

  • доменное имя
  • работающая виртуальная машина Linux с установленными Docker и Docker Compose, на которой будет развернуто ваше приложение (AWS EC2, Google Compute Engine, DigitalOcean, Linode - все это жизнеспособные варианты)

Нужен дешевый домен для тренировок? Некоторые регистраторы доменов предлагают специальные предложения на домены '.xyz'. Кроме того, вы можете создать бесплатный домен на сайте Freenom.

Подход

Существует несколько различных способов защитить контейнерное приложение Django с помощью HTTPS. Пожалуй, наиболее популярным является добавление в файл Docker Compose нового сервиса, использующего Certbot для выпуска и обновления SSL-сертификатов. Хотя такой подход вполне оправдан, мы воспользуемся несколько иным подходом, используя следующие проекты:

  1. nginx-proxy - используется для автоматического построения конфигурации прокси-сервера Nginx для запущенных контейнеров, где каждый контейнер рассматривается как отдельный виртуальный хост
  2. acme-companion - используется для выпуска и обновления SSL-сертификатов Let's Encrypt для каждого из контейнеров, проксируемых nginx-proxy

В совокупности эти проекты упрощают управление конфигурацией Nginx и SSL-сертификатами.

Другой вариант - использовать вместо Nginx Traefik. Вкратце, Traefik работает с Let's Encrypt для выпуска и обновления сертификатов. Подробнее об этом читайте в статье Dockerizing Django with Postgres, Gunicorn, and Traefik.

Let's Encrypt

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

  1. Начните с выпуска сертификатов в среде Let's Encrypt staging
  2. Затем, когда все будет работать как надо, переключитесь на производственную среду Let's Encrypt

Почему?

Для защиты своих серверов компания Let's Encrypt применяет ограничения скорости в своей производственной системе проверки:

  1. 5 сбоев проверки на одну учетную запись, на одно имя хоста, в час
  2. 50 сертификатов может быть создано на один домен в неделю

Если вы допустите опечатку в доменном имени, в записи DNS или что-то подобное, ваш запрос будет отклонен, что будет засчитано в ваш лимит скорости, и вам придется попытаться выпустить новый сертификат.

Чтобы избежать ограничения скорости, во время разработки и тестирования для проверки системы проверки следует использовать среду Staging компании Let's Encrypt. Ограничения по скорости значительно выше в среде staging, что лучше для тестирования. Следует помнить, что выпущенные в staging сертификаты не являются публично доверенными, поэтому, как только все заработает, следует переключиться на их производственную среду.

Настройка проекта

Сначала клонируйте содержимое репозитория проекта на GitHub:

$ git clone https://github.com/testdrivenio/django-on-docker django-on-docker-letsencrypt
$ cd django-on-docker-letsencrypt

Этот репозиторий содержит все необходимое для развертывания Dockerized Django-приложения, за исключением SSL-сертификатов, которые мы будем добавлять в этом руководстве.

Django конфигурация

Во-первых, для запуска приложения Django за HTTPS-прокси необходимо добавить параметр SECURE_PROXY_SSL_HEADER в settings.py:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

В этом кортеже, если X-Forwarded-Proto имеет значение https, запрос является безопасным.

Также необходимо обновить CSRF_TRUSTED_ORIGINS внутри settings.py:

CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS").split(" ")

Docker Compose

Пришло время конфигурировать Docker Compose.

Добавим новый файл Docker Compose для тестирования под названием docker-compose.staging.yml:

version: '3.8'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - ./.env.staging
    depends_on:
      - db
  db:
    image: postgres:13.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.staging.db
  nginx-proxy:
    container_name: nginx-proxy
    build: nginx
    restart: always
    ports:
      - 443:443
      - 80:80
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    depends_on:
      - web
  acme-companion:
    image: nginxproxy/acme-companion
    env_file:
      - ./.env.staging.proxy-companion
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - acme:/etc/acme.sh
    depends_on:
      - nginx-proxy

volumes:
  postgres_data:
  static_volume:
  media_volume:
  certs:
  html:
  vhost:
  acme:

Добавьте файл .env.staging.db для контейнера db:

POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod

Измените значения параметров POSTGRES_USER и POSTGRES_PASSWORD на соответствующие вашему пользователю и паролю.

Мы уже рассмотрели службы web и db в предыдущем учебнике, поэтому перейдем к рассмотрению служб nginx-proxy и acme-companion.

Базы данных - это критически важные сервисы. Добавление дополнительных слоев, таких как Docker, увеличивает ненужный риск в производстве. Для упрощения таких задач, как обновление мелких версий, регулярное резервное копирование и масштабирование, рекомендуется использовать управляемый сервис, например AWS RDS, Google Cloud SQL или DigitalOcean's Managed Database.

Nginx Proxy Service

Для данного сервиса используется проект nginx-proxy для генерации конфигурации обратного прокси для контейнера web с использованием виртуальных хостов для маршрутизации.

Обязательно ознакомьтесь с README на репозитории nginx-proxy.

После запуска контейнер, связанный с nginx-proxy, автоматически обнаруживает контейнеры (в той же сети), у которых установлена переменная окружения VIRTUAL_HOST, и динамически обновляет конфигурацию своих виртуальных хостов.

Далее добавим файл .env.staging для контейнера web:

DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=<YOUR_DOMAIN.COM>
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
VIRTUAL_HOST=<YOUR_DOMAIN.COM>
VIRTUAL_PORT=8000
LETSENCRYPT_HOST=<YOUR_DOMAIN.COM>
CSRF_TRUSTED_ORIGINS=https://<YOUR_DOMAIN.COM>

Примечания:

  1. Измените <YOUR_DOMAIN.COM> на ваш реальный домен и измените значения по умолчанию SQL_USER и SQL_PASSWORD на соответствующие POSTGRES_USER и POSTGRES_PASSWORD (из .env.staging.db).
  2. Как уже упоминалось, VIRTUAL_HOSTVIRTUAL_PORT) необходимы nginx-proxy для автоматического создания конфигурации обратного прокси.
  3. LETSENCRYPT_HOST нужен для того, чтобы nginx-proxy-companion мог выпустить сертификат Let's Encrypt для вашего домена.
  4. Поскольку приложение Django будет слушать порт 8000, мы также установили переменную окружения VIRTUAL_PORT.
  5. Том /var/run/docker.sock:/tmp/docker.sock:ro в docker-compose.staging.yml используется для прослушивания вновь зарегистрированных/де-регистрированных контейнеров.

Для целей тестирования/отладки вы можете использовать * для DJANGO_ALLOWED_HOSTS при первом развертывании, чтобы упростить работу. Только не забудьте ограничить количество разрешенных хостов после завершения тестирования.

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

Далее обновим конфигурацию Nginx в папке "nginx".

Сначала добавьте каталог с именем "vhost.d". Затем добавьте в этот каталог файл default для обслуживания статических и мультимедийных файлов:

location /static/ {
  alias /home/app/web/staticfiles/;
  add_header Access-Control-Allow-Origin *;
}

location /media/ {
  alias /home/app/web/mediafiles/;
  add_header Access-Control-Allow-Origin *;
}

Запросы, соответствующие любому из этих шаблонов, будут обслуживаться из папок static или media. Они не будут проксироваться в другие контейнеры. Контейнеры web и nginx-proxy совместно используют тома, на которых расположены статические и мультимедийные файлы:

static_volume:/home/app/web/staticfiles
media_volume:/home/app/web/mediafiles

Добавьте в папку "nginx" файл custom.conf для хранения пользовательской конфигурации прокси-сервера:

client_max_body_size 10M;

Обновить nginx/Dockerfile:

FROM nginxproxy/nginx-proxy
COPY vhost.d/default /etc/nginx/vhost.d/default
COPY custom.conf /etc/nginx/conf.d/custom.conf

Удалить nginx.conf.

Теперь ваш каталог "nginx" должен выглядеть следующим образом:

└── nginx
    ├── Dockerfile
    ├── custom.conf
    └── vhost.d
        └── default

Служба сопровождения ACME

В то время как сервис nginx-proxy занимается маршрутизацией, acme-companion (через acme-companion) занимается созданием, обновлением и использованием сертификатов Let's Encrypt для проксируемых Docker-контейнеров.

Для выпуска и обновления сертификатов для проксируемых контейнеров необходимо добавить в каждый из них переменную окружения LETSENCRYPT_HOST (что мы уже сделали). Она также должна иметь то же значение, что и VIRTUAL_HOST.

Этот контейнер должен совместно использовать следующие тома с nginx-proxy:

  1. certs:/etc/nginx/certs хранит сертификаты, закрытые ключи и ключи учетных записей ACME
  2. html:/usr/share/nginx/html пишет http-01 файлы вызова
  3. vhost:/etc/nginx/vhost.d изменяет конфигурацию vhosts

Для получения дополнительной информации ознакомьтесь с официальной документацией.

Добавьте файл .env.staging.proxy-companion:

DEFAULT_EMAIL=youremail@yourdomain.com
ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory
NGINX_PROXY_CONTAINER=nginx-proxy

Примечания:

  1. DEFAULT_EMAIL - это адрес электронной почты, который Let's Encrypt будет использовать для отправки уведомлений о ваших сертификатах (включая продление).
  2. ACME_CA_URI - это URL, используемый для выпуска сертификатов. Опять же, используйте staging, пока не убедитесь, что все работает.
  3. NGINX_PROXY_CONTAINER - имя nginx-proxy контейнера.

Запуск контейнеров

Все готово к развертыванию.

Пришло время переходить на свой экземпляр Linux.

Предполагая, что на вашем экземпляре создан каталог проекта, например /home/myuser/django-on-docker, скопируйте в него файлы и папки с помощью SCP:

$ scp -r $(pwd)/{app,nginx,.env.staging,.env.staging.db,.env.staging.proxy-companion,docker-compose.staging.yml} user@your-ip-or-domain:/path/to/django-on-docker

Подключитесь к своему экземпляру по SSH и перейдите в каталог проекта:

$ ssh user@your-ip-or-domain
$ cd /path/to/django-on-docker

После этого можно собирать образы и запускать контейнеры:

$ docker-compose -f docker-compose.staging.yml up -d --build

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

HTTPS certificate not secure

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

Как узнать, все ли работает?

Нажмите на кнопку "Дополнительно" и затем на кнопку "Продолжить". Теперь вы должны увидеть свое приложение. Загрузите изображение, а затем убедитесь, что его можно просмотреть по адресу https://yourdomain.com/mediafiles/IMAGE_FILE_NAME.

Выдача сертификата производства

Теперь, когда все работает как надо, мы можем переключиться на производственную среду Let's Encrypt.

Свернуть существующие контейнеры и выйти из экземпляра:

$ docker-compose -f docker-compose.staging.yml down -v
$ exit

На локальной машине обновите docker-compose.prod.yml:

version: '3.8'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:13.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db
  nginx-proxy:
    container_name: nginx-proxy
    build: nginx
    restart: always
    ports:
      - 443:443
      - 80:80
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    depends_on:
      - web
  acme-companion:
    image: nginxproxy/acme-companion
    env_file:
      - ./.env.prod.proxy-companion
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - acme:/etc/acme.sh
    depends_on:
      - nginx-proxy

volumes:
  postgres_data:
  static_volume:
  media_volume:
  certs:
  html:
  vhost:
  acme:

Единственное отличие от docker-compose.staging.yml в том, что мы использовали разные файлы окружения.

.env.prod:

DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=<YOUR_DOMAIN.COM>
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
VIRTUAL_HOST=<YOUR_DOMAIN.COM>
VIRTUAL_PORT=8000
LETSENCRYPT_HOST=<YOUR_DOMAIN.COM>
CSRF_TRUSTED_ORIGINS=https://<YOUR_DOMAIN.COM>

.env.prod.db:

POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod

.env.prod.proxy-companion:

DEFAULT_EMAIL=youremail@yourdomain.co
NGINX_PROXY_CONTAINER=nginx-proxy

Обновите их соответствующим образом.

Заметили ли вы разницу, начиная со стадийных версий? Переменная окружения ACME_CA_URI не установлена, так как образ acme-companion по умолчанию использует производственное окружение Let's Encrypt.

Скопируйте новые файлы и папки на свой экземпляр с помощью SCP:

$ scp $(pwd)/{.env.prod,.env.prod.db,.env.prod.proxy-companion,docker-compose.prod.yml} user@your-ip-or-domain:/path/to/django-on-docker

Как и прежде, подключитесь к своему экземпляру по SSH и перейдите в каталог проекта:

$ ssh user@your-ip-or-domain
$ cd /path/to/django-on-docker

Сборка образов и разгон контейнеров:

$ docker-compose -f docker-compose.prod.yml up -d --build

Снова перейдите к своему домену. Предупреждение больше не должно появляться.

Поздравляем! Теперь вы используете производственный сертификат Let's Encrypt.

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

$ docker-compose -f docker-compose.prod.yml logs acme-companion

Заключение

В заключение, после того как Docker Compose настроен на запуск Django, для настройки HTTPS необходимо добавить (и настроить) сервисы nginx-proxy и acme-companion в файл Docker Compose. Теперь можно добавить дополнительные контейнеры, настроив переменные окружения VIRTUAL_HOST (маршрутизация) и LETSENCRYPT_HOST (сертификат). Как всегда, не забудьте сначала провести тестирование в тестовой среде Let's Encrypt.

Код можно найти в репо django-on-docker-letsencrypt.

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