Очень удобно, когда благодаря правильно выбранным умолчаниям все работает само и «из коробки» и не нужно ничего настраивать. Эта история о том, что выбранные умолчания должны быть работоспособными всегда, в противном случае есть риск непредвиденного отказа после многих лет беспроблемной работы.
Мы столкнулись с недокументированным поведением Windows Server в web-ролях Microsoft Azure, которое долгие годы маскировало неправильную настройку нашего сервиса Cloud OCR SDK, пока в один не самый прекрасный момент не привело к серьезным проблемам у отдельных пользователей.
В феврале 2016 года – к этому времени сервис работал уже несколько лет – отдельные пользователи стали сообщать о проблемах при попытке установить защищенное соединение. Программы этих пользователей выдавали сообщения о каких-то проблемах с сертификатом сервиса. Поскольку «до этого работало, а теперь сломалось», первым делом пользователи решили, что у сертификата истек срок действия, и вполне предсказуемо были Очень НедовольныTM. На самом деле срок действия истекал только несколько месяцев спустя.
Попытки воспроизвести проблему чаще всего были неудачными – даже на точно тех платформах, которые указывали пользователи. При помощи сторонних средств проверки (один и два) иногда удавалось получить сообщение о неправильной установке промежуточных сертификатов.
С этого момента нужны технические подробности. Выпуск сертификатов осуществляют специальные организации – сертификационные центры, которым все доверяют. Каждый выпущенный сертификат подписан цифровой подписью, которую программы-клиенты проверяют.
У каждого сертификационного центра есть один или несколько самоподписанных корневых сертификатов, которым программы-клиенты безусловно доверяют. Сертификат, выпускаемый сертификационным центром, может в принципе быть подписан корневым сертификатом этого центра, но для этого закрытый ключ сертификата нужен при каждом выпуске нового сертификата, это повышает риск утечки закрытого ключа корневого сертификата. Утечка закрытого ключа корневого сертификата, которому безусловно доверяют миллионы вычислительных систем, – это проблема.
Поэтому сертификационные центры используют промежуточные сертификаты. Промежуточный сертификат однократно подписывают корневым сертификатом, а все выпускаемые сертификаты уже подписывают промежуточным. Возможны варианты с несколькими промежуточными сертификатами в цепочке. Программе-клиенту для проверки цифровой подписи нужны все сертификаты в цепочке – программа хранит корневой сертификат и безусловно доверяет ему, с его помощью может проверить подпись только на промежуточном сертификате, а для проверки подписи на сертификате сервиса ей непременно нужен этот промежуточный сертификат.
Для проверки подписей программам-клиентам достаточно сертификатов без закрытых ключей – закрытые ключи хранятся в секрете сертификационным центром (корневые и промежуточные сертификаты) и владельцем сервиса (закрытый ключ сертификата сервиса).
У программы-клиента есть несколько способов получить промежуточные сертификаты.
Первый вариант – промежуточные сертификаты могут быть заренее установлены на машине, где работает программа. Это неудобно, рассчитывать, что пользователи будут устанавливать промежуточные сертификаты, не стоит.
Второй вариант – программа может попытаться скачать их по сети с серверов сертификационного центра. Это удобно, но поддерживается не всеми реализациями клиентов SSL. Также это ненадежно – нужно, чтобы в момент установления соединения был доступ также и к серверам сертификационного центра. Этот способ замедляет установление первого соединения.
Третий способ – самый надежный и универсальный. Сервер сам отправляет клиенту все промежуточные сертификаты одновременно с отправкой своего сертификата в начале установления защищенного соединения. Примерно: «вот мой сертификат, он подписан вот этими сертификатами, из которых последний подписан корневым, который должен у вас, ценнейшая программа-клиент, быть помечен как заслуживающий доверия, извольте все подписи проверить, убедиться и давайте уже соединение установим».
Сторонние средства проверки иногда показывали, что наш сервис отдает промежуточный сертификат при установлении соединения, а иногда – что не отдает. После проверки оказалось, что, следуя доступным примерам, мы неправильно настроили процесс развертывания сервиса.
Вот так выглядит типичное определение сервиса с web-ролью:
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="CoolCloudService"
xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition"
schemaVersion="2014-06.2.4"
>
<WebRole name="CoolRole">
<Sites>
<Site name="Web" >
<Bindings>
<Binding name="HttpIn" endpointName="HttpIn" />
<Binding name="HttpsIn" endpointName="HttpsIn" />
</Bindings>
</Site>
</Sites>
<Endpoints>
<InputEndpoint name="HttpIn" protocol="http" port="80" />
<InputEndpoint name="HttpsIn" protocol="https" port="443" certificate="ProductionCert"/>
</Endpoints>
<Certificates>
<!--НЕПРАВИЛЬНО!!! Не надо так!!!-->
<Certificate name="ProductionCert" storeLocation="LocalMachine" storeName="My"/>
</Certificates>
</WebRole>
</ServiceDefinition>
Обычно внутри элемента Certificates указывают только сертификат сервиса. Это неправильно. Правильно там же указать и все промежуточные сертификаты в цепочке. Не имеет значения, что они не упоминаются в разделе Endpoints.
Так правильно:
<Certificates>
<!--Так правильно! -->
<Certificate name="IntermediateForProductionCert" storeLocation="LocalMachine" storeName="CA"/>
<!—Если в цепочке более одного промежуточного сертификата, перечислить нужно все-->
<Certificate name="ProductionCert" storeLocation="LocalMachine" storeName="My"/>
</Certificates>
Перечисление всех промежуточных сертификатов приводит к их установке в хранилища на каждом экземпляре роли во время инициализации экземпляра. После этого IIS сможет отправлять промежуточные сертификаты программам-клиентам при установлении защищенного соединения.
WIN? Нет, не так быстро.
После исправления настроек и публикации изменений мы связались с пользователем, который удачно был в близком часовом поясе, и он ответил, что нет, не помогло, ВСЕ ПЛОХО, ничего не работает.
Следующим претендентом стал образ операционной системы, поверх которого работал сервис. Незадолго до возникновения проблем с проверкой сертификатов сервис был переведен на очередной, более новый, образ с очередным набором обновлений. За редкими исключениями, при которых Microsoft заранее сообщает о возможных проблемах, выбираются только «нейтральные» обновления, которые ничего не ломают. В этот раз в списке оказались два обновления, в описании которых упоминались хеши сертификатов и возобновление защищенного соединения.
Сервис временно перевели на предыдущий образ… и проблема была решена.
WIN? Нет, это даже близко не WIN.
Во-первых, было непонятно, какое из двух изменений решило проблему. Оставаться вечно на более раннем образе нельзя – через пару месяцев инфраструктура Azure принудительно переведет сервис на более новый образ, и тогда проблемы могут повториться. Вариант перевести сервис на более новый образ операционной системы и проверить на пользователях нам как-то не подошел.
Во-вторых, было непонятно, почему проблема повторилась именно в это время, а не раньше – настройки не менялись, но «раньше работало, а потом сломалось».
На тестовом сервисе с неправильными настройками «до» – с указанием только одного сертификата – все сторонние средства диагностики показывали, что сертификаты установлены правильно, возвращаются сервисом в ответ на запрос программы-клиента. Добавленный в тестовый сервис код просмотра установленных сертификатов при этом показывал, что промежуточного сертификата в хранилищах нет.
Сертификата в хранилищах нет, но сервис отдает его программам пользователям. Телепортация, видимо. Обычное дело.
Еще у нас были сообщения пользователей. Среди них было примерно такое описание: «вот кусок кода на PHP, мы выполняем его в цикле, он когда-то отрабатывает успешно, а когда-то – со сбоем, по мере того, как эта ваша проблема ухудшалась, он все чаще отрабатывал со сбоем».
Еще у нас есть код, который периодически проверяет срок действия нашего сертификата и на всякий случай проверяет его цепочку доверия методом X509Chain.Build(). Раньше он отрабатывал нормально, а в период времени, когда пользователи сталкивались с проблемой, — этот метод иногда не отрабатывал, выдавая такой набор сообщений:
- PartialChain A certificate chain could not be built to a trusted root authority.
- RevocationStatusUnknown The revocation function was unable to check revocation for the certificate.
- OfflineRevocation The revocation function was unable to check revocation because the revocation server was offline.
Подозрительно похоже на невозможность обратиться к серверам сертификационного центра.
А что если экземпляр роли умеет сам получать у сертификационного центра недостающие промежуточные сертификаты и заботливо припрятывать их, чтобы IIS мог отдавать их программам-клиентам? Это было бы КРАЙНЕ НЕОЖИДАННО, высказывать такое предположение без весомых доказательств очень легкомысленно, с таким же успехом можно высказать предположение, что Windows ест котят.
Нужно было проверить. Попытки настроить межсетевой экран или подпилить разрешение имен правкой файла hosts на экземпляре роли результата не дали. Это было ожидаемо – до момента, когда впервые появляется возможность выполнить эти действия, экземпляр роли работает несколько минут, по окончании которых может быть слишком поздно – сертификат будет скачан и заботливо припрятан.
Поэтому нужен был способ полностью исключить обращения экземпляра роли к инфраструктуре сертификационного центра. Удобно, что Azure позволяет сделать это довольно легко.
Нам хватило «виртуальной сети» (virtual network), в которой мы создали подсеть (subnet) с «группой сетевой безопасности» (network security group), далее в настройках сервиса добавили элемент NetworkConfiguration, чтобы сервис публиковался в эту виртуальную сеть. Это было несложно.
В «группе сетевой безопасности» настраиваются ограничения доступа к сети. Там добавили правило, запрещающее исходящие запросы к диапазону адресов, по которым размещена инфраструктура сертификационного центра.
Если после этого опубликовать тестовый сервис, заявленная пользователями проблема начинает воспроизводиться независимо от свежести образа операционной системы. IIS перестает отдавать промежуточный сертификат. Если в «группе безопасности» изменить правило так, чтобы запросы не ограничивались, и опубликовать сервис еще раз, проблема перестает воспроизводиться.
После основательной проверки этого черного ящика мы установили, что попытка получить промежуточный сертификат производится при изначальной инициализации экземпляра роли, при ее перезагрузке, при ее повторной инициализации с образа операционной системы и при повторной публикации пакета сервиса. Перезапуск пула приложений в IIS не приводит к попытке получения недостающего сертификата. Таким образом, попытка получения промежуточного сертификата связана с моментом развертывания сайта в IIS – предположительно, она происходит в момент установки сертификата сайта. В этой статье (How Certificate Revocation Works) упоминается некий дисковый кеш сертификатов CryptoAPI. CryptoAPI – часть Windows Server.
В обычной ситуации попытка получить недостающий сертификат проходит успешно, неправильная настройка сервиса маскируется, об этом в журналах экземпляра роли нет никаких предупреждений. Если в неподходящий момент попытка получить недостающий сертификат не проходит, конкретный экземпляр роли запускается в не самой лучшей форме, об этом в журналах также нет никаких сообщений, но теперь некоторые программы-клиенты не могут с ним установить защищенное соединение.
Нужно добавить сюда масштабирование и балансировщик нагрузки, распределяющий входящие запросы между экземплярами роли. Разные экземпляры роли могут запускаться в разное время и потому при разной доступности инфраструктуры сертификационного центра, в результате одни экземпляры оказываются в лучшей форме, чем другие. Балансировщик нагрузки по своему усмотрению отправляет разные запросы в разные экземпляры, поэтому разные запросы могут приводить к разному набору сертификатов в ответе. Малоактивные пользователи видят, что «то работает, то не работает», а очень активные – что некоторая доля запросов завершается успешно, а остальные – со сбоем.
Головоломка сложилась. Вот теперь WIN.
Определенно, это не самое удачное поведение по умолчанию.
Теперь у нас есть код, который при проверке нашего сертификата заодно проверяет, что все сертификаты в цепочке лежат в соответствующих хранилищах экземпляра роли. Благодаря силе свежих пул-запросов примеры в документации к Microsoft Azure скоро исправят.
И если у вас есть сервис с web-ролями, очень может быть, что он все еще настроен неправильно.
Дмитрий Мещеряков,
департамент продуктов для разработчиков
Автор: ABBYY