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 і іншых прыкладанняў, якія ўключылі ў сябе падтрымку сертыфікатаў.
Гіпотэзы:
Магчыма наладзіць такое выключэнне для выкарыстання сертыфікатаў (ведаючы, што іх не будзе) да вэб-сокетаў унутраных/вонкавых праксаваных рэсурсаў.
Для вэб-сокетаў можна зрабіць унікальнае бяспечнае і якое абараняецца злучэнне з дапамогай часавых сесій, якія генеруюцца пры звычайным (не вэб-сокет) запыце браўзэра.
Часовыя сесіі можна рэалізаваць з дапамогай аднаго проксі-вэб-сервера (толькі ўбудаваныя модулі і функцыі).
Часовыя сесіі-токены ўжо былі рэалізаваны ў якасці гатовых модуляў 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. Часовыя сэсіі можна рэалізаваць з дапамогай аднаго проксі-вэб-сэрвэра (толькі ўбудаваныя модулі і функцыі).
Як мы высветлілі раней, у 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
Так, гатовыя модулі ёсць, але ўсё прывязаныя да пэўных дзеянняў і валодаюць артэфактамі ў выглядзе старту сесіі і дадатковых 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), уніфікавана і бяспечна.
Мэта дасягнута, вэб-сокеты працуюць і валодаюць не меншым узроўнем бяспекі, чым сертыфікат.