Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

L'article sera utile à ceux qui :

  • sait ce qu'est Client Cert et comprend pourquoi il a besoin de websockets sur Safari mobile ;
  • Je souhaite publier des services web à un cercle restreint de personnes ou uniquement à moi-même ;
  • pense que tout a déjà été fait par quelqu'un et aimerait rendre le monde un peu plus pratique et plus sûr.

L'histoire des websockets a commencé il y a environ 8 ans. Auparavant, les méthodes étaient utilisées sous la forme de longues requêtes http (en fait des réponses) : le navigateur de l'utilisateur envoyait une requête au serveur et attendait qu'il réponde à quelque chose, après la réponse, il se connectait à nouveau et attendait. Mais ensuite les websockets sont apparus.

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

Il y a quelques années, nous avons développé notre propre implémentation en PHP pur, qui ne peut pas utiliser les requêtes https, puisqu'il s'agit de la couche liaison. Il n'y a pas si longtemps, presque tous les serveurs Web ont appris à proxyer les requêtes via https et à prendre en charge connection:upgrade.

Lorsque cela s'est produit, les websockets sont devenus presque le service par défaut pour les applications SPA, car il est pratique de fournir du contenu à l'utilisateur à l'initiative du serveur (transmettre un message d'un autre utilisateur ou télécharger une nouvelle version d'une image, d'un document, d'une présentation). que quelqu'un d'autre est en train d'éditer).

Bien que le certificat client existe depuis un certain temps, il reste encore mal pris en charge, car il crée de nombreux problèmes lorsqu'on tente de le contourner. Et (peut-être :slightly_smiling_face: ) c'est pourquoi les navigateurs IOS (tous sauf Safari) ne veulent pas l'utiliser et le demander au magasin de certificats local. Les certificats présentent de nombreux avantages par rapport aux clés de connexion/pass ou ssh ou à la fermeture des ports nécessaires via un pare-feu. Mais ce n’est pas de cela qu’il s’agit.

Sur iOS, la procédure d'installation d'un certificat est assez simple (non dénuée de précisions), mais en général elle se fait selon des instructions, qui sont nombreuses sur Internet et qui ne sont disponibles que pour le navigateur Safari. Malheureusement, Safari ne sait pas comment utiliser Client Сert pour les sockets Web, mais il existe de nombreuses instructions sur Internet sur la façon de créer un tel certificat, mais en pratique, cela est inaccessible.

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

Pour comprendre les websockets, nous avons utilisé le plan suivant : problème/hypothèse/solution.

Problème: il n'y a pas de prise en charge des sockets Web lors du proxy de requêtes vers des ressources protégées par un certificat client sur le navigateur mobile Safari pour IOS et d'autres applications ayant activé la prise en charge des certificats.

Des hypothèses:

  1. Il est possible de configurer une telle exception pour utiliser des certificats (sachant qu'il n'y en aura pas) aux websockets des ressources proxy internes/externes.
  2. Pour les websockets, vous pouvez établir une connexion unique, sécurisée et défendable à l'aide de sessions temporaires générées lors d'une requête de navigateur normale (non websocket).
  3. Les sessions temporaires peuvent être implémentées à l'aide d'un serveur Web proxy (modules et fonctions intégrés uniquement).
  4. Les jetons de session temporaires ont déjà été implémentés sous forme de modules Apache prêts à l'emploi.
  5. Les jetons de session temporaires peuvent être implémentés en concevant logiquement la structure d'interaction.

État visible après mise en œuvre.

Objectif: la gestion des services et des infrastructures doit être accessible depuis un téléphone mobile sur IOS sans programmes supplémentaires (comme le VPN), unifiée et sécurisée.

Objectif supplémentaire : gain de temps et de ressources/trafic téléphonique (certains services sans sockets web génèrent des requêtes inutiles) avec une livraison plus rapide des contenus sur l'Internet mobile.

Comment vérifier?

1. Pages d'ouverture :

— например, 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 dans la console développeur :

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

Tests d'hypothèses:

1. Il est possible de configurer une telle exception pour utiliser des certificats (sachant qu'il n'y en aura pas) sur les sockets Web des ressources proxy internes/externes.

2 solutions ont été trouvées ici :

a) Au niveau

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

changer de niveau d'accès.

Cette méthode présente les nuances suivantes :

  • La vérification du certificat a lieu après une demande adressée à la ressource mandatée, c'est-à-dire une négociation après la demande. Cela signifie que le proxy chargera d'abord puis coupera la demande adressée au service protégé. C'est mauvais, mais pas critique ;
  • Dans le protocole http2. Il est encore en projet et les fabricants de navigateurs ne savent pas comment l'implémenter #info sur tls1.3 http2 post handshake (ne fonctionne pas maintenant) Implémenter la RFC 8740 « Utilisation de TLS 1.3 avec HTTP/2 »;
  • On ne sait pas comment unifier ce traitement.

b) Au niveau de base, autorisez SSL sans certificat.

SSLVerifyClient require => SSLVerifyClient facultatif, mais cela réduit le niveau de sécurité du serveur proxy, puisqu'une telle connexion sera traitée sans certificat. Cependant, vous pouvez également refuser l'accès aux services proxy avec la directive suivante :

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"

Des informations plus détaillées peuvent être trouvées dans l'article sur SSL : Authentification du certificat client du serveur Apache

Les deux options ont été testées, l'option « b » a été choisie pour sa polyvalence et sa compatibilité avec le protocole http2.

Pour compléter la vérification de cette hypothèse, il a fallu de nombreuses expériences avec la configuration ; les conceptions suivantes ont été testées :

