ProHoster > Блог > адміністрування > Як ми в ZeroTech подружили Apple Safari та клієнтські сертифікати з websocket-ами
Як ми в ZeroTech подружили Apple Safari та клієнтські сертифікати з websocket-ами
Стаття буде корисна тим, хто:
знає, що таке 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 та інших програм, які включили підтримку сертифікатів.
Гіпотези:
Можна налаштувати такий виняток для використання сертифікатів (знаючи, що їх не буде) до веб-сокетів внутрішніх/зовнішніх ресурсів, що проксуються.
Для веб-сокетів можна зробити унікальне безпечне та захищене з'єднання за допомогою тимчасових сесій, які генеруються при звичайному (не веб-сокет) запиті браузера.
Тимчасові сесії можна реалізувати за допомогою одного proxy web-сервера (тільки вбудовані модулі та функції).
Тимчасові сесії-токени вже були реалізовані як готові модулі apache.
Тимчасові сесії-токени можна реалізувати, логічно спроектувавши структуру взаємодій.
Видимий стан після застосування.
Мета роботи: управління сервісами та інфраструктурою має бути доступне з мобільного телефону на 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. Можна налаштувати такий виняток для використання сертифікатів (знаючи, що їх не буде) до веб-сокетів внутрішніх/зовнішніх ресурсів, що проксуються.
Перевірка сертифіката відбувається після запиту до ресурсу, що проксується, тобто post request handshake. Це означає, що проксі спочатку навантажить, а потім відсіче запит до сервісу, що захищається. Це погано, але не критично;
б) На базовому рівні дозволити SSL без сертифіката.
SSLVerifyClient require => SSLVerifyClient optional, але це знижує рівень захисту proxy-сервера, оскільки таке з'єднання буде оброблено без сертифіката. Однак можна далі заборонити доступ до проксованих сервісів такою директивою:
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteRule .? - [F]
ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"
SSLVerifyClient optional
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteCond %{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), докладніше у документації:
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 check
SetEnvIf Cookie "websocket-allowed=(.*)" env-var-name=$1
#or rewrite rule
RewriteCond %{HTTP_COOKIE} !^.*mycookie.*$
#or if
<If "%{HTTP_COOKIE} =~ /(^|; )cookie-names*=s*some-val(;|$)/ >
</If
</If>
</If>
Перевірка засвідчила, що це працює. Можливо через браузер користувача передавати собі 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
Так, готові модулі є, але всі прив'язані до конкретних дій і мають артефакти у вигляді старту сесії та додаткових Cookie. Тобто не на якийсь час.
Ми пішли п'ять годин на пошук, який не дав конкретного результату.
5. Тимчасові сесії-токени можна реалізувати, логічно спроектувавши структуру взаємодій.
Готові модулі надто складні, адже нам потрібно лише кілька функцій.
При цьому проблема з датою в тому, що вбудовані функції Apache не дозволяють генерувати дату з майбутнього, а при перевірці старіння у вбудованих функціях немає математичної складання/віднімання.
Тобто не можна написати:
(%{env:zt-cert-date} + 30) > %{DATE}
Можна порівнювати лише два числа.
Під час пошуку обходу проблеми Safari знайшлася цікава стаття: Securing HomeAssistant with client certificates (роботи з Safari/iOS)
У ній описаний приклад коду Lua для Nginx, і який, як виявилося, дуже повторює логіку тієї частини конфігурації, яку ми вже раніше реалізували, за винятком використання hmac-методу розстановки солі для хешування (такого в Apache не знайшлося).
Стало зрозуміло, що Lua - це мова, зі зрозумілою логікою, можна зробити щось просте і для Apache:
В цілому неважливо, в якій послідовності в конфігурації Аpache (ймовірно і Nginx) написані директиви, тому що в результаті все буде відсортовано виходячи з черговості проходження запиту від користувача, який відповідає схемі відпрацювання Lua-скриптів.
Завершення:
Видимий стан після впровадження (мета):
управління сервісами та інфраструктурою доступне з мобільного телефону на IOS без додаткових програм (VPN), уніфіковано та безпечно.
Мета досягнута, веб-сокети працюють і мають не менший рівень безпеки, ніж сертифікат.