Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

La artikolo estos utila al tiuj, kiuj:

  • scias kio estas Kliento Cert kaj komprenas kial ĝi bezonas retsockets sur movebla Safaro;
  • Mi ŝatus publikigi retservojn al limigita rondo de homoj aŭ nur al mi mem;
  • opinias, ke ĉio jam estas farita de iu, kaj ŝatus fari la mondon iom pli oportuna kaj sekura.

La historio de websockets komenciĝis antaŭ ĉirkaŭ 8 jaroj. Antaŭe, metodoj kiel longaj http-petoj (fakte respondoj) estis uzataj: la retumilo de la uzanto sendis peton al la servilo kaj atendis, ke ĝi respondu ion, post la respondo ĝi denove konektiĝis kaj atendis. Sed tiam aperis retejoj.

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

Antaŭ kelkaj jaroj, ni evoluigis nian propran efektivigon en pura PHP, kiu ne povas uzi https-petojn, ĉar ĉi tiu estas la ligotavolo. Antaŭ nelonge, preskaŭ ĉiuj retserviloj lernis prokuri petojn per https kaj subteni konekton:ĝisdatigi.

Kiam tio okazis, websockets iĝis preskaŭ la defaŭlta servo por SPA-aplikoj, ĉar kiom oportune estas provizi enhavon al la uzanto laŭ iniciato de la servilo (sendi mesaĝon de alia uzanto aŭ elŝuti novan version de bildo, dokumento, prezento). ke iu alia nuntempe redaktas).

Kvankam Klienta Atestilo ekzistas de sufiĉe da tempo, ĝi ankoraŭ restas nebone subtenata, ĉar ĝi kreas multajn problemojn kiam oni provas preteriri ĝin. Kaj (eble :slightly_smiling_face: ) tial IOS-retumiloj (ĉiuj krom Safari) ne volas uzi ĝin kaj peti ĝin de la loka atestilbutiko. Atestiloj havas multajn avantaĝojn kompare kun ensalutu/pasi aŭ ssh-ŝlosiloj aŭ fermi la necesajn havenojn per fajroŝirmilo. Sed ne pri tio temas.

En iOS, la proceduro por instali atestilon estas sufiĉe simpla (ne sen detaloj), sed ĝenerale ĝi estas farita laŭ instrukcioj, el kiuj estas multaj en la Interreto kaj kiuj disponeblas nur por la retumilo Safari. Bedaŭrinde, Safaro ne scias kiel uzi Klienton Сert por retaj ingoj, sed ekzistas multaj instrukcioj en la Interreto pri kiel krei tian atestilon, sed praktike tio estas neatingebla.

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

Por kompreni retsockets, ni uzis la sekvan planon: problemo/hipotezo/solvo.

Problemo: ne ekzistas subteno por interretaj ingoj dum prokurado de petoj al resursoj, kiuj estas protektitaj per klientatestilo en la poŝtelefona retumilo Safari por IOS kaj aliaj aplikoj, kiuj ebligis atestilsubtenon.

Hipotezoj:

  1. Eblas agordi tian escepton por uzi atestojn (sciante, ke ne ekzistos) al retaj sockets de internaj/eksteraj prokuritaj rimedoj.
  2. Por retsockets, vi povas fari unikan, sekuran kaj defendeblan konekton uzante provizorajn sesiojn kiuj estas generitaj dum normala (ne-websocket) retumila peto.
  3. Provizoraj sesioj povas esti efektivigitaj per unu prokura retservilo (nur enkonstruitaj moduloj kaj funkcioj).
  4. Provizoraj sesioĵetonoj jam estis efektivigitaj kiel pretaj Apache-moduloj.
  5. Provizoraj sesioĵetonoj povas esti efektivigitaj logike dezajnante la interagan strukturon.

Videbla stato post efektivigo.

Celo de la laboro: administrado de servoj kaj infrastrukturo devus esti alirebla de poŝtelefono en iOS sen aldonaj programoj (kiel VPN), unuigitaj kaj sekuraj.

