# Об HTTPS { #about-https }

Легко предположить, что HTTPS — это что-то, что просто «включено» или нет.

Но на самом деле всё гораздо сложнее.

/// tip | Совет

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

///

Чтобы **изучить основы HTTPS** с точки зрения пользователя, загляните на [https://howhttps.works/](https://howhttps.works/).

Теперь, со стороны **разработчика**, вот несколько вещей, которые стоит держать в голове, размышляя об HTTPS:

* Для HTTPS **серверу** нужно **иметь «сертификаты»**, сгенерированные  **третьей стороной**.
    * Эти сертификаты на самом деле **приобретаются** у третьей стороны, а не «генерируются».
* У сертификатов есть **срок действия**.
    * Они **истекают**.
    * После этого их нужно **обновлять**, то есть **получать заново** у третьей стороны.
* Шифрование соединения происходит на **уровне TCP**.
    * Это на один уровень **ниже HTTP**.
    * Поэтому **сертификаты и шифрование** обрабатываются **до HTTP**.
* **TCP не знает о «доменах»**. Только об IP-адресах.
    * Информация о **конкретном домене** передаётся в **данных HTTP**.
* **HTTPS-сертификаты** «сертифицируют» **определённый домен**, но протокол и шифрование происходят на уровне TCP, **до того как** становится известен домен, с которым идёт работа.
* **По умолчанию** это означает, что вы можете иметь **лишь один HTTPS-сертификат на один IP-адрес**.
    * Неважно, насколько мощный у вас сервер или насколько маленькие приложения на нём работают.
    * Однако у этого есть **решение**.
* Есть **расширение** протокола **TLS** (того самого, что занимается шифрованием на уровне TCP, до HTTP) под названием **[<abbr title="Server Name Indication - Указание имени сервера">SNI</abbr>](https://en.wikipedia.org/wiki/Server_Name_Indication)**.
    * Это расширение SNI позволяет одному серверу (с **одним IP-адресом**) иметь **несколько HTTPS-сертификатов** и обслуживать **несколько HTTPS-доменов/приложений**.
    * Чтобы это работало, **один** компонент (программа), запущенный на сервере и слушающий **публичный IP-адрес**, должен иметь **все HTTPS-сертификаты** на этом сервере.
* **После** установления защищённого соединения, протокол обмена данными — **всё ещё HTTP**.
    * Содержимое **зашифровано**, несмотря на то, что оно отправляется по **протоколу HTTP**.

Обычно на сервере (машине, хосте и т.п.) запускают **одну программу/HTTP‑сервер**, которая **управляет всей частью, связанной с HTTPS**: принимает **зашифрованные HTTPS-запросы**, отправляет **расшифрованные HTTP-запросы** в само HTTP‑приложение, работающее на том же сервере (в нашем случае это приложение **FastAPI**), получает **HTTP-ответ** от приложения, **шифрует его** с использованием подходящего **HTTPS‑сертификата** и отправляет клиенту по **HTTPS**. Такой сервер часто называют **[прокси‑сервером TLS-терминации](https://en.wikipedia.org/wiki/TLS_termination_proxy)**.

Некоторые варианты, которые вы можете использовать как прокси‑сервер TLS-терминации:

* Traefik (умеет обновлять сертификаты)
* Caddy (умеет обновлять сертификаты)
* Nginx
* HAProxy

## Let's Encrypt { #lets-encrypt }

До появления Let's Encrypt эти **HTTPS-сертификаты** продавались доверенными третьими сторонами.

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

Затем появился **[Let's Encrypt](https://letsencrypt.org/)**.

Это проект Linux Foundation. Он предоставляет **HTTPS‑сертификаты бесплатно**, в автоматическом режиме. Эти сертификаты используют стандартные криптографические механизмы и имеют короткий срок действия (около 3 месяцев), поэтому **безопасность фактически выше** благодаря уменьшенному сроку жизни.

Домены безопасно проверяются, а сертификаты выдаются автоматически. Это также позволяет автоматизировать процесс их продления.

Идея — автоматизировать получение и продление сертификатов, чтобы у вас был **безопасный HTTPS, бесплатно и навсегда**.

## HTTPS для разработчиков { #https-for-developers }

Ниже приведён пример того, как может выглядеть HTTPS‑API, шаг за шагом, с акцентом на идеях, важных для разработчиков.

### Имя домена { #domain-name }

Чаще всего всё начинается с **приобретения** **имени домена**. Затем вы настраиваете его на DNS‑сервере (возможно, у того же облачного провайдера).

Скорее всего, вы получите облачный сервер (виртуальную машину) или что-то подобное, и у него будет <dfn title="Со временем не меняется. Не динамический.">постоянный</dfn> **публичный IP-адрес**.

На DNS‑сервере(ах) вы настроите запись («`A record`» - запись типа A), указывающую, что **ваш домен** должен указывать на публичный **IP‑адрес вашего сервера**.

Обычно это делается один раз — при первоначальной настройке всего.

/// tip | Совет

Часть про доменное имя относится к этапам задолго до HTTPS, но так как всё зависит от домена и IP‑адреса, здесь стоит это упомянуть.

///

### DNS { #dns }

Теперь сфокусируемся на собственно частях, связанных с HTTPS.

Сначала браузер спросит у **DNS‑серверов**, какой **IP соответствует домену**, в нашем примере `someapp.example.com`.

DNS‑серверы ответят браузеру, какой **конкретный IP‑адрес** использовать. Это будет публичный IP‑адрес вашего сервера, который вы указали в настройках DNS.

<img src="/img/deployment/https/https01.drawio.svg">

### Начало TLS-рукопожатия { #tls-handshake-start }

Далее браузер будет общаться с этим IP‑адресом на **порту 443** (порт HTTPS).

Первая часть взаимодействия — установить соединение между клиентом и сервером и договориться о криптографических ключах и т.п.

<img src="/img/deployment/https/https02.drawio.svg">

Это взаимодействие клиента и сервера для установления TLS‑соединения называется **TLS‑рукопожатием**.

### TLS с расширением SNI { #tls-with-sni-extension }

На сервере **только один процесс** может слушать конкретный **порт** на конкретном **IP‑адресе**. Могут быть другие процессы, слушающие другие порты на том же IP‑адресе, но не более одного процесса на каждую комбинацию IP‑адреса и порта.

По умолчанию TLS (HTTPS) использует порт `443`. Значит, он нам и нужен.

Так как только один процесс может слушать этот порт, делать это будет **прокси‑сервер TLS-терминации**.

У прокси‑сервера TLS-терминации будет доступ к одному или нескольким **TLS‑сертификатам** (HTTPS‑сертификатам).

Используя **расширение SNI**, упомянутое выше, прокси‑сервер TLS-терминации определит, какой из доступных TLS (HTTPS)‑сертификатов нужно использовать для этого соединения, выбрав тот, который соответствует домену, ожидаемому клиентом.

В нашем случае это будет сертификат для `someapp.example.com`.

<img src="/img/deployment/https/https03.drawio.svg">

Клиент уже **доверяет** организации, выдавшей этот TLS‑сертификат (в нашем случае — Let's Encrypt, но об этом позже), поэтому может **проверить**, что сертификат действителен.

Затем, используя сертификат, клиент и прокси‑сервер TLS-терминации **договариваются о способе шифрования** остальной **TCP‑коммуникации**. На этом **TLS‑рукопожатие** завершено.

После этого у клиента и сервера есть **зашифрованное TCP‑соединение** — это и предоставляет TLS. И они могут использовать это соединение, чтобы начать собственно **HTTP‑обмен**.

Собственно, **HTTPS** — это обычный **HTTP** внутри **защищённого TLS‑соединения**, вместо чистого (незашифрованного) TCP‑соединения.

/// tip | Совет

Обратите внимание, что шифрование обмена происходит на **уровне TCP**, а не на уровне HTTP.

///

### HTTPS‑запрос { #https-request }

Теперь, когда у клиента и сервера (конкретно у браузера и прокси‑сервера TLS-терминации) есть **зашифрованное TCP‑соединение**, они могут начать **HTTP‑обмен**.

Клиент отправляет **HTTPS‑запрос**. Это обычный HTTP‑запрос через зашифрованное TLS‑соединение.

<img src="/img/deployment/https/https04.drawio.svg">

### Расшифровка запроса { #decrypt-the-request }

Прокси‑сервер TLS-терминации использует согласованное шифрование, чтобы **расшифровать запрос**, и передаёт **обычный (расшифрованный) HTTP‑запрос** процессу, запускающему приложение (например, процессу с Uvicorn, который запускает приложение FastAPI).

<img src="/img/deployment/https/https05.drawio.svg">

### HTTP‑ответ { #http-response }

Приложение обработает запрос и отправит **обычный (незашифрованный) HTTP‑ответ** прокси‑серверу TLS-терминации.

<img src="/img/deployment/https/https06.drawio.svg">

### HTTPS‑ответ { #https-response }

Затем прокси‑сервер TLS-терминации **зашифрует ответ** с использованием ранее согласованного способа шифрования (который начали использовать для сертификата для `someapp.example.com`) и отправит его обратно в браузер.

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

<img src="/img/deployment/https/https07.drawio.svg">

Клиент (браузер) узнает, что ответ пришёл от правильного сервера, потому что используется способ шифрования, о котором они договорились ранее с помощью **HTTPS‑сертификата**.

### Несколько приложений { #multiple-applications }

На одном и том же сервере (или серверах) могут работать **несколько приложений**, например другие программы с API или база данных.

Только один процесс может обрабатывать конкретную комбинацию IP и порта (в нашем примере — прокси‑сервер TLS-терминации), но остальные приложения/процессы тоже могут работать на сервере(ах), пока они не пытаются использовать ту же **комбинацию публичного IP и порта**.

<img src="/img/deployment/https/https08.drawio.svg">

Таким образом, прокси‑сервер TLS-терминации может обрабатывать HTTPS и сертификаты для **нескольких доменов** (для нескольких приложений), а затем передавать запросы нужному приложению в каждом случае.

### Продление сертификата { #certificate-renewal }

Со временем каждый сертификат **истечёт** (примерно через 3 месяца после получения).

Затем будет другая программа (иногда это отдельная программа, иногда — тот же прокси‑сервер TLS-терминации), которая свяжется с Let's Encrypt и продлит сертификат(ы).

<img src="/img/deployment/https/https.drawio.svg">

**TLS‑сертификаты** **связаны с именем домена**, а не с IP‑адресом.

Поэтому, чтобы продлить сертификаты, программа продления должна **доказать** удостоверяющему центру (Let's Encrypt), что она действительно **«владеет» и контролирует этот домен**.

Для этого, учитывая разные потребности приложений, есть несколько способов. Популярные из них:

* **Изменить некоторые DNS‑записи**.
    * Для этого программа продления должна поддерживать API DNS‑провайдера, поэтому, в зависимости от используемого провайдера DNS, этот вариант может быть доступен или нет.
* **Запуститься как сервер** (как минимум на время получения сертификатов) на публичном IP‑адресе, связанном с доменом.
    * Как сказано выше, только один процесс может слушать конкретный IP и порт.
    * Это одна из причин, почему очень удобно, когда тот же прокси‑сервер TLS-терминации также занимается процессом продления сертификатов.
    * В противном случае вам, возможно, придётся временно остановить прокси‑сервер TLS-терминации, запустить программу продления для получения сертификатов, затем настроить их в прокси‑сервере TLS-терминации и перезапустить его. Это не идеально, так как ваше приложение(я) будут недоступны, пока прокси‑сервер TLS-терминации остановлен.

Весь этот процесс продления, совмещённый с обслуживанием приложения, — одна из главных причин иметь **отдельную систему для работы с HTTPS** в виде прокси‑сервера TLS-терминации, вместо использования TLS‑сертификатов напрямую в сервере приложения (например, Uvicorn).

## Пересылаемые HTTP-заголовки прокси { #proxy-forwarded-headers }

Когда вы используете прокси для обработки HTTPS, ваш **сервер приложения** (например, Uvicorn через FastAPI CLI) ничего не знает о процессе HTTPS, он общается обычным HTTP с **прокси‑сервером TLS-терминации**.

Обычно этот **прокси** на лету добавляет некоторые HTTP‑заголовки перед тем, как переслать запрос на **сервер приложения**, чтобы тот знал, что запрос был **проксирован**.

/// note | Технические детали

Заголовки прокси:

* [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For)
* [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto)
* [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host)

///

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

Но вы можете настроить **сервер приложения**, чтобы он доверял *пересылаемым* заголовкам, отправленным **прокси**. Если вы используете FastAPI CLI, вы можете использовать *опцию CLI* `--forwarded-allow-ips`, чтобы указать, с каких IP‑адресов следует доверять этим *пересылаемым* заголовкам.

Например, если **сервер приложения** получает запросы только от доверенного **прокси**, вы можете установить `--forwarded-allow-ips="*"`, чтобы доверять всем входящим IP, так как он всё равно будет получать запросы только с IP‑адреса, используемого **прокси**.

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

Это будет полезно, например, для корректной обработки редиректов.

/// tip | Совет

Подробнее об этом вы можете узнать в документации: [За прокси — Включить пересылаемые заголовки прокси](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers)

///

## Резюме { #recap }

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

Зная базовую информацию о **HTTPS для разработчиков**, вы сможете легко комбинировать и настраивать разные инструменты, чтобы управлять всем этим простым способом.

В некоторых из следующих глав я покажу вам несколько конкретных примеров настройки **HTTPS** для приложений **FastAPI**. 🔒
