Como nós da ZeroTech fizemos amizade com o Apple Safari e certificados de cliente com o websocket

O artigo será útil para quem:



  • sabe o que é o Client Cert e entende por que os websockets no Safari móvel são para ele;
  • gostaria de publicar serviços da web para um círculo limitado de pessoas ou apenas para mim;
  • Ele acha que tudo já foi feito por alguém e gostaria de tornar o mundo um pouco mais conveniente e seguro.


A história dos soquetes da web começou há cerca de 8 anos. Anteriormente, métodos da forma de solicitações HTTP longas (na verdade, respostas) eram usados: o navegador do usuário enviou uma solicitação ao servidor e esperou que ele respondesse alguma coisa, após a resposta ele se conectou novamente e esperou. Mas então surgiram websockets.







Há vários anos, desenvolvemos nossa própria implementação de php puro, que não sabe como usar solicitações https, pois essa é a camada de link de dados. Não faz muito tempo, quase todos os servidores da Web aprenderam a solicitar proxy por https e suporte à conexão: atualização.



Quando isso aconteceu, os soquetes da Web tornaram-se quase o serviço padrão para aplicativos SPA, porque é conveniente fornecer ao usuário o conteúdo por iniciativa do servidor (enviar uma mensagem de outro usuário ou fazer o download de uma nova versão de uma imagem, documento ou apresentação que outra pessoa esteja editando agora) .



Embora o Client Cert já exista há algum tempo, ele ainda é pouco suportado, pois cria muitos problemas ao tentar contorná-lo. E (talvez: mild_smiling_face :), para que os navegadores IOS (todos, exceto o Safari) não desejem usá-lo e solicite ao armazenamento de certificados local. Os certificados têm muitas vantagens em relação às chaves de login / passe ou ssh ou ao firewall das portas corretas. Mas esse não é o ponto.



No IOS, o procedimento para instalar um certificado é bastante simples (não sem detalhes), mas geralmente é feito de acordo com as instruções, que são muito numerosas na rede e estão disponíveis apenas para o navegador Safari. Infelizmente, o Safari não sabe como usar o Client Cert para soquetes da Web, mas há muitas instruções na Internet sobre como fazer esse certificado, mas, na prática, isso é inatingível.







Para entender os web sockets, usamos o seguinte plano: problema / hipótese / solução.



Problema: Não há suporte para soquetes da Web ao solicitar solicitações de proxy para recursos protegidos por um certificado de cliente no navegador Safari móvel para IOS e outros aplicativos que incluíram suporte a certificados.



Hipóteses:



  1. É possível configurar essa exceção para usar certificados (sabendo que eles não estarão disponíveis) para soquetes da Web de recursos de proxy interno / externo.
  2. Para soquetes da Web, é possível estabelecer uma conexão segura e protegida exclusiva usando sessões temporárias geradas durante uma solicitação regular do navegador (sem soquete da Web).
  3. As sessões transitórias podem ser implementadas usando um único servidor web proxy (apenas módulos e funções integrados).
  4. Os tokens de sessão temporária já foram implementados como módulos apache prontos.
  5. Os tokens de sessão temporária podem ser implementados projetando logicamente a estrutura de interação.


Estado visível após a implementação.



Objetivo do trabalho: o gerenciamento de serviços e infraestrutura deve estar disponível a partir de um telefone celular no IOS, sem programas adicionais (como VPN), unificados e seguros.



Um objetivo adicional: economizar tempo e recursos / tráfego telefônico (alguns serviços sem soquetes da Web geram solicitações desnecessárias) com entrega mais rápida de conteúdo na Internet móvel.



Como verificar?



1. Abrir páginas:



— , 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 no console do desenvolvedor:







Hipóteses de teste:



1. É possível configurar essa exceção para usar certificados (sabendo que eles não estarão disponíveis) para soquetes da Web de recursos proxy internos / externos.



Aqui foram encontradas 2 soluções:



a) No nível de



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


alterar o nível de acesso.



Este método possui as seguintes nuances:



  • O certificado é verificado após uma solicitação ao recurso em proxy, ou seja, handshake pós-solicitação. Isso significa que o proxy primeiro será carregado e, em seguida, cortará a solicitação para o serviço protegido. Isso é ruim, mas não crítico;
  • No protocolo http2. Ele ainda está em rascunho e os fabricantes de navegadores não sabem como implementá-lo #info sobre o handshake pós tls1.3 http2 (não está funcionando agora) Implementar a RFC 8740 "Usando o TLS 1.3 com HTTP / 2" ;
  • Não está claro como unificar esse processamento.


b) Em um nível básico, permita ssl sem um certificado.



SSLVerifyClient require => SSLVerifyClient opcional, mas isso reduz o nível de proteção do servidor proxy, pois essa conexão será processada sem um certificado. No entanto, você pode negar ainda mais o acesso a serviços de proxy com a seguinte diretiva:



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 obter mais informações, consulte o artigo sobre ssl: Autenticação de certificado de cliente Apache Server.