Kroma celo: ŝparante tempon kaj rimedojn/telefontrafikon (kelkaj servoj sen interretaj ingoj generas nenecesajn petojn) kun pli rapida liverado de enhavo en la movebla Interreto.

Kiel kontroli?

1. Malfermaj paĝoj:

— например, 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. Aŭ en la programista konzolo:

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

Testo de hipotezo:

1. Eblas agordi tian escepton por uzi atestojn (sciante, ke ne estos) al retaj ingoj de internaj/eksteraj prokuritaj rimedoj.

2 solvoj estis trovitaj ĉi tie:

a) Je la nivelo

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

ŝanĝi alirnivelon.

Ĉi tiu metodo havas la jenajn nuancojn:

  • Atestilkonfirmo okazas post peto al la prokurita rimedo, tio estas, post peto manpremo. Ĉi tio signifas, ke la prokurilo unue ŝargos kaj poste forigos la peton al la protektita servo. Ĉi tio estas malbona, sed ne kritika;
  • En la http2 protokolo. Ĝi ankoraŭ estas en malneto, kaj retumiloj ne scias kiel efektivigi ĝin #info pri tls1.3 http2 post manpremo (ne funkcias nun) Efektivigu RFC 8740 "Uzante TLS 1.3 kun HTTP/2";
  • Ne estas klare kiel unuigi ĉi tiun prilaboradon.

b) Je baza nivelo, permesu ssl sen atestilo.

SSLVerifyClient require => SSLVerifyClient nedeviga, sed ĉi tio reduktas la sekurecnivelon de la prokura servilo, ĉar tia konekto estos prilaborita sen atestilo. Tamen, vi povas plu nei aliron al prokuritaj servoj kun la sekva 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"

Pli detalaj informoj troveblas en la artikolo pri ssl: Atestilo de Atestilo de Apache Servilo

Ambaŭ opcioj estis provitaj, opcio "b" estis elektita por ĝia ĉiuflankeco kaj kongruo kun la http2-protokolo.

Por kompletigi la konfirmon de ĉi tiu hipotezo, necesis multaj eksperimentoj kun la agordo; la sekvaj dezajnoj estis testitaj:

se = postuli = reverki

La rezulto estas la sekva baza dezajno:

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>

Konsiderante la ekzistantan rajtigon de la posedanto de la atestilo, sed kun mankanta atestilo, mi devis aldoni neekzistantan posedanton de atestilo en la formo de unu el la disponeblaj variabloj SSl_PROTOCOL (anstataŭ SSL_CLIENT_S_DN_CN), pli da detaloj en la dokumentado:

Apache Modulo mod_ssl

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

2. Por retejsokoj, vi povas fari unikan, sekuran kaj protektitan konekton uzante provizorajn sesiojn kiuj estas generitaj dum normala (ne-websocket) retumila peto.

Surbaze de antaŭa sperto, vi devas aldoni plian sekcion al la agordo por prepari provizorajn ĵetonojn por interretaj ingo-konektoj dum regula (ne-reta ingo) peto.

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

Testado montris, ke ĝi funkcias. Eblas transdoni Kuketojn al vi mem per la retumilo de la uzanto.

3. Provizoraj sesioj povas esti efektivigitaj per unu prokura retservilo (nur enkonstruitaj moduloj kaj funkcioj).

Kiel ni eksciis pli frue, Apache havas sufiĉe multe da kernaj funkcioj, kiuj permesas vin krei kondiĉajn konstrukciojn. Tamen, ni bezonas rimedojn por protekti niajn informojn dum ĝi estas en la retumilo de la uzanto, do ni establas kion stoki kaj kial, kaj kiajn enkonstruitajn funkciojn ni uzos:

  • Ni bezonas ĵetonon, kiu ne povas esti facile deĉifrita.
  • Ni bezonas ĵetonon, kiu havas malnoviĝon enkonstruitan en ĝi kaj la kapablon kontroli malnoviĝon sur la servilo.
  • Ni bezonas ĵetonon, kiu estos asociita kun la posedanto de la atestilo.