si = exiger = réécrire

Le résultat est la conception de base suivante :

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>

Compte tenu de l'autorisation existante du propriétaire du certificat, mais avec un certificat manquant, j'ai dû ajouter un propriétaire de certificat inexistant sous la forme d'une des variables disponibles SSl_PROTOCOL (au lieu de SSL_CLIENT_S_DN_CN), plus de détails dans la documentation :

Module Apache mod_ssl

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

2. Pour les websockets, vous pouvez établir une connexion unique, sécurisée et protégée à l'aide de sessions temporaires générées lors d'une requête de navigateur normale (non websocket).

Sur la base de votre expérience précédente, vous devez ajouter une section supplémentaire à la configuration afin de préparer des jetons temporaires pour les connexions de socket Web lors d'une requête régulière (non-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>

Les tests ont montré que cela fonctionne. Il est possible de vous transférer des Cookies via le navigateur de l'utilisateur.

3. Les sessions temporaires peuvent être mises en œuvre à l'aide d'un serveur Web proxy (uniquement modules et fonctions intégrés).

Comme nous l'avons découvert précédemment, Apache possède de nombreuses fonctionnalités de base qui vous permettent de créer des constructions conditionnelles. Cependant, nous avons besoin de moyens pour protéger nos informations lorsqu’elles se trouvent dans le navigateur de l’utilisateur. Nous déterminons donc ce que nous devons stocker et pourquoi, ainsi que les fonctions intégrées que nous utiliserons :

  • Nous avons besoin d’un jeton qui ne peut pas être facilement décodé.
  • Nous avons besoin d’un jeton intégrant l’obsolescence et capable de vérifier l’obsolescence sur le serveur.
  • Nous avons besoin d'un token qui sera associé au propriétaire du certificat.

Cela nécessite une fonction de hachage, un sel et une date pour vieillir le jeton. Basé sur la documentation Expressions dans le serveur HTTP Apache nous avons tout prêt à l'emploi sha1 et %{TIME}.

Le résultat a été cette conception :

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

L'objectif a été atteint, mais il y a des problèmes d'obsolescence du serveur (vous pouvez utiliser un Cookie vieux d'un an), ce qui signifie que les tokens, bien que sûrs pour un usage interne, ne sont pas sûrs pour un usage industriel (de masse).

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

4. Les jetons de session temporaires ont déjà été implémentés sous forme de modules Apache prêts à l'emploi.

Un problème important persistait par rapport à l'itération précédente : l'incapacité de contrôler le vieillissement des jetons.

Nous recherchons un module prêt à l'emploi qui fait cela, selon les mots : apache token json two factor auth

Oui, il existe des modules prêts à l'emploi, mais ils sont tous liés à des actions spécifiques et comportent des artefacts sous la forme de démarrage d'une session et de cookies supplémentaires. Autrement dit, pas avant un moment.
Il nous a fallu cinq heures de recherche, qui n'ont donné aucun résultat concret.

5. Les jetons de session temporaires peuvent être implémentés en concevant logiquement la structure des interactions.

Les modules prêts à l'emploi sont trop complexes, car nous n'avons besoin que de quelques fonctions.

Cela étant dit, le problème avec la date est que les fonctions intégrées d'Apache ne permettent pas de générer une date à partir du futur, et il n'y a pas d'addition/soustraction mathématique dans les fonctions intégrées lors de la vérification de l'obsolescence.

Autrement dit, vous ne pouvez pas écrire :

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

Vous ne pouvez comparer que deux nombres.

En cherchant une solution de contournement au problème de Safari, j'ai trouvé un article intéressant : Sécuriser HomeAssistant avec des certificats clients (fonctionne avec Safari/iOS)
Il décrit un exemple de code en Lua pour Nginx, et qui, il s'est avéré, répète très bien la logique de cette partie de la configuration que nous avons déjà implémentée, à l'exception de l'utilisation de la méthode de salage hmac pour le hachage ( cela n'a pas été trouvé dans Apache).

Il est devenu clair que Lua est un langage avec une logique claire, et il est possible de faire quelque chose de simple pour Apache :

Après avoir étudié la différence avec Nginx et Apache :

Et les fonctions disponibles chez le fabricant du langage Lua :
22.1 – Date et heure

Nous avons trouvé un moyen de définir des variables d'environnement dans un petit fichier Lua afin de définir une date du futur à comparer avec la date actuelle.

Voici à quoi ressemble un 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

Et voici comment tout cela fonctionne au total, avec optimisation du nombre de Cookies et remplacement du token la moitié du temps avant l'expiration de l'ancien 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 

Parce que LuaHookAccessChecker ne sera activé qu'après des contrôles d'accès basés sur ces informations de Nginx.

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

Lien vers la source Image.

Un autre point.

En général, peu importe dans quel ordre les directives sont écrites dans la configuration Apache (probablement aussi Nginx), puisqu'au final tout sera trié en fonction de l'ordre de la demande de l'utilisateur, qui correspond au schéma de traitement Scripts Lua.

Achèvement:

État visible après la mise en œuvre (objectif) :
la gestion des services et des infrastructures est disponible depuis un téléphone mobile sur IOS sans programmes supplémentaires (VPN), unifiée et sécurisée.

L'objectif est atteint, les web sockets fonctionnent et ont un niveau de sécurité qui n'est pas inférieur à celui d'un certificat.

Comment chez ZeroTech nous nous sommes liés d'amitié avec Apple Safari et les certificats clients avec les websockets

Source: habr.com

Ajouter un commentaire