Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

El artículo será útil para quienes:

  • sabe qué es Client Cert y comprende por qué necesita websockets en Safari móvil;
  • Me gustaría publicar servicios web para un círculo limitado de personas o sólo para mí;
  • Piensa que alguien ya lo ha hecho todo y le gustaría hacer el mundo un poco más cómodo y seguro.

La historia de websockets comenzó hace unos 8 años. Anteriormente, los métodos se usaban en forma de solicitudes http largas (en realidad, respuestas): el navegador del usuario enviaba una solicitud al servidor y esperaba que respondiera algo, después de la respuesta se conectaba nuevamente y esperaba. Pero entonces aparecieron los websockets.

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

Hace unos años, desarrollamos nuestra propia implementación en PHP puro, que no puede usar solicitudes https, ya que esta es la capa de enlace. No hace mucho, casi todos los servidores web aprendieron a enviar solicitudes a través de https y admitir conexión: actualización.

Cuando esto sucedió, websockets se convirtió casi en el servicio predeterminado para las aplicaciones SPA, debido a lo conveniente que es proporcionar contenido al usuario por iniciativa del servidor (transmitir un mensaje de otro usuario o descargar una nueva versión de una imagen, documento, presentación). que otra persona está editando actualmente).

Aunque el Certificado de Cliente existe desde hace bastante tiempo, todavía sigue teniendo poco soporte, ya que crea muchos problemas al intentar evitarlo. Y (posiblemente :slightly_smiling_face: ) es por eso que los navegadores IOS (todos excepto Safari) no quieren usarlo y solicitarlo desde el almacén de certificados local. Los certificados tienen muchas ventajas en comparación con el inicio de sesión/contraseña o las claves ssh o el cierre de los puertos necesarios a través de un firewall. Pero no se trata de eso.

En iOS, el procedimiento para instalar un certificado es bastante sencillo (no exento de detalles), pero en general se realiza según unas instrucciones, que hay muchas en Internet y que sólo están disponibles para el navegador Safari. Desafortunadamente, Safari no sabe cómo utilizar Client Сert para sockets web, pero hay muchas instrucciones en Internet sobre cómo crear dicho certificado, pero en la práctica esto es inalcanzable.

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

Para comprender websockets, utilizamos el siguiente plan: problema/hipótesis/solución.

Problema: no hay soporte para sockets web cuando se envían solicitudes a recursos que están protegidos por un certificado de cliente en el navegador móvil Safari para IOS y otras aplicaciones que tienen habilitado el soporte de certificados.

Hipótesis

  1. Es posible configurar dicha excepción para usar certificados (sabiendo que no habrá ninguno) para websockets de recursos proxy internos/externos.
  2. Para websockets, puede establecer una conexión única, segura y defendible utilizando sesiones temporales que se generan durante una solicitud de navegador normal (no websocket).
  3. Las sesiones temporales se pueden implementar utilizando un servidor web proxy (solo módulos y funciones integrados).
  4. Los tokens de sesión temporales ya se han implementado como módulos de Apache ya preparados.
  5. Los tokens de sesión temporales se pueden implementar diseñando lógicamente la estructura de interacción.

Estado visible después de la implementación.

Objetivo: La gestión de servicios e infraestructura debe ser accesible desde un teléfono móvil en IOS sin programas adicionales (como VPN), unificada y segura.

Objetivo adicional: ahorro de tiempo y recursos/tráfico telefónico (algunos servicios sin sockets web generan solicitudes innecesarias) con una entrega más rápida de contenido en Internet móvil.

¿Cómo verificar?

1. Páginas iniciales:

— например, 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. O en la consola de desarrollador:

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

Evaluación de la hipótesis:

1. Es posible configurar dicha excepción para usar certificados (sabiendo que no habrá ninguno) en sockets web de recursos proxy internos/externos.

Aquí se encontraron 2 soluciones:

a) Al nivel

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

cambiar el nivel de acceso.

Este método tiene los siguientes matices:

  • La verificación del certificado se produce después de una solicitud al recurso proxy, es decir, después del protocolo de enlace de la solicitud. Esto significa que el proxy primero cargará y luego cortará la solicitud al servicio protegido. Esto es malo, pero no crítico;
  • En el protocolo http2. Todavía está en borrador y los fabricantes de navegadores no saben cómo implementarlo. #información sobre el protocolo de enlace de publicación tls1.3 http2 (ahora no funciona) Implementar RFC 8740 “Uso de TLS 1.3 con HTTP/2”;
  • No está claro cómo unificar este procesamiento.

b) En un nivel básico, permitir ssl sin certificado.

SSLVerifyClient requiere => SSLVerifyClient opcional, pero esto reduce el nivel de seguridad del servidor proxy, ya que dicha conexión se procesará sin un certificado. Sin embargo, puede denegar aún más el acceso a servicios proxy con la siguiente 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"

Puede encontrar información más detallada en el artículo sobre SSL: Autenticación del certificado del cliente del servidor Apache

