Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

Článok bude užitočný pre tých, ktorí:

  • vie, čo je to Client Cert, a chápe, prečo potrebuje webové zásuvky v mobilnom Safari;
  • Chcel by som publikovať webové služby pre obmedzený okruh ľudí alebo len pre seba;
  • si myslí, že všetko už niekto urobil a chcel by urobiť svet o niečo pohodlnejším a bezpečnejším.

História webových soketov sa začala približne pred 8 rokmi. Predtým sa metódy používali vo forme dlhých http požiadaviek (v skutočnosti odpovedí): prehliadač používateľa poslal požiadavku na server a čakal, kým niečo odpovie, po odpovedi sa znova pripojil a čakal. Potom sa však objavili webové zásuvky.

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

Pred pár rokmi sme vyvinuli vlastnú implementáciu v čistom PHP, ktorá nemôže používať https požiadavky, keďže ide o linkovú vrstvu. Nie je to tak dávno, čo sa takmer všetky webové servery naučili zadávať proxy požiadavky cez https a podporovať connection:upgrade.

Keď sa to stalo, websockets sa stali takmer predvolenou službou pre aplikácie SPA, pretože aké pohodlné je poskytnúť obsah používateľovi z iniciatívy servera (preniesť správu od iného používateľa alebo stiahnuť novú verziu obrázka, dokumentu, prezentácie ktorú práve upravuje niekto iný) .

Hoci klientsky certifikát existuje už nejaký čas, stále zostáva nedostatočne podporovaný, pretože pri pokuse o jeho obídenie spôsobuje veľa problémov. A (možno :slightly_smiling_face: ) to je dôvod, prečo ho prehliadače IOS (všetky okrem Safari) nechcú používať a požadujú ho z lokálneho úložiska certifikátov. Certifikáty majú mnoho výhod v porovnaní s kľúčmi login/pass alebo ssh alebo zatváraním potrebných portov cez firewall. Ale o to tu nejde.

V systéme iOS je postup inštalácie certifikátu pomerne jednoduchý (nie bez špecifík), ale vo všeobecnosti sa vykonáva podľa pokynov, ktorých je na internete veľa a sú dostupné iba pre prehliadač Safari. Safari bohužiaľ nevie použiť Client Сert pre webové sokety, no na internete je množstvo návodov, ako si takýto certifikát vytvoriť, no v praxi je to nedosiahnuteľné.

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

Na pochopenie webových soketov sme použili nasledujúci plán: problém/hypotéza/riešenie.

Problém: v mobilnom prehliadači Safari pre IOS a iných aplikáciách, ktoré majú povolenú podporu certifikátov, neexistuje podpora webových soketov pri vytváraní proxy požiadaviek na zdroje, ktoré sú chránené klientskym certifikátom.

hypotézy:

  1. Je možné nakonfigurovať takúto výnimku na používanie certifikátov (s vedomím, že žiadne nebudú) na websockety interných/externých zdrojov proxy.
  2. V prípade websocketov môžete vytvoriť jedinečné, bezpečné a obhajiteľné pripojenie pomocou dočasných relácií, ktoré sa generujú počas normálnej (nie websocket) požiadavky prehliadača.
  3. Dočasné relácie je možné realizovať pomocou jedného proxy webového servera (len vstavané moduly a funkcie).
  4. Dočasné tokeny relácie už boli implementované ako hotové moduly Apache.
  5. Dočasné tokeny relácie možno implementovať logickým návrhom štruktúry interakcie.

Viditeľný stav po realizácii.

Cieľ práce: správa služieb a infraštruktúry by mala byť dostupná z mobilného telefónu na IOS bez dodatočných programov (napríklad VPN), jednotná a bezpečná.

Ďalší cieľ: úspora času a zdrojov/telefonickej prevádzky (niektoré služby bez webových soketov generujú zbytočné požiadavky) s rýchlejším doručovaním obsahu na mobilnom internete.

Ako skontrolovať?

1. Úvodné stránky:

— например, 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. Alebo v konzole pre vývojárov:

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

Testovanie hypotéz:

1. Je možné nakonfigurovať takúto výnimku na používanie certifikátov (s vedomím, že žiadne nebudú) pre webové zásuvky interných/externých zdrojov proxy.

Tu sa našli 2 riešenia:

a) Na úrovni

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

zmeniť úroveň prístupu.

Táto metóda má nasledujúce nuansy:

  • Overenie certifikátu nastáva po požiadavke na zdroj proxy, teda po podaní žiadosti. To znamená, že proxy najprv načíta a potom odreže požiadavku na chránenú službu. To je zlé, ale nie kritické;
  • V protokole http2. Je stále v koncepte a výrobcovia prehliadačov nevedia, ako ho implementovať #info about tls1.3 http2 post handshake (teraz nefunguje) Implementujte RFC 8740 "Používanie TLS 1.3 s HTTP/2";
  • Nie je jasné, ako toto spracovanie zjednotiť.

b) Na základnej úrovni povoliť ssl bez certifikátu.

SSLVerifyClient vyžaduje => SSLVerifyClient voliteľné, ale to znižuje úroveň zabezpečenia proxy servera, pretože takéto pripojenie bude spracované bez certifikátu. Môžete však ďalej odmietnuť prístup k službám proxy pomocou nasledujúcej smernice:

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"

Podrobnejšie informácie nájdete v článku o ssl: Autentifikácia klientskeho certifikátu servera Apache

Obe možnosti boli testované, možnosť „b“ bola zvolená pre jej všestrannosť a kompatibilitu s protokolom http2.

Na dokončenie overenia tejto hypotézy bolo potrebných veľa experimentov s konfiguráciou; testovali sa tieto návrhy:

if = vyžadovať = prepísať

Výsledkom je nasledujúci základný dizajn:

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>

Berúc do úvahy existujúcu autorizáciu vlastníkom certifikátu, ale s chýbajúcim certifikátom, musel som pridať neexistujúceho vlastníka certifikátu v tvare jednej z dostupných premenných SSl_PROTOCOL (namiesto SSL_CLIENT_S_DN_CN), viac podrobností v dokumentácii:

Modul Apache mod_ssl

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

2. V prípade websocketov môžete vytvoriť jedinečné, bezpečné a chránené pripojenie pomocou dočasných relácií, ktoré sa generujú počas normálnej (nie websocket) požiadavky prehliadača.

Na základe predchádzajúcich skúseností musíte do konfigurácie pridať dodatočnú sekciu, aby ste mohli pripraviť dočasné tokeny pre pripojenia webového soketu počas bežnej požiadavky (nie webový soket).

#подготовка передача себе С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>

Testovanie ukázalo, že to funguje. Cookies je možné preniesť na seba prostredníctvom prehliadača používateľa.

3. Dočasné relácie je možné realizovať pomocou jedného proxy webového servera (len vstavané moduly a funkcie).

Ako sme už zistili, Apache má pomerne veľa základných funkcií, ktoré vám umožňujú vytvárať podmienené konštrukcie. Potrebujeme však prostriedky na ochranu našich informácií, kým sú v prehliadači používateľa, a preto určíme, čo a prečo ukladať a aké vstavané funkcie budeme používať:

  • Potrebujeme token, ktorý sa nedá ľahko dekódovať.
  • Potrebujeme token, ktorý má v sebe zabudovanú zastaranosť a schopnosť kontrolovať zastaranie na serveri.
  • Potrebujeme token, ktorý bude spojený s vlastníkom certifikátu.

Vyžaduje to hašovaciu funkciu, soľ a dátum starnutia tokenu. Na základe dokumentácie Výrazy na serveri Apache HTTP Server máme všetko po vybalení sha1 a %{TIME}.

Výsledkom bol tento dizajn:

#нет сертификата, и обращение к 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>

Cieľ bol dosiahnutý, ale vyskytli sa problémy so zastaranosťou servera (môžete použiť rok starý súbor cookie), čo znamená, že tokeny, hoci sú bezpečné pre interné použitie, nie sú bezpečné pre priemyselné (masové) použitie.

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

4. Dočasné tokeny relácie už boli implementované ako hotové moduly Apache.

Z predchádzajúcej iterácie zostal jeden významný problém - neschopnosť kontrolovať starnutie tokenov.

Hľadáme hotový modul, ktorý to robí podľa slov: apache token json two factor auth

Áno, existujú hotové moduly, ale všetky sú viazané na konkrétne akcie a majú artefakty vo forme spustenia relácie a ďalších súborov cookie. Teda na chvíľu nie.
Hľadanie nám trvalo päť hodín, čo neprinieslo konkrétny výsledok.

5. Dočasné tokeny relácie možno implementovať logickým návrhom štruktúry interakcií.

Hotové moduly sú príliš zložité, pretože potrebujeme len niekoľko funkcií.

Ako už bolo povedané, problém s dátumom je v tom, že vstavané funkcie Apache neumožňujú generovanie dátumu z budúcnosti a pri kontrole zastarania v zabudovaných funkciách neexistuje žiadne matematické sčítanie/odčítanie.

To znamená, že nemôžete písať:

(%{env:zt-cert-date} + 30) > %{DATE}

Môžete porovnávať iba dve čísla.

Pri hľadaní riešenia problému Safari som našiel zaujímavý článok: Zabezpečenie HomeAssistant pomocou klientskych certifikátov (funguje so Safari/iOS)
Opisuje príklad kódu v Lua pre Nginx, ktorý, ako sa ukázalo, veľmi opakuje logiku tej časti konfigurácie, ktorú sme už implementovali, s výnimkou použitia metódy hmac salting na hashovanie ( toto sa v Apache nenašlo).

Ukázalo sa, že Lua je jazyk s jasnou logikou a pre Apache je možné urobiť niečo jednoduché:

Po preštudovaní rozdielu s Nginx a Apache:

A dostupné funkcie od výrobcu jazyka Lua:
22.1 – Dátum a čas

Našli sme spôsob, ako nastaviť premenné env v malom súbore Lua, aby sme mohli nastaviť dátum z budúcnosti na porovnanie s aktuálnym.

Takto vyzerá jednoduchý Lua skript:

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

A takto to celé funguje celkovo, s optimalizáciou počtu súborov cookie a výmenou tokenu, keď uplynie polovica času pred vypršaním platnosti starého súboru cookie (tokenu):

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 

Pretože LuaHookAccessChecker bude aktivovaný až po kontrole prístupu na základe týchto informácií od Nginx.

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

Odkaz na zdroj obraz.

Ďalším bodom.

Vo všeobecnosti je jedno, v akom poradí sú direktívy napísané v konfigurácii Apache (pravdepodobne aj Nginx), keďže nakoniec sa všetko zoradí na základe poradia požiadavky od používateľa, čo zodpovedá schéme spracovania Lua skripty.

Dokončenie:

Viditeľný stav po implementácii (cieľ):
správa služieb a infraštruktúry je dostupná z mobilného telefónu na IOS bez dodatočných programov (VPN), jednotná a bezpečná.

Cieľ bol dosiahnutý, webové zásuvky fungujú a majú úroveň zabezpečenia nie menšiu ako certifikát.

Ako sme v ZeroTech prepojili Apple Safari a klientske certifikáty s websocketmi

Zdroj: hab.com

Pridať komentár