знает, что такое 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-файле для того, чтобы установить дату из будущего для сверки с текущей.
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
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), унифицировано и безопасно.
Цель достигнута, веб-сокеты работают и обладают не меньшим уровнем безопасности, чем сертификат.