Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

Artykuł będzie przydatny dla tych, którzy:

  • wie, czym jest Client Cert i rozumie, dlaczego potrzebuje websocktów w mobilnej przeglądarce Safari;
  • Chciałbym publikować usługi internetowe dla ograniczonego kręgu osób lub tylko dla siebie;
  • uważa, że ​​ktoś już wszystko zrobił i chciałby, żeby świat był choć trochę wygodniejszy i bezpieczniejszy.

Historia websocketów rozpoczęła się około 8 lat temu. Wcześniej stosowano metody w postaci długich żądań http (właściwie odpowiedzi): przeglądarka użytkownika wysyłała żądanie do serwera i czekała, aż on na coś odpowie, po odpowiedzi ponownie się łączyła i czekała. Ale potem pojawiły się websockety.

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

Kilka lat temu opracowaliśmy własną implementację w czystym PHP, która nie może korzystać z żądań https, ponieważ jest to warstwa linków. Nie tak dawno temu prawie wszystkie serwery internetowe nauczyły się przesyłać żądania proxy przez https i obsługiwać połączenie: aktualizacja.

Kiedy to nastąpiło, websockets stały się niemal domyślną usługą dla aplikacji SPA, bo jakże wygodne jest dostarczanie treści użytkownikowi z inicjatywy serwera (przesłanie wiadomości od innego użytkownika lub pobranie nowej wersji obrazu, dokumentu, prezentacji że ktoś inny aktualnie edytuje).

Chociaż Certyfikat Klienta istnieje już od dłuższego czasu, nadal jest słabo obsługiwany, ponieważ stwarza wiele problemów przy próbie jego obejścia. I (prawdopodobnie :slightly_smiling_face: ) dlatego przeglądarki IOS (wszystkie z wyjątkiem Safari) nie chcą go używać i żądają go z lokalnego magazynu certyfikatów. Certyfikaty mają wiele zalet w porównaniu z kluczami logowania/hasła, ssh lub zamykaniem niezbędnych portów przez zaporę ogniową. Ale nie o to tu chodzi.

Na iOS procedura instalacji certyfikatu jest dość prosta (nie bez szczegółów), ale generalnie odbywa się to zgodnie z instrukcjami, których jest mnóstwo w Internecie i które są dostępne tylko dla przeglądarki Safari. Niestety Safari nie wie, jak używać Client Сert dla gniazd sieciowych, ale w Internecie jest wiele instrukcji, jak utworzyć taki certyfikat, ale w praktyce jest to nieosiągalne.

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

Aby zrozumieć websockety, zastosowaliśmy następujący plan: problem/hipoteza/rozwiązanie.

Problem: nie ma obsługi gniazd internetowych podczas przesyłania żądań proxy do zasobów chronionych certyfikatem klienta w przeglądarce mobilnej Safari dla systemu IOS i innych aplikacjach, które mają włączoną obsługę certyfikatów.

Hipotezy:

  1. Można skonfigurować taki wyjątek, aby używać certyfikatów (wiedząc, że ich nie będzie) do gniazd sieciowych zasobów wewnętrznego/zewnętrznego serwera proxy.
  2. W przypadku gniazd internetowych można nawiązać unikalne, bezpieczne i możliwe do obrony połączenie przy użyciu sesji tymczasowych generowanych podczas normalnego żądania przeglądarki (innego niż websocket).
  3. Sesje tymczasowe można realizować przy użyciu jednego serwera proxy (tylko wbudowane moduły i funkcje).
  4. Tymczasowe tokeny sesji zostały już zaimplementowane jako gotowe moduły Apache.
  5. Tymczasowe tokeny sesji można wdrożyć poprzez logiczne projektowanie struktury interakcji.

Stan widoczny po wykonaniu.

Cel: zarządzanie usługami i infrastrukturą powinno być dostępne z telefonu komórkowego na IOS bez dodatkowych programów (takich jak VPN), ujednolicone i bezpieczne.