Se probaron ambas opciones, se eligió la opción “b” por su versatilidad y compatibilidad con el protocolo http2.

Para completar la verificación de esta hipótesis, fueron necesarios muchos experimentos con la configuración; se probaron los siguientes diseños:

si = requerir = reescribir

El resultado es el siguiente diseñ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>

Teniendo en cuenta la autorización existente por parte del propietario del certificado, pero con un certificado faltante, tuve que agregar un propietario de certificado inexistente en forma de una de las variables disponibles SSl_PROTOCOL (en lugar de SSL_CLIENT_S_DN_CN), más detalles en la documentación:

Módulo Apache mod_ssl

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

2. Para websockets, puede realizar una conexión única, segura y protegida mediante sesiones temporales que se generan durante una solicitud de navegador normal (no websocket).

Según la experiencia previa, debe agregar una sección adicional a la configuración para preparar tokens temporales para conexiones de socket web durante una solicitud regular (no de 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>

Las pruebas demostraron que funciona. Es posible transferirse Cookies a usted mismo a través del navegador del usuario.

3. Las sesiones temporales se pueden implementar utilizando un servidor web proxy (solo módulos y funciones integrados).

Como descubrimos anteriormente, Apache tiene una gran cantidad de funciones básicas que le permiten crear construcciones condicionales. Sin embargo, necesitamos medios para proteger nuestra información mientras está en el navegador del usuario, por lo que establecemos qué almacenar y por qué, y qué funciones integradas usaremos:

  • Necesitamos un token que no pueda decodificarse fácilmente.
  • Necesitamos un token que tenga obsolescencia incorporada y la capacidad de verificar la obsolescencia en el servidor.
  • Necesitamos un token que se asociará con el propietario del certificado.

Esto requiere una función hash, una sal y una fecha para envejecer el token. Basado en la documentación Expresiones en Apache HTTP Server lo tenemos todo listo para usar sha1 y %{TIME}.

El resultado fue este diseñ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>

El objetivo se ha logrado, pero hay problemas con la obsolescencia del servidor (se puede utilizar una cookie de un año de antigüedad), lo que significa que los tokens, aunque son seguros para uso interno, no lo son para uso industrial (masivo).

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

4. Los tokens de sesión temporales ya se han implementado como módulos de Apache ya preparados.

Quedaba un problema importante de la iteración anterior: la incapacidad de controlar el envejecimiento del token.

Estamos buscando un módulo listo para usar que haga esto, de acuerdo con las palabras: apache token json two factor auth

Sí, hay módulos ya preparados, pero todos están vinculados a acciones específicas y tienen artefactos en forma de inicio de sesión y cookies adicionales. Es decir, no por un tiempo.
Nos llevó cinco horas realizar una búsqueda que no arrojó ningún resultado concreto.

5. Los tokens de sesión temporales se pueden implementar diseñando lógicamente la estructura de interacciones.

Los módulos ya preparados son demasiado complejos porque sólo necesitamos un par de funciones.

Dicho esto, el problema con la fecha es que las funciones integradas de Apache no permiten generar una fecha del futuro, y no hay suma/resta matemática en las funciones integradas al comprobar la obsolescencia.

Es decir, no puedes escribir:

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

Sólo puedes comparar dos números.

Mientras buscaba una solución al problema de Safari, encontré un artículo interesante: Proteger HomeAssistant con certificados de cliente (funciona con Safari/iOS)
Describe un ejemplo de código en Lua para Nginx, y que, como resultó, repite en gran medida la lógica de esa parte de la configuración que ya hemos implementado, con la excepción del uso del método hmac salting para hash ( esto no se encontró en Apache).

Quedó claro que Lua es un lenguaje con una lógica clara y es posible hacer algo simple para Apache:

Habiendo estudiado la diferencia con Nginx y Apache:

Y funciones disponibles del fabricante del lenguaje Lua:
22.1 – Fecha y Hora

Encontramos una manera de configurar variables env en un pequeño archivo Lua para establecer una fecha futura para compararla con la actual.

Así es como se ve un script Lua simple:

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

Y así es como funciona todo en total, con optimización del número de Cookies y sustitución del token cuando llega la mitad del tiempo antes de que caduque la antigua 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 solo se activará después de realizar comprobaciones de acceso basadas en esta información de Nginx.

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

Enlace a la fuente imagen.

Otro punto.

En general, no importa en qué orden estén escritas las directivas en la configuración de Apache (probablemente también Nginx), ya que al final todo se ordenará según el orden de solicitud del usuario, que corresponde al esquema de procesamiento. Guiones Lua.

Completar:

Estado visible después de la implementación (objetivo):
La gestión de servicios e infraestructura está disponible desde un teléfono móvil en IOS sin programas adicionales (VPN), unificada y segura.

El objetivo se ha conseguido, los web sockets funcionan y tienen un nivel de seguridad nada menos que un certificado.

Cómo conectamos en ZeroTech Apple Safari y los certificados de cliente con websockets

Fuente: habr.com

Añadir un comentario