Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

O artigo será útil para aqueles que:

  • sabe o que é Client Cert e entende por que necesita websockets en Safari móbil;
  • Gustaríame publicar servizos web para un círculo limitado de persoas ou só para min;
  • pensa que todo xa o fixo alguén, e gustaríalle facer o mundo un pouco máis cómodo e seguro.

A historia dos websockets comezou hai uns 8 anos. Anteriormente, usábanse métodos como as peticións http longas (en realidade respostas): o navegador do usuario enviaba unha solicitude ao servidor e agardaba a que contestase algo, despois da resposta conectouse de novo e agardou. Pero entón apareceron os websockets.

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

Hai uns anos, desenvolvemos a nosa propia implementación en PHP puro, que non pode usar solicitudes https, xa que esta é a capa de ligazón. Non hai moito tempo, case todos os servidores web aprenderon a solicitar solicitudes de proxy a través de https e admitir conexión: actualización.

Cando isto ocorreu, os websockets convertéronse case no servizo predeterminado das aplicacións SPA, porque o cómodo que é proporcionar contido ao usuario por iniciativa do servidor (transmitir unha mensaxe doutro usuario ou descargar unha nova versión dunha imaxe, documento, presentación). que outra persoa está editando actualmente).

Aínda que o certificado de cliente existe desde hai bastante tempo, aínda segue sendo mal compatible, xa que crea moitos problemas ao tentar evitalo. E (posiblemente :slightly_smiling_face: ) é por iso que os navegadores IOS (todos agás Safari) non queren usalo e solicitalo na tenda de certificados local. Os certificados teñen moitas vantaxes en comparación coas claves de inicio de sesión/paso ou ssh ou o peche dos portos necesarios a través dun cortalumes. Pero isto non é do que se trata.

En iOS, o procedemento para instalar un certificado é bastante sinxelo (non exento de detalles), pero en xeral faise seguindo instrucións, das que hai moitas en Internet e que só están dispoñibles para o navegador Safari. Desafortunadamente, Safari non sabe como usar Client Сert para sockets web, pero hai moitas instrucións en Internet sobre como crear tal certificado, pero na práctica isto é inalcanzable.

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

Para entender os websockets, usamos o seguinte plan: problema/hipótese/solución.

Problema: non hai soporte para sockets web ao enviar solicitudes de proxy a recursos que están protexidos por un certificado de cliente no navegador móbil Safari para IOS e outras aplicacións que habilitaron a compatibilidade con certificados.

Hipóteses:

  1. É posible configurar tal excepción para usar certificados (sabendo que non haberá ningún) para websockets de recursos proxy internos/externos.
  2. Para os websockets, pode facer unha conexión única, segura e defendible mediante sesións temporais que se xeran durante unha solicitude de navegador normal (non websocket).
  3. As sesións temporais pódense implementar mediante un servidor web proxy (só módulos e funcións incorporados).
  4. Os tokens de sesión temporais xa se implementaron como módulos Apache preparados.
  5. Os tokens de sesión temporais pódense implementar deseñando loxicamente a estrutura de interacción.

Estado visible despois da implementación.

Obxectivo do traballo: a xestión de servizos e infraestruturas debe ser accesible desde un teléfono móbil en iOS sen programas adicionais (como VPN), unificado e seguro.

Obxectivo adicional: aforro de tempo e recursos/tráfico telefónico (algúns servizos sen sockets web xeran solicitudes innecesarias) cunha entrega máis rápida de contidos na Internet móbil.

Como comprobar?

1. Páxinas de apertura:

— например, 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. Ou na consola do programador:

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

Proba de hipótese:

1. É posible configurar tal excepción para utilizar certificados (sabendo que non haberá ningún) en sockets web de recursos proxy internos/externos.

Aquí atopáronse 2 solucións:

a) A nivel

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

cambiar o nivel de acceso.

Este método ten os seguintes matices:

  • A verificación do certificado prodúcese despois dunha solicitude ao recurso proxy, é dicir, despois do axuste de contacto da solicitude. Isto significa que primeiro cargará o proxy e despois cortará a solicitude ao servizo protexido. Isto é malo, pero non crítico;
  • No protocolo http2. Aínda está en borrador e os fabricantes de navegadores non saben como implementalo #información sobre tls1.3 http2 post apretón de mans (non funciona agora) Implementar RFC 8740 "Uso de TLS 1.3 con HTTP/2";
  • Non está claro como unificar este procesamento.

b) A nivel básico, permitir ssl sen certificado.

SSLVerifyClient require => SSLVerifyClient opcional, pero isto reduce o nivel de seguridade do servidor proxy, xa que tal conexión procesarase sen certificado. Non obstante, pode denegar aínda máis o acceso aos servizos proxy coa seguinte directiva:

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"

Pódese atopar información máis detallada no artigo sobre ssl: Autenticación do certificado do cliente do servidor Apache

Probáronse ambas as opcións, escolleuse a opción “b” pola súa versatilidade e compatibilidade co protocolo http2.