Dodatkowy cel: oszczędność czasu i zasobów/ruchu telefonicznego (niektóre usługi bez gniazd sieciowych generują niepotrzebne żądania) dzięki szybszemu dostarczaniu treści w mobilnym Internecie.

Jak sprawdzić?

1. Strony otwierające:

— например, 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. Lub w konsoli programisty:

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

Testowanie hipotez:

1. Można skonfigurować taki wyjątek, aby używać certyfikatów (wiedząc, że ich nie będzie) do gniazd internetowych zasobów wewnętrznego/zewnętrznego proxy.

Znaleziono tutaj 2 rozwiązania:

a) Na poziomie

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

zmienić poziom dostępu.

Ta metoda ma następujące niuanse:

  • Weryfikacja certyfikatu następuje po wysłaniu żądania do zasobu proxy, czyli po uzgodnieniu żądania. Oznacza to, że serwer proxy najpierw załaduje, a następnie odetnie żądanie do chronionej usługi. To jest złe, ale nie krytyczne;
  • W protokole http2. Jest wciąż w wersji roboczej i producenci przeglądarek nie wiedzą, jak go zaimplementować #info o uzgadnianiu postu tls1.3 http2 (teraz nie działa) Zaimplementuj RFC 8740 „Korzystanie z TLS 1.3 z HTTP/2”;
  • Nie jest jasne, jak ujednolicić to przetwarzanie.

b) Na poziomie podstawowym zezwól na ssl bez certyfikatu.

SSLVerifyClient wymaga => SSLVerifyClient jest opcjonalny, ale zmniejsza to poziom bezpieczeństwa serwera proxy, ponieważ takie połączenie będzie przetwarzane bez certyfikatu. Możesz jednak dodatkowo odmówić dostępu do usług proxy za pomocą następującej dyrektywy:

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"

Więcej szczegółowych informacji znajdziesz w artykule o ssl: Uwierzytelnianie certyfikatu klienta serwera Apache

Przetestowano obie opcje, wybrano opcję „b” ze względu na jej wszechstronność i kompatybilność z protokołem http2.

Aby dokończyć weryfikację tej hipotezy, należało przeprowadzić wiele eksperymentów z konfiguracją, przetestowano następujące konstrukcje:

if = wymagaj = przepisz

Rezultatem jest następujący podstawowy projekt:

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>

Biorąc pod uwagę istniejącą autoryzację przez właściciela certyfikatu, ale z brakującym certyfikatem, musiałem dodać nieistniejącego właściciela certyfikatu w postaci jednej z dostępnych zmiennych SSl_PROTOCOL (zamiast SSL_CLIENT_S_DN_CN), więcej szczegółów w dokumentacji:

Moduł Apache mod_ssl

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

2. W przypadku websocket możesz nawiązać unikalne, bezpieczne i chronione połączenie, korzystając z sesji tymczasowych generowanych podczas normalnego żądania przeglądarki (innego niż websocket).

Bazując na wcześniejszych doświadczeniach, należy dodać do konfiguracji dodatkową sekcję, aby przygotować tymczasowe tokeny dla połączeń z gniazdem sieciowym podczas zwykłego żądania (innego niż gniazdo sieciowe).

#подготовка передача себе С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>

Testy wykazały, że to działa. Możliwe jest przesyłanie plików Cookies do siebie poprzez przeglądarkę użytkownika.

3. Sesje tymczasowe można realizować przy wykorzystaniu jednego serwera proxy (tylko wbudowane moduły i funkcje).

Jak dowiedzieliśmy się wcześniej, Apache ma całkiem sporo podstawowej funkcjonalności, która pozwala na tworzenie konstrukcji warunkowych. Potrzebujemy jednak środków, aby chronić nasze informacje, gdy znajdują się one w przeglądarce użytkownika, dlatego ustalamy, co i dlaczego przechowywać oraz z jakich wbudowanych funkcji będziemy korzystać:

  • Potrzebujemy tokena, którego nie da się łatwo rozszyfrować.
  • Potrzebujemy tokena, który ma wbudowaną dezaktualizację i możliwość sprawdzenia dezaktualizacji na serwerze.
  • Potrzebujemy tokena, który będzie powiązany z właścicielem certyfikatu.

