Cómo en ZeroTech hicimos certificados de amigos y clientes de Apple Safari con websockets

El artículo será útil para aquellos que:



  • sabe qué es Client Cert y entiende por qué necesita websockets en Safari móvil;
  • quisiera publicar servicios web para un círculo limitado de personas o solo para mí;
  • piensa que alguien ya lo ha hecho todo y quisiera hacer que el mundo sea un poco más conveniente y seguro.


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







Hace unos años, desarrollamos nuestra propia implementación en php puro, que no sabe cómo usar las solicitudes https, ya que esta es una capa de enlace. No hace mucho tiempo, casi todos los servidores web aprendieron a las solicitudes de proxy a través de https y conexión de soporte: actualización.



Cuando esto sucedió, los sockets web se convirtieron prácticamente en el servicio predeterminado para las aplicaciones de SPA, ya que es conveniente proporcionar al usuario contenido iniciado por el servidor (envíe un mensaje de otro usuario o cargue una nueva versión de una imagen, documento, presentación que alguien más está editando) ...



Aunque Client Cert ha existido durante bastante tiempo, aún sigue siendo poco compatible, ya que crea muchos problemas al tratar de evitarlo. Y (tal vez: ligeramente_smiling_face :) para que los navegadores IOS (todos excepto Safari) no quieran usarlo y pregunten al almacén de certificados local. Los certificados tienen muchas ventajas sobre las claves de inicio de sesión / contraseña o ssh o cortafuegos en los puertos correctos. Pero esto no se trata de eso.



En iOS, el procedimiento para instalar un certificado es bastante simple (no sin detalles), pero en general se realiza de acuerdo con las instrucciones, que son muy numerosas en la red y que están disponibles solo para el navegador Safari. Desafortunadamente, Safari no sabe cómo usar Client ert para los sockets web, pero hay muchas instrucciones en Internet sobre cómo hacer dicho certificado, pero en la práctica esto es inalcanzable.







Para comprender los websockets, utilizamos el siguiente esquema: 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 Safari móvil para iOS y otras aplicaciones que han incluido soporte de certificado.



Hipótesis



  1. Es posible configurar dicha excepción para usar certificados (sabiendo que no estarán disponibles) en los zócalos web de recursos proxy internos / externos.
  2. Para los sockets web, puede establecer una conexión segura única utilizando sesiones temporales que se generan mediante una solicitud de navegador normal (sin sockets web).
  3. Las sesiones transitorias se pueden implementar utilizando un único servidor web proxy (solo módulos integrados y funciones).
  4. Las sesiones de token temporales ya se han implementado como módulos apache listos para usar.
  5. Los tokens de sesión temporales se pueden implementar mediante el diseño lógico de la estructura de interacción.


Estado visible después de la implementación.



Propósito del trabajo: la administración de servicios e infraestructura debe estar disponible desde un teléfono móvil en iOS sin programas adicionales (como VPN), unificada y segura.



Objetivo adicional: ahorrar tiempo y recursos / tráfico telefónico (algunos servicios sin enchufes web generan solicitudes innecesarias) mientras se acelera la entrega de contenido en Internet móvil.



¿Como revisar?



1. Páginas 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. O en la consola del desarrollador:







Hipótesis de prueba:



1. Es posible configurar dicha excepción para usar certificados (sabiendo que no estarán disponibles) a los zócalos web de recursos proxy internos / externos.



Aquí se encontraron 2 soluciones:



a) En el nivel



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


cambiar el nivel de acceso.



Este método tiene los siguientes matices:



  • El certificado se verifica después de una solicitud al recurso proxy, es decir, un apretón de manos posterior a la solicitud. Esto significa que el proxy primero se cargará y luego cortará la solicitud al servicio protegido. Esto es malo, pero no crítico;
  • En el http2. Todavía está en borrador, y los proveedores de navegadores no saben cómo implementarlo #info sobre tls1.3 http2 post apretón de manos (no funciona ahora) Implemente RFC 8740 "Usando TLS 1.3 con HTTP / 2" ;
  • No está claro cómo unificar este procesamiento.


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



