Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

Artiklen vil være nyttig for dem, der:

  • ved, hvad Client Cert er og forstår, hvorfor det har brug for websockets på mobil Safari;
  • Jeg vil gerne udgive webtjenester til en begrænset kreds af mennesker eller kun til mig selv;
  • mener, at alt allerede er gjort af nogen, og vil gerne gøre verden lidt mere bekvem og mere sikker.

Websockets historie begyndte for omkring 8 år siden. Tidligere blev metoder brugt i form af lange http-anmodninger (faktisk svar): Brugerens browser sendte en anmodning til serveren og ventede på, at den svarede noget, efter svaret tilsluttede den sig igen og ventede. Men så dukkede websockets op.

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

For nogle år siden udviklede vi vores egen implementering i ren PHP, som ikke kan bruge https requests, da dette er linklaget. For ikke længe siden lærte næsten alle webservere at proxy-anmodninger over https og understøtte forbindelse:opgradering.

Da dette skete, blev websockets næsten standardtjenesten for SPA-applikationer, for hvor praktisk er det at levere indhold til brugeren på initiativ af serveren (send en besked fra en anden bruger eller download en ny version af et billede, dokument, præsentation som en anden er i gang med at redigere).

Selvom klientcertifikat har eksisteret i et stykke tid, er det stadig dårligt understøttet, da det skaber en masse problemer, når man forsøger at omgå det. Og (muligvis :slightly_smiling_face: ) det er derfor, IOS-browsere (alle undtagen Safari) ikke ønsker at bruge det og anmode om det fra det lokale certifikatlager. Certifikater har mange fordele sammenlignet med login/pass eller ssh nøgler eller lukning af de nødvendige porte gennem en firewall. Men det er ikke det, det handler om.

På iOS er proceduren for installation af et certifikat ret enkel (ikke uden detaljer), men generelt udføres det i henhold til instruktioner, som der er mange af på internettet, og som kun er tilgængelige for Safari-browseren. Desværre ved Safari ikke, hvordan man bruger Client Сert til web-sockets, men der er mange instruktioner på internettet om, hvordan man opretter et sådant certifikat, men i praksis er dette uopnåeligt.

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

For at forstå websockets brugte vi følgende plan: problem/hypotese/løsning.

problem: der er ingen understøttelse af web-sockets ved proxy-forespørgsler til ressourcer, der er beskyttet af et klientcertifikat på Safari-mobilbrowseren til IOS og andre applikationer, der har aktiveret certifikatunderstøttelse.

Hypoteser:

  1. Det er muligt at konfigurere en sådan undtagelse til at bruge certifikater (velvidende at der ikke vil være nogen) til websockets af interne/eksterne proxy-ressourcer.
  2. For websockets kan du oprette en unik, sikker og forsvarlig forbindelse ved hjælp af midlertidige sessioner, der genereres under en normal (ikke-websocket) browseranmodning.
  3. Midlertidige sessioner kan implementeres ved hjælp af én proxy-webserver (kun indbyggede moduler og funktioner).
  4. Midlertidige sessionstokens er allerede blevet implementeret som færdige Apache-moduler.
  5. Midlertidige sessionstokens kan implementeres ved logisk at designe interaktionsstrukturen.

Synlig tilstand efter implementering.

Objektiv: administration af tjenester og infrastruktur bør være tilgængelig fra en mobiltelefon på IOS uden yderligere programmer (såsom VPN), samlet og sikkert.

Yderligere mål: sparer tid og ressourcer/telefontrafik (nogle tjenester uden web-sockets genererer unødvendige anmodninger) med hurtigere levering af indhold på det mobile internet.

Hvordan man tjekker

1. Åbningssider:

— например, 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. Eller i udviklerkonsollen:

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

Hypotese testning:

1. Det er muligt at konfigurere en sådan undtagelse til at bruge certifikater (velvidende at der ikke vil være nogen) til web-sockets af interne/eksterne proxy-ressourcer.

2 løsninger blev fundet her:

a) På niveau

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

ændre adgangsniveau.

Denne metode har følgende nuancer:

  • Certifikatbekræftelse sker efter en anmodning til den proxy-ressource, det vil sige håndtryk efter anmodning. Det betyder, at proxyen først vil indlæse og derefter afbryde anmodningen til den beskyttede tjeneste. Dette er dårligt, men ikke kritisk;
  • I http2-protokollen. Den er stadig under udkast, og browserproducenterne ved ikke, hvordan de skal implementere den #info om tls1.3 http2 post-håndtryk (fungerer ikke nu) Implementer RFC 8740 "Brug af TLS 1.3 med HTTP/2";
  • Det er ikke klart, hvordan man forener denne behandling.

b) Tillad ssl på et grundlæggende niveau uden certifikat.

SSLVerifyClient require => SSLVerifyClient valgfri, men dette reducerer sikkerhedsniveauet for proxyserveren, da en sådan forbindelse vil blive behandlet uden et certifikat. Du kan dog yderligere nægte adgang til proxytjenester med følgende direktiv:

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"

Mere detaljeret information kan findes i artiklen om ssl: Apache Server Client Certificate Authentication

Begge muligheder blev testet, mulighed "b" blev valgt på grund af dens alsidighed og kompatibilitet med http2-protokollen.

For at fuldføre verifikationen af ​​denne hypotese krævede det en masse eksperimenter med konfigurationen; følgende design blev testet:

hvis = kræver = omskriv

Resultatet er følgende grundlæggende design:

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>

Under hensyntagen til den eksisterende autorisation fra certifikatejeren, men med et manglende certifikat, var jeg nødt til at tilføje en ikke-eksisterende certifikatejer i form af en af ​​de tilgængelige variabler SSl_PROTOCOL (i stedet for SSL_CLIENT_S_DN_CN), flere detaljer i dokumentationen:

Apache-modul mod_ssl

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

2. For websockets kan du oprette en unik, sikker og beskyttet forbindelse ved hjælp af midlertidige sessioner, der genereres under en normal (ikke-websocket) browseranmodning.

Baseret på tidligere erfaringer skal du tilføje et ekstra afsnit til konfigurationen for at forberede midlertidige tokens til web-socket-forbindelser under en almindelig (ikke-web-socket) anmodning.

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

Test viste, at det virker. Det er muligt at overføre cookies til dig selv via brugerens browser.

3. Midlertidige sessioner kan implementeres ved hjælp af én proxy-webserver (kun indbyggede moduler og funktioner).

Som vi fandt ud af tidligere, har Apache en hel del kernefunktionalitet, der giver dig mulighed for at oprette betingede konstruktioner. Vi har dog brug for midler til at beskytte vores oplysninger, mens de er i brugerens browser, så vi fastslår, hvad vi skal gemme og hvorfor, og hvilke indbyggede funktioner vi vil bruge:

  • Vi har brug for et token, der ikke let kan afkodes.
  • Vi har brug for et token, der har forældelse indbygget og mulighed for at kontrollere forældelse på serveren.
  • Vi har brug for et token, der vil være knyttet til ejeren af ​​certifikatet.

Dette kræver en hashing-funktion, et salt og en dato for at ælde tokenet. Baseret på dokumentationen Udtryk i Apache HTTP Server vi har det hele ud af kassen sha1 og %{TIME}.

Resultatet blev dette design:

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

Målet er nået, men der er problemer med serverens forældelse (du kan bruge en år gammel cookie), hvilket betyder, at tokens, selvom de er sikre til intern brug, er usikre til industriel (masse)brug.

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

4. Midlertidige sessionstokens er allerede blevet implementeret som færdige Apache-moduler.

Et væsentligt problem var tilbage fra den tidligere iteration - manglende evne til at kontrollere token-ældning.

Vi leder efter et færdiglavet modul, der gør dette, ifølge ordene: apache token json two factor auth

Ja, der er færdige moduler, men de er alle bundet til specifikke handlinger og har artefakter i form af at starte en session og yderligere cookies. Det vil sige ikke for et stykke tid.
Det tog os fem timer at søge, hvilket ikke gav et konkret resultat.

5. Midlertidige sessionstokens kan implementeres ved logisk at designe strukturen af ​​interaktioner.

Færdige moduler er for komplekse, fordi vi kun har brug for et par funktioner.

Når det så er sagt, er problemet med datoen, at Apaches indbyggede funktioner ikke tillader generering af en dato fra fremtiden, og der er ingen matematisk addition/subtraktion i de indbyggede funktioner, når man tjekker for forældelse.

Det vil sige, du kan ikke skrive:

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

Du kan kun sammenligne to tal.

Mens jeg søgte efter en løsning til Safari-problemet, fandt jeg en interessant artikel: Sikring af HomeAssistant med klientcertifikater (fungerer med Safari/iOS)
Den beskriver et eksempel på kode i Lua for Nginx, og som, som det viste sig, i høj grad gentager logikken i den del af konfigurationen, som vi allerede har implementeret, med undtagelse af brugen af ​​hmac-saltningsmetoden til hashing ( dette blev ikke fundet i Apache).

Det blev klart, at Lua er et sprog med klar logik, og det er muligt at gøre noget simpelt for Apache:

Efter at have studeret forskellen med Nginx og Apache:

Og tilgængelige funktioner fra Lua-sprogproducenten:
22.1 – Dato og tid

Vi fandt en måde at indstille env-variabler i en lille Lua-fil for at sætte en dato fra fremtiden for at sammenligne med den nuværende.

Sådan ser et simpelt Lua-script ud:

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

Og sådan fungerer det i alt, med optimering af antallet af Cookies og udskiftning af token, når halvdelen af ​​tiden kommer, før den gamle Cookie (token) udløber:

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 

Fordi LuaHookAccessChecker kun vil blive aktiveret efter adgangskontrol baseret på disse oplysninger fra Nginx.

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

Link til kilde Billede.

Et andet punkt.

Generelt er det ligegyldigt, i hvilken rækkefølge direktiverne er skrevet i Apache (sandsynligvis også Nginx) konfigurationen, da alt i sidste ende vil blive sorteret baseret på rækkefølgen af ​​anmodningen fra brugeren, som svarer til ordningen for behandling Lua scripts.

Færdiggørelse:

Synlig tilstand efter implementering (mål):
styring af tjenester og infrastruktur er tilgængelig fra en mobiltelefon på IOS uden yderligere programmer (VPN), samlet og sikkert.

Målet er nået, web-sockets virker og har et sikkerhedsniveau, der ikke er mindre end et certifikat.

Hvordan vi hos ZeroTech forbandt Apple Safari og klientcertifikater med websockets

Kilde: www.habr.com

Tilføj en kommentar