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

Članak će biti koristan onima koji:

  • zna što je Client Cert i razumije zašto su mu potrebni websocketi na mobilnom Safariju;
  • Želio bih objaviti web usluge ograničenom krugu ljudi ili samo sebi;
  • misli da je sve već netko napravio i htio bi svijet učiniti malo ugodnijim i sigurnijim.

Povijest websocketa započela je prije otprilike 8 godina. Ranije su se metode koristile u obliku dugih http zahtjeva (zapravo odgovora): korisnikov preglednik šalje zahtjev poslužitelju i čeka da on nešto odgovori, nakon odgovora se ponovno spaja i čeka. Ali onda su se pojavili websocketi.

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

Prije nekoliko godina razvili smo vlastitu implementaciju u čistom PHP-u, koji ne može koristiti https zahtjeve, budući da je ovo sloj veze. Nedavno su gotovo svi web-poslužitelji naučili proxy zahtjeve preko https-a i podržavali connection:upgrade.

Kada se to dogodilo, websockets je postao gotovo zadana usluga za SPA aplikacije, jer kako je zgodno pružiti sadržaj korisniku na inicijativu poslužitelja (prenijeti poruku drugog korisnika ili preuzeti novu verziju slike, dokumenta, prezentacije) koju netko drugi trenutačno uređuje) .

Iako Client Certificate postoji već duže vrijeme, još uvijek je slabo podržan jer stvara mnogo problema kada ga se pokušava zaobići. I (možda :slightly_smiling_face: ) zato ga IOS preglednici (svi osim Safarija) ne žele koristiti i traže ga od lokalnog spremišta certifikata. Certifikati imaju mnoge prednosti u usporedbi s login/pass ili ssh ključevima ili zatvaranjem potrebnih portova kroz vatrozid. Ali nije riječ o tome.

Na iOS-u je postupak instaliranja certifikata prilično jednostavan (nije bez specifičnosti), ali općenito se radi prema uputama kojih ima jako puno na internetu i koje su dostupne samo za preglednik Safari. Nažalost, Safari ne zna koristiti Client Sert za web sockete, no na internetu postoji mnogo uputa kako napraviti takav certifikat, no u praksi je to nedostižno.

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

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

Problem: ne postoji podrška za web utičnice kada proxy zahtjevi prema resursima koji su zaštićeni certifikatom klijenta na mobilnom pregledniku Safari za IOS i drugim aplikacijama koje imaju omogućenu podršku za certifikate.

Hipoteze:

  1. Moguće je konfigurirati takvu iznimku za korištenje certifikata (znajući da ih neće biti) za websockete unutarnjih/vanjskih proxy resursa.
  2. Za websockete možete uspostaviti jedinstvenu, sigurnu i branjivu vezu koristeći privremene sesije koje se generiraju tijekom uobičajenog (ne-websocket) zahtjeva preglednika.
  3. Privremene sesije mogu se implementirati pomoću jednog proxy web poslužitelja (samo ugrađeni moduli i funkcije).
  4. Privremeni tokeni sesije već su implementirani kao gotovi Apache moduli.
  5. Privremeni tokeni sesije mogu se implementirati logičkim projektiranjem strukture interakcije.

Vidljivo stanje nakon implementacije.

Cilj: upravljanje uslugama i infrastrukturom treba biti dostupno s mobilnog telefona na iOS-u bez dodatnih programa (kao što je VPN), unificirano i sigurno.

Dodatni cilj: ušteda vremena i resursa/telefonskog prometa (neke usluge bez web socketa generiraju 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 razvojne programere:

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

Testiranje hipoteze:

1. Moguće je konfigurirati takvu iznimku za korištenje certifikata (znajući da ih neće biti) za web utičnice unutarnjih/vanjskih proxy resursa.

Ovdje su pronađena 2 rješenja:

a) Na razini

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

promijeniti razinu pristupa.

