Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

Članak će biti koristan onima koji:

  • zna šta je Client Cert i razumije zašto su mu potrebni websockets na mobilnom Safariju;
  • Želio bih objavljivati ​​web servise ograničenom krugu ljudi ili samo sebi;
  • misli da je sve već neko uradio, i želi da učini svijet malo praktičnijim i sigurnijim.

Istorija websocketa počela je prije otprilike 8 godina. Ranije su se metode koristile u obliku dugih http zahtjeva (zapravo odgovora): korisnikov pretraživač je poslao zahtjev serveru i čekao da nešto odgovori, nakon odgovora ponovo se povezao i čekao. Ali onda su se pojavili websockets.

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

Prije nekoliko godina razvili smo vlastitu implementaciju u čistom PHP-u, koja ne može koristiti https zahtjeve, jer je ovo sloj veze. Ne tako davno, skoro svi web serveri su naučili da proxy zahtjeve preko https i podržavaju connection:upgrade.

Kada se to dogodilo, websocketi su postali gotovo zadana usluga za SPA aplikacije, jer koliko je zgodno pružiti sadržaj korisniku na inicijativu servera (prenijeti poruku drugog korisnika ili preuzeti novu verziju slike, dokumenta, prezentacije da neko drugi trenutno uređuje) .

Iako Client Certificate postoji već duže vrijeme, i dalje je slabo podržan, jer stvara mnogo problema kada se pokušava zaobići. I (možda :slightly_smiling_face: ) zato IOS pretraživači (svi osim Safarija) ne žele da ga koriste i traže od lokalnog skladišta certifikata. Certifikati imaju mnoge prednosti u odnosu na login/pass ili ssh ključeve ili zatvaranje potrebnih portova kroz firewall. Ali ne radi se o tome.

Na iOS-u je postupak instaliranja certifikata prilično jednostavan (ne bez specifičnosti), ali općenito se radi prema uputama kojih ima dosta na internetu i koje su dostupne samo za Safari pretraživač. Nažalost, Safari ne zna kako da koristi Client Sert za web sokete, ali na internetu postoji mnogo uputstava kako napraviti takav certifikat, ali u praksi je to nedostižno.

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

Da bismo razumjeli websockets, koristili smo sljedeći plan: problem/hipoteza/rješenje.

Problem: nema podrške za web utičnice prilikom proxyja zahtjeva prema resursima koji su zaštićeni certifikatom klijenta na Safari mobilnom pretraživaču za IOS i drugim aplikacijama koje su omogućile podršku za certifikate.

hipoteze:

  1. Moguće je konfigurirati takav izuzetak da koristi certifikate (znajući da ih neće biti) za websockete internih/eksternih proksi resursa.
  2. Za websocket, možete napraviti jedinstvenu, sigurnu i odbranjivu vezu koristeći privremene sesije koje se generiraju tokom normalnog (ne-websocket) zahtjeva pretraživača.
  3. Privremene sesije se mogu implementirati pomoću jednog proxy web servera (samo ugrađeni moduli i funkcije).
  4. Privremeni tokeni sesije su već implementirani kao gotovi Apache moduli.
  5. Privremeni tokeni sesije mogu se implementirati logičkim dizajniranjem strukture interakcije.

Vidljivo stanje nakon implementacije.

Cilj rada: upravljanje uslugama i infrastrukturom treba da bude dostupno sa mobilnog telefona na IOS-u bez dodatnih programa (kao što je VPN), ujedinjeno i sigurno.

Dodatni cilj: ušteda vremena i resursa/telefonskog saobraćaja (neki servisi bez web utičnica stvaraju nepotrebne zahtjeve) uz bržu isporuku sadržaja na mobilnom internetu.

Kako provjeriti?

1. Otvaranje stranica:

— например, 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. Ili u konzoli za programere:

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

Testiranje hipoteza:

1. Moguće je konfigurirati takav izuzetak da koristi certifikate (znajući da ih neće biti) za web utičnice internih/eksternih proksi resursa.

Ovdje su pronađena 2 rješenja:

a) Na nivou

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

promenite nivo pristupa.

Ova metoda ima sljedeće nijanse:

  • Provjera certifikata se događa nakon zahtjeva upućenog proksi resursu, odnosno rukovanja nakon zahtjeva. To znači da će proxy prvo učitati, a zatim prekinuti zahtjev zaštićenoj usluzi. Ovo je loše, ali nije kritično;
  • U http2 protokolu. Još uvijek je u nacrtu, a proizvođači pretraživača ne znaju kako ga implementirati #info o tls1.3 http2 nakon rukovanja (sada ne radi) Implementirajte RFC 8740 "Upotreba TLS 1.3 sa HTTP/2";
  • Nije jasno kako objediniti ovu obradu.

b) Na osnovnom nivou, dozvoli ssl bez sertifikata.

SSLVerifyClient zahtijeva => SSLVerifyClient opciono, ali ovo smanjuje nivo sigurnosti proxy servera, jer će takva veza biti obrađena bez certifikata. Međutim, možete dalje zabraniti pristup proxy uslugama sa sljedećom direktivom:

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"

Detaljnije informacije možete pronaći u članku o ssl-u: Apache Server Client Certificate Autentifikacija

Obje opcije su testirane, opcija “b” je odabrana zbog svoje svestranosti i kompatibilnosti sa http2 protokolom.

Da bi se završila verifikacija ove hipoteze, bilo je potrebno mnogo eksperimenata sa konfiguracijom, testirani su sledeći dizajni:

if = zahtijeva = prepisati

Rezultat je sljedeći osnovni 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>

Uzimajući u obzir postojeće ovlaštenje od strane vlasnika certifikata, ali uz certifikat koji nedostaje, morao sam dodati nepostojećeg vlasnika certifikata u obliku jedne od dostupnih varijabli SSl_PROTOCOL (umjesto SSL_CLIENT_S_DN_CN), više detalja u dokumentaciji:

Apache modul mod_ssl

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

2. Za websocket, možete napraviti jedinstvenu, sigurnu i zaštićenu vezu koristeći privremene sesije koje se generišu tokom normalnog (ne-websocket) zahtjeva pretraživača.

Na osnovu prethodnog iskustva, potrebno je da dodate dodatni odjeljak u konfiguraciju kako biste pripremili privremene tokene za web socket veze tokom redovnog (ne-web socket) zahtjeva.

#подготовка передача себе С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 radi. Moguće je prenijeti kolačiće na sebe putem pretraživača korisnika.

3. Privremene sesije se mogu implementirati pomoću jednog proxy web servera (samo ugrađeni moduli i funkcije).

Kao što smo ranije saznali, Apache ima dosta osnovnih funkcionalnosti koje vam omogućavaju da kreirate uslovne konstrukcije. Međutim, potrebna su nam sredstva za zaštitu naših podataka dok su u korisnikovom pregledniku, tako da utvrđujemo što ćemo pohraniti i zašto, te koje ugrađene funkcije ćemo koristiti:

  • Potreban nam je token koji se ne može lako dekodirati.
  • Potreban nam je token koji ima ugrađenu zastarjelost i mogućnost provjere zastarjelosti na serveru.
  • Potreban nam je token koji će biti povezan s vlasnikom certifikata.

Ovo zahtijeva funkciju heširanja, sol i datum za starost tokena. Na osnovu dokumentacije Izrazi u Apache HTTP serveru imamo sve iz kutije sha1 i %{TIME}.

Rezultat je bio ovaj 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 postignut, ali postoje problemi sa zastarjelošću servera (možete koristiti Cookie star godinu dana), što znači da su tokeni, iako sigurni za internu upotrebu, nesigurni za industrijsku (masovnu) upotrebu.

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

4. Privremeni tokeni sesije su već implementirani kao gotovi Apache moduli.

Jedan značajan problem ostao je iz prethodne iteracije - nemogućnost kontrole starenja tokena.

Tražimo gotov modul koji to radi, prema riječima: apache token json two factor auth

Da, postoje gotovi moduli, ali svi su vezani za određene radnje i imaju artefakte u vidu pokretanja sesije i dodatnih kolačića. Odnosno, ne neko vreme.
Trajalo nam je pet sati, što nije dalo konkretan rezultat.

5. Privremeni tokeni sesije mogu se implementirati logičkim dizajniranjem strukture interakcija.

Gotovi moduli su previše složeni, jer nam treba samo nekoliko funkcija.

S obzirom na to, problem sa datumom je taj što ugrađene funkcije Apachea ne dozvoljavaju generiranje datuma iz budućnosti i nema matematičkog sabiranja/oduzimanja u ugrađenim funkcijama prilikom provjere zastarjelosti.

Odnosno, ne možete napisati:

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

Možete uporediti samo dva broja.

Dok sam tražio rješenje za Safari problem, pronašao sam zanimljiv članak: Osiguravanje HomeAssistant-a s klijentskim certifikatima (radi sa Safari/iOS)
Opisuje primjer koda u Lua za Nginx, koji, kako se pokazalo, uvelike ponavlja logiku onog dijela konfiguracije koji smo već implementirali, s izuzetkom upotrebe hmac salting metode za heširanje ( ovo nije pronađeno u Apache).

Postalo je jasno da je Lua jezik sa jasnom logikom i moguće je učiniti nešto jednostavno za Apache:

Proučavajući razliku između Nginxa i Apachea:

I dostupne funkcije od proizvođača jezika Lua:
22.1 – Datum i vrijeme

Pronašli smo način da postavimo env varijable u maloj Lua datoteci kako bismo postavili datum iz budućnosti za upoređivanje sa trenutnim.

Ovako izgleda jednostavna Lua skripta:

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

I ovako sve funkcionira ukupno, uz optimizaciju broja kolačića i zamjenu tokena kada dođe pola vremena prije nego što stari kolačić (token) istekne:

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 

Zato što će LuaHookAccessChecker biti aktiviran samo nakon provjere pristupa na osnovu ovih informacija iz Nginxa.

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

Link do izvora Slike.

Još jedna stvar.

Općenito, nije bitno kojim redoslijedom su direktive napisane u Apache (vjerovatno i Nginx) konfiguraciji, jer će na kraju sve biti sortirano na osnovu redoslijeda zahtjeva od korisnika, koji odgovara šemi za obradu Lua skripte.

Završetak:

Vidljivo stanje nakon implementacije (cilj):
upravljanje uslugama i infrastrukturom dostupno je sa mobilnog telefona na iOS-u bez dodatnih programa (VPN), objedinjeno i sigurno.

Cilj je postignut, web soketi rade i imaju nivo sigurnosti ne manji od sertifikata.

Kako smo mi u ZeroTech-u povezali Apple Safari i klijentske certifikate s websocketima

izvor: www.habr.com

Dodajte komentar