Para completar a verificación desta hipótese, foron necesarios moitos experimentos coa configuración; probáronse os seguintes deseños:

se = requirir = reescribir

O resultado é o seguinte deseño básico:

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>

Tendo en conta a autorización existente do propietario do certificado, pero con un certificado que faltaba, tiven que engadir un propietario de certificado inexistente en forma dunha das variables dispoñibles SSl_PROTOCOL (en lugar de SSL_CLIENT_S_DN_CN), máis detalles na documentación:

Módulo Apache mod_ssl

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

2. Para os websockets, pode realizar unha conexión única, segura e protexida mediante sesións temporais que se xeran durante unha solicitude de navegador normal (non websocket).

Segundo a experiencia previa, cómpre engadir unha sección adicional á configuración para preparar tokens temporais para as conexións de socket web durante unha solicitude normal (que non sexa socket web).

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

As probas demostraron que funciona. É posible transferir Cookies a si mesmo a través do navegador do usuario.

3. As sesións temporais pódense implementar mediante un servidor web proxy (só módulos e funcións incorporados).

Como descubrimos anteriormente, Apache ten unha gran cantidade de funcionalidades básicas que che permiten crear construcións condicionais. Non obstante, necesitamos medios para protexer a nosa información mentres está no navegador do usuario, polo que establecemos que almacenar e por que, e que funcións integradas usaremos:

  • Necesitamos un token que non se pode decodificar facilmente.
  • Necesitamos un token que teña a obsolescencia incorporada e a capacidade de comprobar a obsolescencia no servidor.
  • Necesitamos un token que estará asociado co propietario do certificado.

Isto require unha función de hash, un sal e unha data para envellecer o token. En base á documentación Expresións no servidor HTTP Apache témolo todo fóra da caixa sha1 e %{TIME}.

O resultado foi este deseño:

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

O obxectivo conseguiuse, pero hai problemas coa obsolescencia do servidor (pódese usar unha Cookie de anos), o que significa que os tokens, aínda que son seguros para o uso interno, non son seguros para o uso industrial (masivo).

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

4. Os tokens de sesión temporais xa se implementaron como módulos Apache preparados.

Da iteración anterior quedou un problema significativo: a incapacidade de controlar o envellecemento do token.

Buscamos un módulo preparado que faga isto, segundo as palabras: apache token json two factor auth

Si, hai módulos preparados, pero todos están ligados a accións específicas e teñen artefactos en forma de inicio dunha sesión e Cookies adicionais. É dicir, non por un tempo.
Tardamos cinco horas en buscar, o que non deu un resultado concreto.

5. Os tokens de sesión temporais pódense implementar deseñando loxicamente a estrutura das interaccións.

Os módulos preparados son demasiado complexos, porque só necesitamos un par de funcións.

Dito isto, o problema coa data é que as funcións integradas de Apache non permiten xerar unha data do futuro, e non hai ningunha suma/resta matemática nas funcións integradas ao comprobar a obsolescencia.

É dicir, non podes escribir:

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

Só podes comparar dous números.

Mentres buscaba unha solución para o problema de Safari, atopei un artigo interesante: Asegurar HomeAssistant con certificados de cliente (funciona con Safari/iOS)
Describe un exemplo de código en Lua para Nginx, e que, como se viu, repite moito a lóxica desa parte da configuración que xa implementamos, coa excepción do uso do método de salting hmac para o hash ( isto non se atopou en Apache).

Quedou claro que Lua é unha linguaxe cunha lóxica clara, e é posible facer algo sinxelo para Apache:

Despois de estudar a diferenza con Nginx e Apache:

E funcións dispoñibles do fabricante da lingua Lua:
22.1 – Data e hora

Atopamos unha forma de establecer variables env nun pequeno ficheiro Lua para establecer unha data do futuro para comparar coa actual.

Este é o aspecto dun simple script Lua:

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

E así é como funciona todo en total, con optimización do número de Cookies e substitución do token cando transcorre a metade do tempo antes de que caduque a antiga Cookie (token):

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 

Porque LuaHookAccessChecker só se activará despois das comprobacións de acceso baseadas nesta información de Nginx.

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

Ligazón á fonte Imaxe.

Unha cousa máis.

En xeral, non importa en que orde se escriben as directivas na configuración de Apache (probablemente tamén Nginx), xa que ao final todo ordenarase en función da orde da solicitude do usuario, que corresponde ao esquema de procesamento. Guións Lua.

Finalización:

Estado visible despois da implementación (obxectivo):
a xestión de servizos e infraestruturas está dispoñible desde un teléfono móbil en iOS sen programas adicionais (VPN), unificado e seguro.

O obxectivo conseguiuse, os sockets web funcionan e teñen un nivel de seguridade nada menos que un certificado.

Como en ZeroTech conectamos Apple Safari e certificados de cliente con websockets

Fonte: www.habr.com

Engadir un comentario