SSLVerifyClient require => SSLVerifyClient opcional, pero esto reduce el nivel de protección del servidor proxy, ya que dicha conexión se procesará sin un certificado. Sin embargo, puede negar aún más el acceso a los 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"


Para obtener más información, consulte 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 universalidad y compatibilidad con el protocolo http2.



Para completar la verificación de esta hipótesis, se requirieron muchos experimentos de configuración, se verificaron las construcciones:



if = require = rewrite



Tenemos la siguiente construcción básica:
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>




Dada la autorización existente para el titular del certificado, pero con un certificado faltante, tuve que agregar un titular de certificado inexistente como una de las variables SSl_PROTOCOL disponibles (en lugar de SSL_CLIENT_S_DN_CN), más en la documentación:



Módulo Apache mod_ssl







2. Para los zócalos web, puede hacer una conexión segura única a utilizando sesiones temporales que se generan durante una solicitud de navegador normal (no un socket web).



Según la experiencia previa, debe agregar una sección adicional a la configuración, de modo que durante una solicitud regular (no de socket web), se preparen tokens temporales para conexiones 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-name\s*=\s*some-val(;|$)/ >
</If

</If>
</If>


Las pruebas han demostrado que funciona. Es posible transferir cookies a través de un navegador de 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 funcionalidades centrales que le permiten crear construcciones condicionales. Sin embargo, necesitamos un medio para proteger nuestra información mientras está en el navegador del usuario, por lo que establecemos qué y para qué almacenar, y qué funciones integradas usaremos:



  • Necesitamos un token que desafíe la decodificación simple.
  • Necesita un token que tenga obsolescencia y la capacidad de verificar la obsolescencia en el servidor.
  • Necesita un token que se asociará con el propietario del certificado.


Para hacer esto, necesita una función hash, sal y fecha para que caduque el token. De acuerdo con la documentación de Expressions in Apache HTTP Server , tenemos todo esto listo para usar sha1 y% {TIME}.



El resultado es la siguiente construcción:
# ,    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 (puede usar la cookie hace un año), lo que significa tokens, aunque seguros para uso interno, pero inseguros para industrial (masivo).







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



De la iteración anterior, quedaba un problema importante: la incapacidad de controlar la caducidad del token.



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





Sí, hay módulos listos para usar, 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 buscar, lo que no produjo ningún resultado concreto.



5. Las sesiones de token temporales se pueden implementar mediante el diseño lógico de la estructura de interacción.



Los módulos listos para usar son demasiado complicados porque solo necesitamos un par de funciones.



Al mismo tiempo, el problema con la fecha es que las funciones integradas de Apache no permiten generar una fecha del futuro, y cuando se verifica la obsolescencia en las funciones integradas no hay suma / resta matemática.



Es decir, no puedes escribir:



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


Solo se pueden comparar dos números.



Al buscar una solución alternativa para Safari, encontré un artículo interesante: Asegurar HomeAssistant con certificados de cliente (funciona con Safari / iOS)

Describe un ejemplo de código Lua para Nginx y que, como resultó, repite la lógica de la parte de la configuración que ya implementamos, con la excepción del uso del método hmac para organizar la sal para el hash (esto no se encontró en Apache).



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





Habiendo estudiado la diferencia con Nginx y Apache:





Y las funciones disponibles del fabricante del lenguaje Lua:

22.1 - Fecha y hora



Se encontró un método para configurar las variables env en un pequeño archivo Lua con el fin de establecer una fecha del futuro para la reconciliación con el 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 todo esto funciona en total, con la optimización del número de cookies y la sustitución del token cuando ha pasado la mitad del tiempo antes de que expiren las cookies antiguas (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 las comprobaciones de acceso basadas en esta información de Nginx.







Enlace a la fuente de la imagen .



Un punto mas.



En general, no importa en qué secuencia se escriben las directivas en la configuración de Apache (probablemente también en Nginx), ya que al final todo se ordenará según el orden en que pasa la solicitud del usuario, que corresponde al esquema para procesar los scripts de Lua.



Finalización:



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 logra, los websockets funcionan y no tienen menos seguridad que un certificado.






All Articles