Wymaga to funkcji mieszającej, soli i daty, aby token był starszy. Na podstawie dokumentacji Wyrażenia w serwerze Apache HTTP mamy to wszystko od razu po wyjęciu z pudełka sha1 i %{TIME}.

W rezultacie powstał taki projekt:

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

Cel został osiągnięty, jednak występują problemy ze starzeniem się serwerów (można użyć rocznego Cookie), co oznacza, że ​​tokeny, choć bezpieczne do użytku wewnętrznego, są niebezpieczne do użytku przemysłowego (masowego).

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

4. Tymczasowe tokeny sesji zostały już zaimplementowane jako gotowe moduły Apache.

Z poprzedniej iteracji pozostał jeden istotny problem – brak możliwości kontrolowania starzenia się tokenów.

Poszukujemy gotowego modułu, który zrobi to według słów: Apache token json two-factor auth

Tak, istnieją gotowe moduły, jednak wszystkie są powiązane z konkretnymi działaniami i posiadają artefakty w postaci rozpoczęcia sesji oraz dodatkowych plików Cookies. To znaczy, że nie przez jakiś czas.
Poszukiwania zajęły nam pięć godzin, ale nie dały konkretnego rezultatu.

5. Tymczasowe tokeny sesji można wdrożyć poprzez logiczne projektowanie struktury interakcji.

Gotowe moduły są zbyt skomplikowane, bo potrzebujemy tylko kilku funkcji.

Biorąc to pod uwagę, problem z datą polega na tym, że wbudowane funkcje Apache'a nie pozwalają na generowanie daty z przyszłości, a we wbudowanych funkcjach nie ma matematycznego dodawania/odejmowania podczas sprawdzania, czy nie są już nieaktualne.

Oznacza to, że nie można napisać:

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

Można porównać tylko dwie liczby.

Szukając rozwiązania problemu z Safari, znalazłem ciekawy artykuł: Zabezpieczanie HomeAssistanta certyfikatami klienta (działa z Safari/iOS)
Opisuje przykładowy kod w Lua dla Nginx, który jak się okazało bardzo mocno powtarza logikę tej części konfiguracji, którą już zaimplementowaliśmy, z wyjątkiem użycia metody solenia hmac do haszowania ( nie znaleziono tego w Apache).

Stało się jasne, że Lua jest językiem o przejrzystej logice i dla Apache’a można zrobić coś prostego:

Po przestudiowaniu różnicy z Nginx i Apache:

Oraz dostępne funkcje od producenta języka Lua:
22.1 – Data i godzina

Znaleźliśmy sposób na ustawienie zmiennych env w małym pliku Lua, aby ustawić datę z przyszłości i porównać ją z obecną.

Tak wygląda prosty skrypt 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

A tak to wszystko w sumie działa, z optymalizacją liczby Cookies i wymianą tokena, gdy upłynie połowa czasu do wygaśnięcia starego Cookie (tokena):

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 

Ponieważ LuaHookAccessChecker zostanie aktywowany dopiero po sprawdzeniu dostępu na podstawie informacji z Nginx.

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

Link do źródła Obraz.

Jeszcze jedna chwila.

Ogólnie rzecz biorąc, nie ma znaczenia, w jakiej kolejności zapisywane są dyrektywy w konfiguracji Apache (prawdopodobnie także Nginx), ponieważ ostatecznie wszystko zostanie posortowane na podstawie kolejności żądania od użytkownika, co odpowiada schematowi przetwarzania Skrypty Lua.

Ukończenie:

Stan widoczny po wdrożeniu (cel):
zarządzanie usługami i infrastrukturą jest dostępne z telefonu komórkowego na IOS bez dodatkowych programów (VPN), ujednolicone i bezpieczne.

Cel został osiągnięty, gniazda internetowe działają i mają poziom bezpieczeństwa nie mniejszy niż certyfikat.

Jak w ZeroTech połączyliśmy Apple Safari i certyfikaty klienta z websocketami

Źródło: www.habr.com

Dodaj komentarz