Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

Artikkelen vil være nyttig for de som:

  • vet hva Client Cert er og forstår hvorfor det trenger websockets på mobil Safari;
  • Jeg vil gjerne publisere webtjenester til en begrenset krets av mennesker eller bare til meg selv;
  • tror at alt allerede er gjort av noen, og vil gjerne gjøre verden litt mer praktisk og tryggere.

Historien til websockets begynte for omtrent 8 år siden. Tidligere ble metoder brukt i form av lange http-forespørsler (faktisk svar): brukerens nettleser sendte en forespørsel til serveren og ventet på at den skulle svare på noe, etter svaret koblet den seg til igjen og ventet. Men så dukket websockets opp.

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

For noen år siden utviklet vi vår egen implementering i ren PHP, som ikke kan bruke https-forespørsler, siden dette er lenkelaget. For ikke lenge siden lærte nesten alle nettservere å proxy-forespørsler over https og støtte forbindelse:oppgradering.

Da dette skjedde, ble websockets nesten standardtjenesten for SPA-applikasjoner, fordi hvor praktisk det er å gi innhold til brukeren på initiativ fra serveren (sende en melding fra en annen bruker eller last ned en ny versjon av et bilde, dokument, presentasjon som noen andre for øyeblikket redigerer).

Selv om klientsertifikatet har eksistert en stund, er det fortsatt dårlig støttet, da det skaper mange problemer når du prøver å omgå det. Og (muligens :slightly_smiling_face: ) det er grunnen til at IOS-nettlesere (alle unntatt Safari) ikke vil bruke det og be om det fra det lokale sertifikatlageret. Sertifikater har mange fordeler sammenlignet med login/pass eller ssh-nøkler eller lukking av nødvendige porter gjennom en brannmur. Men det er ikke det dette handler om.

På iOS er prosedyren for å installere et sertifikat ganske enkel (ikke uten spesifikasjoner), men generelt gjøres det i henhold til instruksjoner, som det er mange av på Internett og som kun er tilgjengelige for Safari-nettleseren. Dessverre vet ikke Safari hvordan man bruker Client Сert for web-sockets, men det er mange instruksjoner på Internett om hvordan man lager et slikt sertifikat, men i praksis er dette uoppnåelig.

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

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

problem: det er ingen støtte for web-sockets ved proxy-forespørsler til ressurser som er beskyttet av et klientsertifikat i Safari-mobilnettleseren for IOS og andre applikasjoner som har aktivert sertifikatstøtte.

Hypoteser:

  1. Det er mulig å konfigurere et slikt unntak til å bruke sertifikater (velvitende at det ikke vil være noen) til websockets av interne/eksterne proxy-ressurser.
  2. For websockets kan du opprette en unik, sikker og forsvarlig tilkobling ved å bruke midlertidige økter som genereres under en normal (ikke-websocket) nettleserforespørsel.
  3. Midlertidige økter kan implementeres ved hjelp av én proxy-webserver (kun innebygde moduler og funksjoner).
  4. Midlertidige økttokens er allerede implementert som ferdige Apache-moduler.
  5. Midlertidige økttokens kan implementeres ved å logisk utforme interaksjonsstrukturen.

Synlig tilstand etter implementering.

Objektiv: administrasjon av tjenester og infrastruktur bør være tilgjengelig fra en mobiltelefon på IOS uten tilleggsprogrammer (som VPN), enhetlig og sikker.

Ekstra mål: sparer tid og ressurser/telefontrafikk (noen tjenester uten web-sockets genererer unødvendige forespørsler) med raskere levering av innhold på mobilt internett.

Hvordan sjekke?

1. Åpningssider:

— например, 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 utviklerkonsollen:

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

Hypotesetesting:

1. Det er mulig å konfigurere et slikt unntak til å bruke sertifikater (velvitende at det ikke vil være noen) til web-sockets av interne/eksterne proxy-ressurser.

2 løsninger ble funnet her:

a) På nivået

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

endre tilgangsnivå.

Denne metoden har følgende nyanser:

  • Sertifikatverifisering skjer etter en forespørsel til proxy-ressursen, det vil si håndtrykk etter forespørsel. Dette betyr at proxyen først vil laste inn og deretter avbryte forespørselen til den beskyttede tjenesten. Dette er dårlig, men ikke kritisk;
  • I http2-protokollen. Den er fortsatt i utkast, og nettleserprodusentene vet ikke hvordan de skal implementere den #info om tls1.3 http2 post-håndtrykk (fungerer ikke nå) Implementer RFC 8740 "Bruke TLS 1.3 med HTTP/2";
  • Det er ikke klart hvordan denne behandlingen skal forenes.

b) På et grunnleggende nivå, tillat ssl uten sertifikat.

SSLVerifyClient require => SSLVerifyClient valgfritt, men dette reduserer sikkerhetsnivået til proxy-serveren, siden en slik tilkobling vil bli behandlet uten sertifikat. Du kan imidlertid nekte tilgang til proxy-tjenester ytterligere 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"

Mer detaljert informasjon finner du i artikkelen om ssl: Apache Server Client Certificate Authentication

Begge alternativene ble testet, alternativ "b" ble valgt på grunn av sin allsidighet og kompatibilitet med http2-protokollen.

For å fullføre verifiseringen av denne hypotesen, tok det mange eksperimenter med konfigurasjonen; følgende design ble testet:

hvis = krever = omskriv

Resultatet er følgende grunnleggende 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>

Med hensyn til eksisterende autorisasjon fra sertifikateieren, men med et manglende sertifikat, måtte jeg legge til en ikke-eksisterende sertifikateier i form av en av de tilgjengelige variablene SSl_PROTOCOL (i stedet for SSL_CLIENT_S_DN_CN), flere detaljer i dokumentasjonen:

Apache-modul mod_ssl

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

2. For websockets kan du opprette en unik, sikker og beskyttet tilkobling ved å bruke midlertidige økter som genereres under en normal (ikke-websocket) nettleserforespørsel.

Basert på tidligere erfaring, må du legge til en ekstra seksjon til konfigurasjonen for å forberede midlertidige tokens for nettsocket-tilkoblinger under en vanlig (ikke-web-socket) forespørsel.

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

Testing viste at det fungerer. Det er mulig å overføre informasjonskapsler til deg selv gjennom brukerens nettleser.

3. Midlertidige økter kan implementeres med én proxy-webserver (kun innebygde moduler og funksjoner).

Som vi fant ut tidligere, har Apache ganske mye kjernefunksjonalitet som lar deg lage betingede konstruksjoner. Vi trenger imidlertid midler for å beskytte informasjonen vår mens den er i brukerens nettleser, så vi fastslår hva vi skal lagre og hvorfor, og hvilke innebygde funksjoner vi vil bruke:

  • Vi trenger et symbol som ikke enkelt kan dekodes.
  • Vi trenger et token som har foreldelse innebygd og muligheten til å sjekke foreldelse på serveren.
  • Vi trenger en token som vil være knyttet til eieren av sertifikatet.

Dette krever en hashing-funksjon, et salt og en dato for å elde tokenet. Basert på dokumentasjonen Uttrykk i Apache HTTP Server vi har alt ut av esken sha1 og %{TIME}.

Resultatet ble dette designet:

#нет сертификата, и обращение к 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ådd, men det er problemer med serverforeldelse (du kan bruke en år gammel informasjonskapsel), som betyr at tokens, selv om de er trygge for intern bruk, er usikre for industriell (masse)bruk.

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

4. Midlertidige økttokens er allerede implementert som ferdige Apache-moduler.

Et betydelig problem gjensto fra forrige iterasjon - manglende evne til å kontrollere token-aldring.

Vi ser etter en ferdig modul som gjør dette, ifølge ordene: apache token json two factor auth

Ja, det finnes ferdige moduler, men de er alle knyttet til spesifikke handlinger og har artefakter i form av å starte en økt og ytterligere informasjonskapsler. Det vil si, ikke på en stund.
Det tok oss fem timer å søke, noe som ikke ga noe konkret resultat.

5. Midlertidige økttokens kan implementeres ved å logisk utforme strukturen til interaksjoner.

Ferdige moduler er for komplekse, fordi vi bare trenger et par funksjoner.

Når det er sagt, er problemet med datoen at Apaches innebygde funksjoner ikke tillater generering av en dato fra fremtiden, og det er ingen matematisk addisjon/subtraksjon i de innebygde funksjonene når man sjekker for foreldelse.

Det vil si at du ikke kan skrive:

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

Du kan bare sammenligne to tall.

Mens jeg søkte etter en løsning for Safari-problemet, fant jeg en interessant artikkel: Sikring av HomeAssistant med klientsertifikater (fungerer med Safari/iOS)
Den beskriver et eksempel på kode i Lua for Nginx, og som, som det viste seg, i stor grad gjentar logikken til den delen av konfigurasjonen som vi allerede har implementert, med unntak av bruken av hmac-saltingsmetoden for hashing ( dette ble ikke funnet i Apache).

Det ble klart at Lua er et språk med klar logikk, og det er mulig å gjøre noe enkelt for Apache:

Etter å ha studert forskjellen med Nginx og Apache:

Og tilgjengelige funksjoner fra Lua-språkprodusenten:
22.1 – Dato og klokkeslett

Vi fant en måte å sette env-variabler i en liten Lua-fil for å sette en dato fra fremtiden for å sammenligne med den nåværende.

Slik ser et enkelt Lua-skript ut:

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 slik fungerer det totalt, med optimalisering av antall informasjonskapsler og utskifting av token når halvparten av tiden kommer før den gamle informasjonskapselen (token) utløper:

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 bare aktiveres etter tilgangskontroller basert på denne informasjonen fra Nginx.

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

Link til kilde bilde.

Et annet punkt.

Generelt spiller det ingen rolle i hvilken rekkefølge direktivene er skrevet i Apache-konfigurasjonen (sannsynligvis også Nginx), siden alt til slutt vil bli sortert basert på rekkefølgen på forespørselen fra brukeren, som tilsvarer skjemaet for behandling Lua-manus.

Fullføring:

Synlig tilstand etter implementering (mål):
administrasjon av tjenester og infrastruktur er tilgjengelig fra en mobiltelefon på IOS uten tilleggsprogrammer (VPN), enhetlig og sikker.

Målet er nådd, web-sockets fungerer og har et sikkerhetsnivå som ikke er mindre enn et sertifikat.

Hvordan vi i ZeroTech koblet Apple Safari og klientsertifikater med websockets

Kilde: www.habr.com

Legg til en kommentar