Ĉi tio postulas haŝan funkcion, salon kaj daton por maljunigi la ĵetonon. Surbaze de la dokumentado Esprimoj en Apache HTTP-Servilo ni havas ĉion el la skatolo sha1 kaj %{TIME}.

La rezulto estis ĉi tiu dezajno:

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

La celo estis atingita, sed estas problemoj pri malnoviĝo de servilo (vi povas uzi jaraĝan Kuketon), kio signifas, ke la ĵetonoj, kvankam sekuraj por interna uzo, estas nesekuraj por industria (amasa) uzo.

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

4. Provizoraj sesiaj ĵetonoj jam estis efektivigitaj kiel pretaj Apache-moduloj.

Unu grava problemo restis de la antaŭa ripeto - la malkapablo kontroli ĵetonan maljuniĝon.

Ni serĉas pretan modulon, kiu faras ĉi tion, laŭ la vortoj: apache token json two factor auth

Jes, ekzistas pretaj moduloj, sed ili ĉiuj estas ligitaj al specifaj agoj kaj havas artefaktojn en formo de komenco de sesio kaj pliaj Kuketoj. Tio estas, ne por momento.
Ni bezonis kvin horojn por serĉi, kio ne donis konkretan rezulton.

5. Portempaj sesiaj ĵetonoj povas esti efektivigitaj logike dezajnante la strukturon de interagoj.

Pretaj moduloj estas tro kompleksaj, ĉar ni bezonas nur kelkajn funkciojn.

Dirite, la problemo kun la dato estas ke la enkonstruitaj funkcioj de Apache ne permesas generi daton de la estonteco, kaj ekzistas neniu matematika aldono/subtraho en la enkonstruitaj funkcioj kiam kontrolado de malnoviĝo.

Tio estas, vi ne povas skribi:

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

Vi povas nur kompari du nombrojn.

Serĉante solvon por la problemo de Safaro, mi trovis interesan artikolon: Sekurigi HomeAssistant kun klientatestiloj (funkcias kun Safaro/iOS)
Ĝi priskribas ekzemplon de kodo en Lua por Nginx, kaj kiu, kiel montriĝis, tre ripetas la logikon de tiu parto de la agordo, kiun ni jam efektivigis, escepte de la uzo de la hmac-saliga metodo por haĉado ( ĉi tio ne estis trovita en Apache).

Evidentiĝis, ke Lua estas lingvo kun klara logiko, kaj eblas fari ion simplan por Apache:

Studinte la diferencon kun Nginx kaj Apache:

Kaj disponeblaj funkcioj de la fabrikanto de Lua lingvo:
22.1 - Dato kaj Tempo

Ni trovis manieron agordi env-variablojn en malgranda Lua-dosiero por agordi daton de la estonteco por kompari kun la nuna.

Jen kiel aspektas simpla Lua-skripto:

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

Kaj jen kiel ĉio funkcias entute, kun optimumigo de la nombro da Kuketoj kaj anstataŭigo de la ĵetono kiam duono de la tempo venas antaŭ ol la malnova Kuketo (ĵetono) eksvalidiĝas:

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 

Ĉar LuaHookAccessChecker nur estos aktivigita post alirkontroloj bazitaj sur ĉi tiu informo de Nginx.

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

Ligo al fonto bildo.

Ankoraŭ unu afero.

Ĝenerale, ne gravas en kia ordo la direktivoj estas skribitaj en la agordo Apache (verŝajne ankaŭ Nginx), ĉar finfine ĉio estos ordigita laŭ la ordo de la peto de la uzanto, kiu respondas al la skemo por prilaborado. Lua skriptoj.

Kompletigo:

Videbla stato post efektivigo (celo):
administrado de servoj kaj infrastrukturo estas disponebla de poŝtelefono en iOS sen aldonaj programoj (VPN), unuigitaj kaj sekuraj.

La celo estis atingita, retaj ingoj funkcias kaj havas nivelon de sekureco ne malpli ol atestilo.

Kiel ni ĉe ZeroTech kunligis Apple Safari kaj klientajn atestilojn kun retkonektoj

fonto: www.habr.com

Aldoni komenton