Ambas as opções foram testadas, a opção "b" foi escolhida para universalidade e compatibilidade com o protocolo http2.



Para concluir a verificação dessa hipótese, foram necessárias muitas experiências de configuração, as construções foram verificadas:



if = require = rewrite



Temos a seguinte construção 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>




Considerando a autorização existente pelo proprietário do certificado, mas com o certificado ausente, tivemos que adicionar o proprietário inexistente do certificado na forma de uma das variáveis ​​SSl_PROTOCOL disponíveis (em vez de SSL_CLIENT_S_DN_CN), para obter mais detalhes, consulte a documentação:



Apache Module mod_ssl







2. Para soquetes da Web, você pode fazer uma conexão segura e protegida exclusiva usando sessões temporárias geradas durante uma solicitação de navegador regular (não um soquete da Web).



Com base na experiência anterior, você precisa adicionar uma seção adicional à configuração para preparar tokens temporários para uma conexão de soquete da Web com uma solicitação normal (não um soquete da 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>


O teste mostrou que funciona. É possível transmitir um cookie para o navegador do usuário.



3. Sessões temporárias podem ser implementadas usando um servidor Web proxy (apenas módulos e funções integrados).



Como descobrimos anteriormente, o Apache possui muitas funcionalidades principais que permitem criar construções condicionais. No entanto, precisamos de um meio de proteger nossas informações enquanto estão no navegador do usuário, para definir o que e para o que armazenar e quais funções internas usaremos:



  • Precisamos de um token que desafie a decodificação simples.
  • Precisamos de um token no qual a obsolescência e a capacidade de verificar a obsolescência no servidor estejam protegidas.
  • Precisamos de um token que será associado ao proprietário do certificado.


Isso requer uma função de hash, salt e uma data para expirar o token. Com base na documentação do Expressions in Apache HTTP Server , temos tudo isso pronto para o uso sha1 e% {TIME}.



O resultado é a seguinte construçã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 objetivo foi alcançado, mas há problemas com a obsolescência do servidor (você pode usar um cookie de um ano atrás), o que significa que os tokens, embora seguros para uso interno, não são seguros para uso industrial (em massa).







4. Os tokens de sessão temporária já foram implementados como módulos Apache prontos.



Da iteração anterior, um problema significativo permaneceu - a incapacidade de controlar a expiração do token.



Estamos procurando por um módulo pronto que faça isso, de acordo com as palavras: apache token json two factor auth





Sim, existem módulos prontos, mas todos estão vinculados a ações específicas e possuem artefatos na forma de início de sessão e cookies adicionais. Ou seja, não por um tempo.

Levamos cinco horas para pesquisar, o que não produziu nenhum resultado concreto.



5. Sessões temporárias de token podem ser implementadas projetando logicamente a estrutura de interação.



Os módulos prontos são muito complicados porque precisamos apenas de algumas funções.



Dito isso, o problema com a data é que as funções internas do Apache não permitem gerar uma data a partir do futuro e não há adição / subtração matemática nas funções internas ao verificar a obsolescência.



Ou seja, você não pode escrever:



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


Apenas dois números podem ser comparados.



Ao procurar uma solução alternativa para o Safari, foi encontrado um artigo interessante: Protegendo o HomeAssistant com certificados de cliente (funciona com Safari / iOS)

Descreve um exemplo de código Lua para Nginx e, como se vê, repete a lógica da parte da configuração que já implementamos, exceto pelo uso do método hmac de organizar o sal para o hash (isso não foi encontrado no Apache).



Ficou claro que Lua é uma linguagem com lógica clara, é possível fazer algo simples para o Apache:





Tendo estudado a diferença com Nginx e Apache:





E as funções disponíveis do fabricante da linguagem Lua:

22.1 - Data e hora



Foi encontrada uma maneira de definir variáveis ​​env em um pequeno arquivo Lua para definir uma data do futuro para verificar com a atual.



É assim que um script Lua simples se parece:
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 é assim que tudo funciona em suma, com a otimização do número do Cookie e a substituição do token quando o intervalo chegar antes da expiração do antigo 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 será ativado somente após verificações de acesso com base nessas informações do Nginx.







Link para a fonte da imagem .



Mais um ponto.



Em geral, não importa em qual sequência as diretivas são escritas na configuração do Apache (provavelmente também no Nginx), pois no final tudo será classificado com base na ordem em que a solicitação do usuário passa, que corresponde ao esquema para o processamento de scripts Lua.



Conclusão:



Estado visível após a implementação (objetivo): o

gerenciamento de serviços e infraestrutura está disponível em um telefone móvel no IOS sem programas adicionais (VPN), unificados e seguros.



O objetivo é alcançado, os soquetes da Web funcionam e têm menos segurança que um certificado.






All Articles