Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

Članek bo koristen za tiste, ki:

  • ve, kaj je Client Cert, in razume, zakaj potrebuje spletne vtičnice v mobilnem Safariju;
  • Spletne storitve želim objaviti omejenemu krogu ljudi ali samo sebi;
  • misli, da je nekdo že vse naredil, in bi rad naredil svet malo bolj udoben in varnejši.

Zgodovina spletnih vtičnic se je začela pred približno 8 leti. Prej so se metode uporabljale v obliki dolgih http zahtev (pravzaprav odgovorov): uporabnikov brskalnik je poslal zahtevo strežniku in čakal, da mu ta nekaj odgovori, po odgovoru pa se je znova povezal in čakal. Potem pa so se pojavile spletne vtičnice.

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

Pred nekaj leti smo razvili lastno izvedbo v čistem PHP-ju, ki ne more uporabljati https zahtev, saj je to povezovalna plast. Nedolgo nazaj so se skoraj vsi spletni strežniki naučili posredovati zahteve preko https in podpirati connection:upgrade.

Ko se je to zgodilo, so spletne vtičnice postale skoraj privzeta storitev za aplikacije SPA, ker kako priročno je zagotoviti vsebino uporabniku na pobudo strežnika (prenesti sporočilo drugega uporabnika ali prenesti novo različico slike, dokumenta, predstavitve). ki ga trenutno ureja nekdo drug) .

Čeprav Client Certificate obstaja že kar nekaj časa, še vedno ostaja slabo podprt, saj povzroča veliko težav, ko ga poskušamo obiti. In (mogoče :slightly_smiling_face: ) zato ga IOS brskalniki (vsi razen Safarija) nočejo uporabljati in ga zahtevajo od lokalne shrambe potrdil. Certifikati imajo številne prednosti v primerjavi s ključi za prijavo/geslo ali ssh ali zapiranjem potrebnih vrat prek požarnega zidu. Ampak ne gre za to.

Na iOS-u je postopek namestitve certifikata precej preprost (ne brez posebnosti), na splošno pa poteka po navodilih, ki jih je na spletu ogromno in so na voljo le za brskalnik Safari. Na žalost Safari ne zna uporabljati Client Сert za web sockets, je pa na internetu veliko navodil, kako ustvariti takšen certifikat, vendar je v praksi to nedosegljivo.

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

Za razumevanje spletnih vtičnic smo uporabili naslednji načrt: problem/hipoteza/rešitev.

Težava: ni podpore za spletne vtičnice pri posredovanju zahtevkov virom, ki so zaščiteni s potrdilom odjemalca v mobilnem brskalniku Safari za IOS in drugih aplikacijah, ki imajo omogočeno podporo za potrdila.

Hipoteze:

  1. Takšno izjemo je mogoče konfigurirati za uporabo potrdil (ob zavedanju, da jih ne bo) za spletne vtičnice notranjih/zunanjih posredniških virov.
  2. Za spletne vtičnice lahko vzpostavite edinstveno, varno in obrambno povezavo z uporabo začasnih sej, ki so ustvarjene med običajno (ne-websocket) zahtevo brskalnika.
  3. Začasne seje je mogoče izvajati z uporabo enega proxy spletnega strežnika (samo vgrajeni moduli in funkcije).
  4. Začasni žetoni sej so že implementirani kot že pripravljeni moduli Apache.
  5. Začasne žetone seje je mogoče implementirati z logičnim oblikovanjem interakcijske strukture.

Vidno stanje po izvedbi.

Cilj: upravljanje storitev in infrastrukture naj bo dostopno z mobilnega telefona na IOS brez dodatnih programov (npr. VPN), poenoteno in varno.

Dodatni cilj: prihranek časa in virov/telefonskega prometa (nekatere storitve brez web socketov ustvarjajo nepotrebne zahteve) s hitrejšo dostavo vsebin na mobilnem internetu.

Kako preveriti?

1. Začetne strani:

— например, 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. Ali v konzoli za razvijalce:

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

Preizkušanje hipotez:

1. Takšno izjemo je mogoče konfigurirati za uporabo potrdil (vedoč, da jih ne bo) za spletne vtičnice notranjih/zunanjih posredniških virov.

Tu sta bili najdeni 2 rešitvi:

a) Na ravni

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

spremenite raven dostopa.

Ta metoda ima naslednje nianse:

  • Preverjanje potrdila se izvede po zahtevi posredniškemu viru, to je rokovanju po zahtevi. To pomeni, da bo proxy najprej naložil in nato prekinil zahtevo do zaščitene storitve. To je slabo, vendar ni kritično;
  • V protokolu http2. Še vedno je v osnutku in proizvajalci brskalnikov ne vedo, kako bi ga implementirali #info o tls1.3 http2 post handhake (trenutno ne deluje) Izvedite RFC 8740 "Uporaba TLS 1.3 s HTTP/2";
  • Ni jasno, kako poenotiti to obdelavo.

b) Na osnovni ravni dovolite ssl brez potrdila.

SSLVerifyClient require => SSLVerifyClient neobvezno, vendar to zmanjša raven varnosti strežnika proxy, saj bo taka povezava obdelana brez potrdila. Vendar pa lahko dodatno zavrnete dostop do posredniških storitev z naslednjo direktivo:

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še informacije najdete v članku o ssl: Preverjanje pristnosti potrdila odjemalca strežnika Apache

Obe možnosti sta bili testirani, možnost "b" je bila izbrana zaradi vsestranskosti in združljivosti s protokolom http2.

