Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

O artigo será útil para quem:

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

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

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

Há alguns anos, desenvolvemos nossa própria implementação em PHP puro, que não pode utilizar solicitações https, pois esta é a camada de link. Não muito tempo atrás, quase todos os servidores web aprenderam a fazer proxy de solicitações por https e a oferecer suporte a connection:upgrade.

Quando isso aconteceu, os websockets se tornaram quase o serviço padrão para aplicações SPA, pois como é conveniente fornecer conteúdo ao usuário por iniciativa do servidor (transmitir uma mensagem de outro usuário ou baixar uma nova versão de uma imagem, documento, apresentação que outra pessoa está editando no momento).

Embora o Certificado de Cliente já exista há algum tempo, ele ainda permanece pouco suportado, pois cria muitos problemas ao tentar contorná-lo. E (possivelmente :slightly_smiling_face: ) é por isso que os navegadores IOS (todos exceto Safari) não querem usá-lo e solicitá-lo no armazenamento de certificados local. Os certificados têm muitas vantagens em comparação com login/senha ou chaves ssh ou fechamento das portas necessárias através de um firewall. Mas não é disso que se trata.

No iOS, o procedimento de instalação de um certificado é bastante simples (não sem detalhes), mas em geral é feito de acordo com as instruções, que existem muitas na Internet e que só estão disponíveis para o navegador Safari. Infelizmente, o Safari não sabe como usar o Client Сert para web sockets, mas existem muitas instruções na Internet sobre como criar tal certificado, mas na prática isso é inatingível.

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

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

Problema: não há suporte para web sockets ao fazer proxy de solicitações para recursos protegidos por um certificado de cliente no navegador móvel Safari para IOS e outros aplicativos que tenham suporte de certificado ativado.

Hipóteses:

  1. É possível configurar tal exceção para usar certificados (sabendo que não haverá nenhum) para websockets de recursos proxy internos/externos.
  2. Para websockets, você pode fazer uma conexão exclusiva, segura e defensável usando sessões temporárias geradas durante uma solicitação normal do navegador (não websocket).
  3. Sessões temporárias podem ser implementadas usando um servidor web proxy (somente módulos e funções integrados).
  4. Os tokens de sessão temporários 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: o gerenciamento de serviços e infraestrutura deve ser acessível a partir de um telefone celular no IOS sem programas adicionais (como VPN), unificado e seguro.

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

Como verificar?

1. Abrindo 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:

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

Testando hipóteses:

1. É possível configurar tal exceção para usar certificados (sabendo que não haverá nenhum) para web sockets de recursos proxy internos/externos.

2 soluções foram encontradas aqui:

a) Ao nível

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

alterar o nível de acesso.

Este método possui as seguintes nuances:

  • A verificação do certificado ocorre após uma solicitação ao recurso proxy, ou seja, handshake pós-solicitação. Isso significa que o proxy primeiro carregará e depois interromperá a solicitação ao serviço protegido. Isto é 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 tls1.3 http2 post handshake (não funciona agora) Implementar RFC 8740 "Usando TLS 1.3 com HTTP/2";
  • Não está claro como unificar esse processamento.

b) Em um nível básico, permitir SSL sem certificado.

SSLVerifyClient require => SSLVerifyClient opcional, mas isso reduz o nível de segurança do servidor proxy, pois tal conexão será processada sem certificado. No entanto, você pode negar ainda mais o acesso aos 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"

Informações mais detalhadas podem ser encontradas no artigo sobre SSL: Autenticação de certificado de cliente do servidor Apache

Ambas as opções foram testadas, a opção “b” foi escolhida pela sua versatilidade e compatibilidade com o protocolo http2.

Para completar a verificação desta hipótese, foram necessários vários experimentos com a configuração; os seguintes projetos foram testados:

if = exigir = reescrever

O resultado é o seguinte design 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>

Levando em consideração a autorização existente do proprietário do certificado, mas com certificado ausente, tive que adicionar um proprietário de certificado inexistente na forma de uma das variáveis ​​disponíveis SSl_PROTOCOL (ao invés de SSL_CLIENT_S_DN_CN), mais detalhes na documentação:

Módulo Apache mod_ssl

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

2. Para websockets, você pode fazer uma conexão exclusiva, segura e protegida usando sessões temporárias geradas durante uma solicitação normal do navegador (não websocket).

Com base na experiência anterior, você precisa adicionar uma seção adicional à configuração para preparar tokens temporários para conexões de soquete da Web durante uma solicitação regular (sem 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-names*=s*some-val(;|$)/ >
</If

</If>
</If>

Os testes mostraram que funciona. É possível transferir Cookies para si mesmo através do 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 básicas que permitem criar construções condicionais. Porém, precisamos de meios para proteger nossas informações enquanto elas estão no navegador do usuário, por isso estabelecemos o que armazenar e por que, e quais funções integradas utilizaremos:

  • Precisamos de um token que não possa ser facilmente decodificado.
  • Precisamos de um token que tenha obsolescência incorporada e a capacidade de verificar a obsolescência no servidor.
  • Precisamos de um token que será associado ao proprietário do certificado.

Isso requer uma função hash, um salt e uma data para envelhecer o token. Com base na documentação Expressões no servidor HTTP Apache temos tudo pronto para uso sha1 e %{TIME}.

O resultado foi este desenho:

#нет сертификата, и обращение к 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 com um ano de idade), o que significa que os tokens, embora seguros para uso interno, não são seguros para uso industrial (em massa).

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

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

Um problema significativo permaneceu na iteração anterior: a incapacidade de controlar o envelhecimento do token.

Estamos procurando 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. Isto é, não por um tempo.
Demorámos cinco horas a pesquisar, o que não deu um resultado concreto.

5. Os tokens de sessão temporária podem ser implementados projetando logicamente a estrutura das interações.

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

Dito isto, 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}

Você só pode comparar dois números.

Ao procurar uma solução alternativa para o problema do Safari, encontrei um artigo interessante: Protegendo o HomeAssistant com certificados de cliente (funciona com Safari/iOS)
Ele descreve um exemplo de código em Lua para Nginx, e que, como se viu, repete muito a lógica daquela parte da configuração que já implementamos, com exceção do uso do método hmac salting para hash ( isso não foi encontrado no Apache).

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

Tendo estudado a diferença com Nginx e Apache:

E funções disponíveis no fabricante da linguagem Lua:
22.1 – Data e Hora

Encontramos uma maneira de definir variáveis ​​​​env em um pequeno arquivo Lua para definir uma data futura para comparar com a atual.

Esta é a aparência de um script Lua simples:

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 no total, com otimização da quantidade de Cookies e substituição do token quando chega a metade do tempo antes do Cookie (token) antigo expirar:

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ó será ativado após verificações de acesso com base nessas informações do Nginx.

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

Link para a fonte Imagem.

Outro ponto.

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

Conclusão:

Estado visível após implementação (meta):
o gerenciamento de serviços e infraestrutura está disponível a partir de um celular no IOS sem programas adicionais (VPN), unificado e seguro.

O objetivo foi alcançado, os web sockets funcionam e possuem um nível de segurança não inferior ao de um certificado.

Como nós da ZeroTech conectamos o Apple Safari e certificados de cliente com websockets

Fonte: habr.com

Adicionar um comentário