Ситуация типичная: сервер российского API отдаёт цепочку, подписанную сертификатами Минцифры, а Python-клиент падает с CERTIFICATE_VERIFY_FAILED. Соблазн быстрый — поставить verify=False или закинуть корневой сертификат в системное хранилище. Первое отключает проверку и оставляет вас без защиты от подмены, второе расширяет доверие на все приложения в системе. Есть третий путь: доверять нужной цепочке ровно в одном клиенте, не трогая остальную среду. Ниже — как это сделать осознанно и что при этом важно не упустить.

Почему стандартная проверка не проходит

По умолчанию Python-клиенты проверяют цепочку до известного корневого сертификата из системного или встроенного хранилища. Если сервер присылает неполную цепочку, а корень Минцифры отсутствует в доверенных, проверка обрывается. Иногда проблема не в отсутствии корня, а в том, что цепочка приходит без промежуточного звена, и клиент не может её достроить. Прежде чем что-то менять, полезно понять, что именно приходит на проводе: полная цепочка, частичная или самоподписанный якорь. Это влияет на выбор решения.

Типичная ошибка — лечить симптом, не разобравшись в причине. Если сервер отдаёт промежуточный сертификат, а корень отсутствует, можно сделать этот промежуточный сертификат якорем доверия для конкретного клиента. Именно для таких случаев в Python есть флаг VERIFY_X509_PARTIAL_CHAIN: он разрешает считать доверенным сертификат, который не является корневым, если он есть в вашем хранилище.

Свой SSLContext: пошаговая настройка

Смысл подхода — создать отдельный SSLContext, загрузить в него нужный сертификат как якорь и передать этот контекст только тому клиенту, которому вы доверяете. Остальные соединения в процессе продолжают работать по стандартным правилам.

  • Получите сертификат, которому собираетесь доверять: промежуточный из цепочки сервера или корневой Минцифры, если он вам действительно нужен как якорь.
  • Сохраните его в отдельный файл, например mincifry_anchor.pem, и держите вне репозитория с кодом, если это возможно.
  • Создайте контекст: ssl.create_default_context(), затем загрузите якорь через load_verify_locations(cafile=...).
  • Включите VERIFY_X509_PARTIAL_CHAIN в context.verify_flags, чтобы промежуточный сертификат мог выступать якорем.
  • Передайте контекст в конкретный клиент: в requests и httpx это параметр verify с путём к файлу или объектом контекста, в aiohttp — ssl=context, в urllib — context=context в urlopen.

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

Как это выглядит в разных клиентах

В requests достаточно передать путь к PEM-файлу в verify или собрать контекст вручную и передать его. В httpx аналогично: параметр verify принимает либо путь, либо SSLContext. В aiohttp контекст передаётся в TCPConnector(ssl=context). В urllib — в urlopen(..., context=context). Во всех случаях флаг частичной цепочки задаётся на уровне контекста, а не клиента, поэтому его нужно выставить до создания соединений.

Частая ошибка — задать флаг, но забыть загрузить якорь. Тогда частичная цепочка не сработает: клиенту не от чего строить доверие. Другая ошибка — загрузить корень Минцифры в системное хранилище «на всякий случай», хотя задача была локальной. Это возвращает нас к расширению поверхности доверия.

Риски и проверка отпечатка

Флаг VERIFY_X509_PARTIAL_CHAIN меняет правила построения цепочки: якорем может стать сертификат, который не является корневым. Это удобно, но требует дисциплины. Если вы доверяете промежуточному сертификату, вы фактически доверяете всему, что он подписывает в рамках этой цепочки. Поэтому храните якорь в защищённом месте, ограничьте доступ к файлу и не переиспользуйте контекст там, где он не нужен.

Перед тем как доверять сертификату, сверьте его отпечаток с эталоном, полученным по независимому каналу. Это защищает от подмены на этапе получения файла. Отпечаток можно вычислить стандартными средствами и сравнить вручную; автоматическая сверка в коде полезна, но не заменяет проверку источника сертификата.

  • Храните якорь отдельно от кода и не коммитьте его в публичный репозиторий.
  • Ограничьте область применения контекста одним клиентом или одним модулем.
  • Сверяйте отпечаток сертификата при обновлении.
  • Не отключайте проверку хоста и не используйте verify=False как обходной путь.

Отдельно продумайте, что произойдёт при смене сертификата на стороне API. Если якорь устареет, клиент снова начнёт падать. Заранее опишите процедуру обновления: где лежит файл, кто его меняет, как проверяется отпечаток и как откатиться. Это дешевле, чем разбираться в инциденте в момент, когда интеграция уже сломана.

Что в итоге

Локальный SSLContext с промежуточным сертификатом как якорем и флагом VERIFY_X509_PARTIAL_CHAIN решает задачу точечно: доверие действует только в одном клиенте, системное хранилище не меняется, проверка не отключается. Это компромисс между удобством и безопасностью, и он оправдан, когда нужно подключиться к конкретному API с цепочкой Минцифры. Главное — не превращать временное решение в привычку: фиксируйте, кому и зачем вы доверяете, проверяйте отпечатки и держите процедуру обновления сертификата под контролем.