Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

Артыкул будзе карысны тым, хто:

  • ведае, што такое Client Cert, і разумее для чаго яму websocket-ы на мабільным Safari;
  • хацеў бы публікаваць web-сэрвісы абмежаванаму колу асоб ці толькі сабе;
  • думае, што ўсё ўжо кімсьці зроблена, і хацеў бы зрабіць свет крыху зручней і бяспечней.

Гісторыя вэб-сокетаў пачалася прыкладна 8 гадоў таму. Раней выкарыстоўваліся метады выгляду доўгіх http-запытаў (насамрэч адказаў): браўзэр карыстача адпраўляў запыт на сервер і чакаў, пакуль ён яму нешта адкажа, пасля адказу падлучаўся ізноў і чакаў. Але потым з'явіліся вэб-сокеты.

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

Некалькі гадоў таму мы распрацавалі ўласную рэалізацыю на чыстым php, якая не ўмее выкарыстоўваць запыты https, бо гэта канальны ўзровень. Не так даўно практычна ўсе web-серверы навучыліся праксіраваць запыты па https і падтрымліваць connection:upgrade.

Калі гэта здарылася, вэб-сокеты сталі практычна сэрвісам па змаўчанні ў SPA-прыкладанняў, бо як зручна падаваць карыстачу кантэнт па ініцыятыве сервера (перадаць паведамленне ад іншага карыстача або загрузіць новую версію малюнка, дакумента, прэзентацыі, якую зараз хтосьці яшчэ рэдагуе) .

Хоць Сlient Сert з'явіўся ўжо даўнавата, ён усё яшчэ застаецца мала падтрымоўваным, бо стварае масу праблем са спробамі яго абыйсці. І (магчыма :slightly_smiling_face: ) таму IOS-браўзэры (усе, акрамя Safari) не жадаюць яго выкарыстоўваць і запытваць у лакальнага сховішчы сертыфікатаў. Сертыфікаты валодаюць масай пераваг у параўнанні з ключамі login/pass ці ssh або зачыненнем праз firewall патрэбных партоў. Але размова не пра гэта.

На IOS працэдура ўсталёўкі сертыфіката даволі простая (не без спецыфікі), але ўвогуле робіцца па інструкцыям, якіх у сетцы вельмі шмат і якія даступныя толькі для браўзэра Safari. Нажаль, Safari не ўмее выкарыстоўваць Сlient Сert для вэб-сокетаў, але ў інтэрнэце ёсць мноства інструкцый, як зрабіць такі сертыфікат, але на практыку гэта недасяжна.

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

Каб разабрацца ў вэб-сокетах, мы выкарыстоўвалі наступны план: праблема/гіпотэза/рашэнне.

Праблема: адсутнічае падтрымка вэб-сокетаў пры праксіраванні запытаў да рэсурсаў, якія абаронены кліенцкім сертыфікатам на мабільным браўзэры Safari для IOS і іншых прыкладанняў, якія ўключылі ў сябе падтрымку сертыфікатаў.

Гіпотэзы:

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

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

Праверка гіпотэз:

1. Магчыма наладзіць такое выключэнне для выкарыстання сертыфікатаў (ведаючы, што іх не будзе) да вэб-сокетаў унутраных / знешніх праксіраваных рэсурсаў.

Тут было знойдзена 2 рашэнні:

а) На ўзроўні

<Location sock*> SSLVerifyClient optional </Location>
<Location /> SSLVerifyClient require </Location>

мяняць узровень доступу.

У дадзенага метаду ўзніклі такія нюансы:

  • Праверка сертыфіката адбываецца пасля запыту да праксіраванага рэсурсу, гэта значыць post request handshake. Гэта азначае, што проксі спачатку нагрузіць, а потым адсячэ запыт да які абараняецца сэрвісу. Гэта дрэнна, але не крытычна;
  • У пратаколе http2. Ён яшчэ знаходзіцца ў draft-е, і вытворцы браўзэраў не ведаюць, як яго рэалізаваць. Implement RFC 8740 "Using TLS 1.3 with HTTP/2";
  • Незразумела, як уніфікаваць гэтую апрацоўку.

б) На базавым узроўні дазволіць 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"

Больш падрабязная інфармацыя - у артыкуле аб ssl: Apache Server Client Certificate Authentication

Абодва варыянты былі правераны, абраны варыянт "б" за ўніверсальнасць і сумяшчальнасць з пратаколам http2.

Для завяршэння праверкі гэтай гіпотэзы запатрабавалася нямала эксперыментаў з канфігурацыяй, былі правераны канструкцыі:

if = require = rewrite

Атрымалася наступная базавая канструкцыя:

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), падрабязней у дакументацыі:

Apache Module mod_ssl

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

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 гадавой даўнасці), а значыць токены, хоць і бяспечныя для ўнутранага выкарыстання, але небяспечныя для прамысловага (масавага).

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

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:

Вывучыўшы розніцу з Nginx і Apache:

І даступныя функцыі ад вытворцы мовы 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.OK
end

І вось так гэта ўсё працуе ў суме, з аптымізацыяй ліку Cookie і заменай токена пры надыходзе палавіннага часу да заканчэння старых Cookie (токена):

SSLVerifyClient optional

#LuaScope thread
#generate event variables zt-cert-date-next
LuaHookAccessChecker /usr/local/etc/apache24/sslincludes/websocket_token.lua handler early

#запрещаем без сертификата что-то ещё, кроме webscoket
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 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.

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

Спасылка на крыніцу малюнка.

Яшчэ адзін момант.

У цэлым усё роўна, у якой паслядоўнасці ў канфігурацыі Аpache (верагодна і Nginx) напісаны дырэктывы, бо ў выніку ўсё будзе адсартавана зыходзячы з чарговасці мінання запыту ад карыстача, які адпавядае схеме для адпрацоўкі Lua-скрыптоў.

Завяршэнне:

Бачны стан пасля ўкаранення (мэта):
кіраванне сэрвісамі і інфраструктурай даступна з мабільнага тэлефона на IOS без дадатковых праграм (VPN), уніфікавана і бяспечна.

Мэта дасягнута, вэб-сокеты працуюць і валодаюць не меншым узроўнем бяспекі, чым сертыфікат.

Як мы ў ZeroTech пасябравалі Apple Safari і кліенцкія сертыфікаты з websocket-амі

Крыніца: habr.com

Дадаць каментар