Статья будет полезна тем, кто:
знает, что такое Client Cert, и понимает для чего ему websocket-ы
на мобильном Safari;
хотел бы публиковать web-сервисы ограниченному кругу лиц или только
себе;
думает, что всё уже кем-то сделано, и хотел бы сделать мир немного
удобнее и безопаснее.
История веб-сокетов началась примерно 8 лет назад. Ранее
использовались методы вида долгих http-запросов (на самом деле
ответов): браузер пользователя отправлял запрос на сервер и ждал,
пока он ему что-то ответит, после ответа подключался вновь и ждал.
Но потом появились веб-сокеты.
Несколько лет назад мы разработали собственную реализацию на чистом
php, которая не умеет использовать запросы https, так как это
канальный уровень. Не так давно практически все web-серверы
научились проксировать запросы по https и поддерживать
connection:upgrade.
Когда это случилось, веб-сокеты стали практически сервисом по
умолчанию у SPA-приложений, ведь как удобно предоставлять
пользователю контент по инициативе сервера (передать сообщение от
другого пользователя или загрузить новую версию изображения,
документа, презентации, которую сейчас кто-то ещё редактирует).
Хотя Сlient Сert появился уже довольно давно, он всё ещё остаётся
мало поддерживаемым, так как создаёт массу проблем с попытками его
обойти. И (возможно :slightly_smiling_face: ) поэтому IOS-браузеры
(все, кроме Safari) не хотят его использовать и запрашивать у
локального хранилища сертификатов. Сертификаты обладают массой
преимуществ по сравнению с ключами login/pass или ssh или закрытием
через firewall нужных портов. Но речь не об этом.
На IOS процедура установки сертификата довольно проста (не без
специфики), но в общем делается по инструкциям, которых в сети
очень много и которые доступны только для браузера Safari. К
сожалению, Safari не умеет использовать Сlient Сert для
веб-сокетов, но в интернете есть множество инструкций, как сделать
такой сертификат, но на практике это недостижимо.
Чтобы разобраться в веб-сокетах, мы использовали следующий план:
проблема/гипотеза/решение.
Проблема: отсутствует поддержка веб-сокетов при
проксировании запросов к ресурсам, которые защищены клиентским
сертификатом на мобильном браузере Safari для IOS и других
приложений, которые включили у себя поддержку сертификатов.
Гипотезы:
1. Возможно настроить такое исключение для использования
сертификатов (зная, что их не будет) к веб-сокетам
внутренних/внешних проксируемых ресурсов.
2. Для веб-сокетов можно сделать уникальное безопасное и защищаемое
соединение с помощью временных сессий, которые генерируются при
обычном (не веб-сокет) запросе браузера.
3. Временные сессии можно реализовать с помощью одного proxy
web-сервера (только встроенные модули и функции).
4. Временные сессии-токены уже были реализованы в качестве готовых
модулей apache.
5. Временные сессии-токены можно реализовать, логически
спроектировав структуру взаимодействий.
Видимое состояние после внедрения.
Цель работы: управление сервисами и
инфраструктурой должно быть доступно с мобильного телефона на IOS
без дополнительных программ (таких как VPN), унифицировано и
безопасно.
Дополнительная цель: экономия времени и
ресурсов/трафика телефона (некоторые сервисы без веб-сокетов
генерируют лишние запросы) с ускорением отдачи контента на
мобильном интернете.
Как проверить?
1. Открытие страниц:
например, https://teamcity.yourdomain.com в мобильном браузере Safari (доступен также в десктопной версии) вызывает успешное подключение к веб-сокетам. например, https://teamcity.yourdomain.com/admin/admin.html?item=diagnostics&tab=webS показывает ping/pong. например, https://rancher.yourdomain.com/p/c-84bnv:p-vkszd/workload/deployment:danidb:ph-> viewlogs показывает логи контейнера.
2. Или в консоли разработчика:
Проверка гипотез:
1. Возможно настроить такое исключение для использования
сертификатов (зная, что их не будет) к веб-сокетам
внутренних/внешних проксируемых ресурсов.
Тут было найдено 2 решения:
а) На уровне
<Location sock*> SSLVerifyClient optional </Location><Location /> SSLVerifyClient require </Location>
менять уровень доступа.
У данного метода возникли такие нюансы:
Проверка сертификата происходит после запроса к проксируемому
ресурсу, то есть post request handshake. Это означает, что прокси
сначала нагрузит, а потом отсечёт запрос к защищаемому сервису. Это
плохо, но не критично;
В протоколе http2. Он ещё находится в draft-е, и производители
браузеров не знают, как его реализовать #info about tls1.3 http2
post handshake (not working now)
Implement RFC 8740 Using TLS
1.3 with HTTP/2;
Непонятно, как унифицировать эту обработку.
б) На базовом уровне разрешить ssl без сертификата.
SSLVerifyClient require => SSLVerifyClient optional, но это
снижает уровень защиты proxy-сервера, так как такое соединение
будет обработано без сертификата. Однако можно далее запретить
доступ к проксируемым сервисам такой директивой:
RewriteEngine onRewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESSRewriteRule .? - [F]ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"
Более подробная информация в статье о ssl:
Apache Server Client
Certificate Authentication
Оба варианта были проверены, выбран вариант б за универсальность и
совместимость с протоколом http2.
Для завершения проверки этой гипотезы потребовалось немало
экспериментов с конфигурацией, были проверены конструкции:
if = require = rewrite
Apache Core Features
Expressions in Apache HTTP Server
Получилась следующая базовая конструкция:
SSLVerifyClient optionalRewriteEngine onRewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESSRewriteCond %{HTTP:Upgrade} !=websocket [NC]RewriteRule .? - [F]#ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"#websocket for safari without cert auth<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'"><If "%{HTTP:Upgrade} = 'websocket'">... #замещаем авторизацию по владельцу сертификата на авторизацию по номеру протокола SSLUserName SSl_PROTOCOL</If></If>
С учётом существующей авторизации по владельцу сертификата, но при
отсутствующем сертификате пришлось добавить несуществующего
владельца сертификата в виде одной из доступных переменных
SSl_PROTOCOL (вместо SSL_CLIENT_S_DN_CN), подробнее в
документации:
Apache Module mod_ssl
2. Для веб-сокетов можно сделать уникальное безопасное и защищаемое
соединение с помощью временных сессий, которые генерируются при
обычном (не веб-сокет) запросе браузера.
Исходя из предыдущего опыта нужно добавить дополнительную секцию в
конфигурацию, чтобы при обычном (не веб-сокет) запросе готовить
временные токены для веб-сокет соединений.
#подготовка передача себе Сookie через пользовательский браузер<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'"><If "%{HTTP:Upgrade} != 'websocket'">Header set Set-Cookie "websocket-allowed=true; path=/; Max-Age=100"</If></If>#проверка Cookie для установления веб-сокет соединения<source lang="javascript"><If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'"><If "%{HTTP:Upgrade} = 'websocket'">#check for exists cookie#get and checkSetEnvIf Cookie "websocket-allowed=(.*)" env-var-name=$1#or rewrite ruleRewriteCond %{HTTP_COOKIE} !^.*mycookie.*$#or if<If "%{HTTP_COOKIE} =~ /(^|; )cookie-name\s*=\s*some-val(;|$)/ ></If</If></If>
Проверка показала, что это работает. Возможно через
пользовательский браузер передавать cебе Cookie.
3. Временные сессии можно реализовать с помощью одного proxy
web-сервера (только встроенные модули и функции).
Как мы выяснили ранее, у Apache довольно много
core-функциональности, которая позволяет создавать условные
конструкции. Однако нам нужны средства защиты нашей информации,
пока она находится в пользовательском браузере, поэтому
устанавливаем, что и для чего хранить, и какие встроенные функции
будем задействовать:
Нужен такой токен, который не поддаётся простому декодированию.
Нужен такой токен, в котором зашито устаревание и возможность
проверки устаревания на сервере.
Нужен такой токен, который будет связан с владельцем
сертификата.
Для этого нужна функция хеширования, соль и дата для устаревания
токена. Исходя из документации
Expressions in Apache HTTP
Server у нас есть всё это из коробки sha1 и %{TIME}.
Получилась такая конструкция:
#нет сертификата, и обращение к websocket<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'"><If "%{HTTP:Upgrade} = 'websocket'"> SetEnvIf Cookie "zt-cert-sha1=([^;]+)" zt-cert-sha1=$1 SetEnvIf Cookie "zt-cert-uid=([^;]+)" zt-cert-uid=$1 SetEnvIf Cookie "zt-cert-date=([^;]+)" zt-cert-date=$1#только так можно работать с переменными, полученными в env-ах в этот момент времени, более они нигде не доступны для функции хеширования (по отдельности можно, но не вместе, да и ещё с хешированием) <RequireAll> Require expr %{sha1:salt1%{env:zt-cert-date}salt3%{env:zt-cert-uid}salt2} == %{env:zt-cert-sha1} Require expr %{env:zt-cert-sha1} =~ /^.{40}$/ </RequireAll></If></If>#есть сертификат, запрашивается не websocket<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'"><If "%{HTTP:Upgrade} != 'websocket'"> SetEnvIf Cookie "zt-cert-sha1=([^;]+)" HAVE_zt-cert-sha1=$1 SetEnv zt_cert "path=/; HttpOnly;Secure;SameSite=Strict"#Новые куки ставятся, если старых нет Header add Set-Cookie "expr=zt-cert-sha1=%{sha1:salt1%{TIME}salt3%{SSL_CLIENT_S_DN_CN}salt2};%{env:zt_cert}" env=!HAVE_zt-cert-sha1 Header add Set-Cookie "expr=zt-cert-uid=%{SSL_CLIENT_S_DN_CN};%{env:zt_cert}" env=!HAVE_zt-cert-sha1 Header add Set-Cookie "expr=zt-cert-date=%{TIME};%{env:zt_cert}" env=!HAVE_zt-cert-sha1</If></If>
Цель достигнута, но есть проблемы с серверным устареванием (можно
использовать Cookie годичной давности), а значит токены, хоть и
безопасны для внутреннего использования, но небезопасны для
промышленного (массового).
4. Временные сессии-токены уже были реализованы в качестве готовых
модулей Аpache.
С предыдущей итерации осталась одна существенная проблема
невозможность контролировать устаревание токена.
Ищем готовый модуль, который это делает, по словам: apache token
json two factor auth
Client authentication using tokens based on JSON Web
Tokens
Apache Two-Factor (2FA) Authentication
How to Add Two-Factor Authentication to Apache
Bring two-factor authentication to your Apache instance with a
simple module install
Да, готовые модули есть, но все привязаны к конкретным действиями и
обладают артефактами в виде старта сессии и дополнительных Cookie.
То есть не на время.
У нас ушло пять часов на поиск, который не дал конкретного
результата.
5. Временные сессии-токены можно реализовать, логически
спроектировав структуру взаимодействий.
Готовые модули слишком сложны, ведь нам нужно только пару
функций.
При этом проблема с датой в том, что встроенные функции Apache не
позволяют генерировать дату из будущего, а при проверке устаревания
во встроенных функциях нет математического сложения/вычитания.
То есть нельзя написать:
(%{env:zt-cert-date} + 30) > %{DATE}
Можно сравнивать только два числа.
При поиске обхода проблемы Safari нашлась интересная статья:
Securing HomeAssistant with client certificates (works with
Safari/iOS)
В ней описан пример кода на Lua для Nginx, и который, как
оказалось, очень повторяет логику той части конфигурации, которую
мы уже ранее реализовали, за исключением использования hmac-метода
расстановки соли для хеширования (такого в Apache не нашлось).
Стало понятно, что Lua это язык, с понятной логикой, возможно
сделать что-то простенькое и для Apache:
LuaHookAccessChecker Directive
UnsetEnv Directive
Изучив разницу с Nginx и Apache:
modules_lua
lua_load_resty_core
И доступные функции от производителя языка Lua:
22.1 Date and Time
Найден способ задания переменных env в небольшом Lua-файле для
того, чтобы установить дату из будущего для сверки с текущей.
Вот так выглядит простенький Lua-скрипт:
require 'apache2'function handler(r) local fmt = '%Y%m%d%H%M%S' local timeout = 3600 -- 1 hour r.notes['zt-cert-timeout'] = timeout r.notes['zt-cert-date-next'] = os.date(fmt,os.time()+timeout) r.notes['zt-cert-date-halfnext'] = os.date(fmt,os.time()+ (timeout/2)) r.notes['zt-cert-date-now'] = os.date(fmt,os.time()) return apache2.OKend
И вот так это всё работает в сумме, c оптимизацией
числа Cookie и заменой токена при наступлении половинного времени
до истечения старых Cookie (токена):
SSLVerifyClient optional#LuaScope thread#generate event variables zt-cert-date-nextLuaHookAccessChecker /usr/local/etc/apache24/sslincludes/websocket_token.lua handler early#запрещаем без сертификата что-то ещё, кроме webscoketRewriteEngine onRewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESSRewriteCond %{HTTP:Upgrade} !=websocket [NC]RewriteRule .? - [F]#ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"#websocket for safari without certauth<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'"><If "%{HTTP:Upgrade} = 'websocket'"> SetEnvIf Cookie "zt-cert=([^,;]+),([^,;]+),[^,;]+,([^,;]+)" zt-cert-sha1=$1 zt-cert-date=$2 zt-cert-uid=$3 <RequireAll> Require expr %{sha1:salt1%{env:zt-cert-date}salt3%{env:zt-cert-uid}salt2} == %{env:zt-cert-sha1} Require expr %{env:zt-cert-sha1} =~ /^.{40}$/ Require expr %{env:zt-cert-date} -ge %{env:zt-cert-date-now} </RequireAll> #замещаем авторизацию по владельцу сертификата на авторизацию по номеру протокола SSLUserName SSl_PROTOCOL SSLOptions -FakeBasicAuth</If></If><If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'"><If "%{HTTP:Upgrade} != 'websocket'"> SetEnvIf Cookie "zt-cert=([^,;]+),[^,;]+,([^,;]+)" HAVE_zt-cert-sha1=$1 HAVE_zt-cert-date-halfnow=$2 SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge %{TIME} && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1 Define zt-cert "path=/;Max-Age=%{env:zt-cert-timeout};HttpOnly;Secure;SameSite=Strict" Define dates_user "%{env:zt-cert-date-next},%{env:zt-cert-date-halfnext},%{SSL_CLIENT_S_DN_CN}" Header set Set-Cookie "expr=zt-cert=%{sha1:salt1%{env:zt-cert-date-next}sal3%{SSL_CLIENT_S_DN_CN}salt2},${dates_user};${zt-cert}" env=!HAVE_zt-cert-sha1-found</If></If>SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge %{TIME} && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1работает,а так работать не будетSetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge env('zt-cert-date-now') && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1
Потому что LuaHookAccessChecker будет активирован только после
проверок доступа исходя из этой информации от Nginx.
Cсылка на источник
изображения.
Ещё один момент.
В целом неважно, в какой последовательности в конфигурации Аpache
(вероятно и Nginx) написаны директивы, так как в итоге всё будет
отсортировано исходя из очерёдности прохождения запроса от
пользователя, который соответствует схеме для отработки
Lua-скриптов.
Завершение:
Видимое состояние после внедрения (цель):
Управление сервисами и инфраструктурой доступно с мобильного
телефона на IOS без дополнительных программ (VPN), унифицировано и
безопасно.
Цель достигнута, веб-сокеты работают и обладают не меньшим уровнем
безопасности, чем сертификат.