Ova metoda ima sljedeće nijanse:

  • Provjera certifikata događa se nakon zahtjeva za proxy resurs, to jest, rukovanje nakon zahtjeva. To znači da će proxy prvo učitati, a zatim prekinuti zahtjev prema zaštićenoj usluzi. Ovo je loše, ali nije kritično;
  • U http2 protokolu. Još uvijek je u nacrtu, a proizvođači preglednika ne znaju kako ga implementirati #info o tls1.3 http2 post rukovanju (sada ne radi) Implementirajte RFC 8740 "Korištenje TLS-a 1.3 s HTTP/2";
  • Nije jasno kako objediniti ovu obradu.

b) Na osnovnoj razini dopustite ssl bez certifikata.

SSLVerifyClient zahtijeva => SSLVerifyClient izborno, ali to smanjuje razinu sigurnosti proxy poslužitelja, jer će takva veza biti obrađena bez certifikata. Međutim, možete dodatno zabraniti pristup proxy uslugama 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: Autentifikacija klijentskog certifikata poslužitelja Apache

Obje opcije su testirane, opcija "b" je odabrana zbog svoje svestranosti i kompatibilnosti s http2 protokolom.

Za dovršetak provjere ove hipoteze bilo je potrebno mnogo eksperimenata s konfiguracijom; testirani su sljedeći dizajni:

ako = zahtijevati = 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ću autorizaciju 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 ZeroTechu povezali Apple Safari i klijentske certifikate s websocketima

2. Za websockete možete uspostaviti jedinstvenu, sigurnu i zaštićenu vezu koristeći privremene sesije koje se generiraju tijekom uobičajenog (ne-websocket) zahtjeva preglednika.

Na temelju prethodnog iskustva, trebate dodati dodatni odjeljak u konfiguraciju kako biste pripremili privremene tokene za web socket veze tijekom redovnog zahtjeva (ne-web socket).

#подготовка передача себе С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. Kolačiće je moguće prenijeti sebi putem preglednika korisnika.

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

Kao što smo ranije saznali, Apache ima dosta osnovnih funkcija koje vam omogućuju stvaranje uvjetnih konstrukcija. Međutim, potrebna su nam sredstva za zaštitu naših informacija dok su u pregledniku korisnika, tako da određujemo što ćemo pohraniti i zašto te koje ćemo ugrađene funkcije koristiti:

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

To zahtijeva funkciju raspršivanja, sol i datum za starenje tokena. Na temelju dokumentacije Izrazi u Apache HTTP poslužitelju imamo sve izvan 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 poslužitelja (možete koristiti kolačić star godinu dana), što znači da tokeni, iako sigurni za internu upotrebu, nisu sigurni za industrijsku (masovnu) upotrebu.

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

4. Privremeni tokeni sesije već su 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 akcije i imaju artefakte u obliku pokretanja sesije i dodatnih kolačića. Odnosno, ne za neko vrijeme.
Tragala nam je pet sati, koja nije dala konkretan rezultat.

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

Gotovi moduli su presloženi, jer nam treba samo nekoliko funkcija.

Uz to, problem s datumom je taj što Apacheove ugrađene funkcije ne dopuštaju generiranje datuma iz budućnosti, a nema matematičkog zbrajanja/oduzimanja u ugrađenim funkcijama prilikom provjere zastarjelosti.

Odnosno, ne možete napisati:

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

Možete usporediti samo dva broja.

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

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

Proučivši razliku između Nginxa i Apachea:

I dostupne funkcije 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 usporedbu s 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

A ovako to sve ukupno funkcionira, uz optimizaciju broja kolačića i zamjenu tokena kada prođe pola vremena prije isteka starog kolačića (tokena):

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 

Budući da će se LuaHookAccessChecker aktivirati tek nakon provjere pristupa na temelju ovih informacija iz Nginxa.

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

Link na izvor Slika.

Još jedna točka.

Općenito, nije važno kojim su redoslijedom direktive napisane u Apache (vjerojatno i Nginx) konfiguraciji, budući da će na kraju sve biti poredano na temelju redoslijeda zahtjeva korisnika, koji odgovara shemi za obradu Lua skripte.

Završetak:

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

Cilj je postignut, web utičnice rade i imaju razinu sigurnosti ništa manju od certifikata.

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

Izvor: www.habr.com

Dodajte komentar