Za dokončanje preverjanja te hipoteze je bilo potrebnih veliko eksperimentov s konfiguracijo; testirani so bili naslednji modeli:

če = zahtevaj = prepiši

Rezultat je naslednja osnovna zasnova:

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>

Upoštevajoč obstoječo avtorizacijo s strani lastnika certifikata, vendar z manjkajočim certifikatom, sem moral dodati neobstoječega lastnika certifikata v obliki ene od razpoložljivih spremenljivk SSl_PROTOCOL (namesto SSL_CLIENT_S_DN_CN), več podrobnosti v dokumentaciji:

Modul Apache mod_ssl

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

2. Za spletne vtičnice lahko vzpostavite edinstveno, varno in zaščiteno povezavo z uporabo začasnih sej, ki so ustvarjene med običajno (ne-websocket) zahtevo brskalnika.

Glede na prejšnje izkušnje morate konfiguraciji dodati dodaten razdelek, da pripravite začasne žetone za povezave s spletnimi vtičnicami med običajno zahtevo (ne-spletne vtičnice).

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

Testiranje je pokazalo, da deluje. Preko uporabnikovega brskalnika je mogoče piškotke prenesti k sebi.

3. Začasne seje je mogoče izvajati z uporabo enega proxy spletnega strežnika (samo vgrajeni moduli in funkcije).

Kot smo že ugotovili, ima Apache kar veliko osnovnih funkcij, ki vam omogočajo ustvarjanje pogojnih konstruktov. Vendar pa potrebujemo sredstva za zaščito naših podatkov, medtem ko so v uporabnikovem brskalniku, zato določimo, kaj shraniti in zakaj ter katere vgrajene funkcije bomo uporabljali:

  • Potrebujemo žeton, ki ga ni mogoče zlahka dekodirati.
  • Potrebujemo žeton, ki ima vgrajeno zastarelost in možnost preverjanja zastarelosti na strežniku.
  • Potrebujemo žeton, ki bo povezan z lastnikom potrdila.

To zahteva funkcijo zgoščevanja, sol in datum za staranje žetona. Na podlagi dokumentacije Izrazi v strežniku HTTP Apache imamo vse pripravljeno sha1 in %{TIME}.

Rezultat je bil ta 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>

Cilj je dosežen, pojavljajo pa se težave z zastarelostjo strežnika (lahko uporabite leto dni star piškotek), kar pomeni, da žetoni, čeprav varni za interno uporabo, niso varni za industrijsko (množično) uporabo.

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

4. Začasni žetoni sej so že implementirani kot že pripravljeni moduli Apache.

Ena pomembna težava je ostala iz prejšnje ponovitve - nezmožnost nadzora staranja žetona.

Iščemo že pripravljen modul, ki to počne, v skladu z besedami: apache token json two factor auth

Da, obstajajo že pripravljeni moduli, vendar so vsi vezani na določena dejanja in imajo artefakte v obliki zagona seje in dodatnih piškotkov. Se pravi, ne za nekaj časa.
Pet ur je trajalo iskanje, ki pa ni dalo konkretnega rezultata.

5. Začasne žetone seje je mogoče implementirati z logičnim oblikovanjem strukture interakcij.

Že pripravljeni moduli so preveč zapleteni, saj potrebujemo le nekaj funkcij.

Kot rečeno, težava z datumom je v tem, da vgrajene funkcije Apache ne omogočajo generiranja datuma iz prihodnosti in v vgrajenih funkcijah pri preverjanju zastarelosti ni matematičnega seštevanja/odštevanja.

To pomeni, da ne morete napisati:

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

Primerjate lahko samo dve številki.

Med iskanjem rešitve za težavo Safari sem našel zanimiv članek: Zaščita HomeAssistant s certifikati odjemalcev (deluje s Safari/iOS)
Opisuje primer kode v Lua za Nginx in ki, kot se je izkazalo, zelo ponavlja logiko tistega dela konfiguracije, ki smo ga že implementirali, z izjemo uporabe metode soljenja hmac za zgoščevanje ( tega ni bilo mogoče najti v Apache).

Postalo je jasno, da je Lua jezik z jasno logiko in da je za Apache mogoče narediti nekaj preprostega:

Po preučevanju razlike med Nginxom in Apacheom:

In razpoložljive funkcije proizvajalca jezika Lua:
22.1 – Datum in čas

Našli smo način za nastavitev spremenljivk env v majhni datoteki Lua, da bi nastavili datum iz prihodnosti za primerjavo s trenutnim.

Takole izgleda preprost skript 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

In tako vse skupaj deluje z optimizacijo števila piškotkov in zamenjavo žetona, ko pride polovica časa pred iztekom starega piškotka (žetona):

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 

Ker bo LuaHookAccessChecker aktiviran šele po preverjanju dostopa na podlagi teh informacij iz Nginxa.

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

Povezava do vira slika.

Še ena točka.

Na splošno ni pomembno, v kakšnem vrstnem redu so zapisane direktive v konfiguraciji Apache (verjetno tudi Nginx), saj bo na koncu vse razvrščeno glede na vrstni red zahteve uporabnika, ki ustreza shemi za obdelavo Lua skripte.

Dokončanje:

Vidno stanje po izvedbi (cilj):
upravljanje storitev in infrastrukture je na voljo z mobilnega telefona na IOS brez dodatnih programov (VPN), poenoteno in varno.

Cilj je bil dosežen, spletne vtičnice delujejo in imajo raven varnosti nič manj kot certifikat.

Kako smo pri ZeroTechu povezali Apple Safari in odjemalska potrdila s spletnimi vtičnicami

Vir: www.habr.com

Dodaj komentar