Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

Článek bude užitečný pro ty, kteří:

  • ví, co je to Client Cert, a chápe, proč potřebuje websockety na mobilním Safari;
  • Rád bych publikoval webové služby omezenému okruhu lidí nebo pouze sobě;
  • si myslí, že všechno už někdo udělal, a rád by udělal svět o něco pohodlnějším a bezpečnějším.

Historie webových soketů začala asi před 8 lety. Dříve se metody používaly ve formě dlouhých http požadavků (ve skutečnosti odpovědí): prohlížeč uživatele poslal požadavek na server a čekal, až něco odpoví, po odpovědi se znovu připojil a čekal. Pak se ale objevily webové zásuvky.

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

Před několika lety jsme vyvinuli vlastní implementaci v čistém PHP, která nemůže používat požadavky https, protože se jedná o linkovou vrstvu. Není to tak dávno, co se téměř všechny webové servery naučily zadávat proxy požadavky přes https a podporovat connection:upgrade.

Když k tomu došlo, staly se websockets téměř výchozí službou pro SPA aplikace, protože jak pohodlné je poskytovat obsah uživateli z iniciativy serveru (přenést zprávu od jiného uživatele nebo stáhnout novou verzi obrázku, dokumentu, prezentace kterou právě upravuje někdo jiný) .

Přestože klientský certifikát existuje již nějakou dobu, stále zůstává nedostatečně podporován, protože při pokusu o jeho obcházení vytváří mnoho problémů. A (možná :slightly_smiling_face: ) to je důvod, proč jej prohlížeče IOS (všechny kromě Safari) nechtějí používat a požadují jej z místního úložiště certifikátů. Certifikáty mají mnoho výhod ve srovnání s login/pass nebo ssh klíči nebo uzavřením potřebných portů přes firewall. Ale o tom to není.

Na iOS je postup instalace certifikátu celkem jednoduchý (ne bez specifik), ale obecně se to dělá podle návodů, kterých je na internetu spousta a které jsou dostupné pouze pro prohlížeč Safari. Safari bohužel neumí používat Client Сert pro webové sokety, ale na internetu je spousta návodů, jak takový certifikát vytvořit, ale v praxi je to nedosažitelné.

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

Abychom porozuměli webovým soketům, použili jsme následující plán: problém/hypotéza/řešení.

Problém: neexistuje žádná podpora webových soketů při odesílání požadavků na zdroje, které jsou chráněny klientským certifikátem, v mobilním prohlížeči Safari pro IOS a dalších aplikacích, které mají povolenou podporu certifikátů.

hypotézy:

  1. Je možné nakonfigurovat takovou výjimku pro použití certifikátů (s vědomím, že žádné nebudou) pro websockety interních/externích proxy zdrojů.
  2. U websocketů můžete vytvořit jedinečné, bezpečné a obhajitelné připojení pomocí dočasných relací, které jsou generovány během normálního (newebsocket) požadavku prohlížeče.
  3. Dočasné relace lze realizovat pomocí jednoho proxy webového serveru (pouze vestavěné moduly a funkce).
  4. Dočasné tokeny relace již byly implementovány jako hotové moduly Apache.
  5. Dočasné tokeny relace lze implementovat logickým navržením struktury interakce.

Viditelný stav po implementaci.

Objektivní: správa služeb a infrastruktury by měla být dostupná z mobilního telefonu na IOS bez dalších programů (např. VPN), jednotná a bezpečná.

Další cíl: úspora času a zdrojů/telefonního provozu (některé služby bez webových soketů generují zbytečné požadavky) s rychlejším doručováním obsahu na mobilním internetu.

Jak zkontrolovat?

1. Úvodní stránky:

— например, 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. Nebo ve vývojářské konzoli:

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

Testování hypotéz:

1. Je možné nakonfigurovat takovou výjimku pro použití certifikátů (s vědomím, že žádné nebudou) do webových soketů interních/externích proxy zdrojů.

Zde byla nalezena 2 řešení:

a) Na úrovni

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

změnit úroveň přístupu.

Tato metoda má následující nuance:

  • Ověření certifikátu nastane po požadavku na zdroj proxy, tj. po navázání spojení s žádostí. To znamená, že proxy nejprve načte a poté odřízne požadavek na chráněnou službu. To je špatné, ale ne kritické;
  • V protokolu http2. Je stále ve fázi návrhu a výrobci prohlížečů nevědí, jak jej implementovat #info about tls1.3 http2 post handshake (nyní nefunguje) Implementujte RFC 8740 "Using TLS 1.3 with HTTP/2";
  • Není jasné, jak toto zpracování sjednotit.

b) Na základní úrovni povolit ssl bez certifikátu.

SSLVerifyClient vyžaduje => SSLVerifyClient volitelné, ale to snižuje úroveň zabezpečení proxy serveru, protože takové připojení bude zpracováno bez certifikátu. Přístup ke službám proxy však můžete dále odepřít pomocí následující směrnice:

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"

Podrobnější informace naleznete v článku o ssl: Ověření klientského certifikátu serveru Apache

Obě možnosti byly testovány, volba „b“ byla vybrána pro svou univerzálnost a kompatibilitu s protokolem http2.

K dokončení ověření této hypotézy bylo zapotřebí mnoho experimentů s konfigurací; byly testovány následující návrhy:

if = vyžadovat = přepsat

Výsledkem je následující základní design:

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>

S přihlédnutím ke stávající autorizaci vlastníkem certifikátu, ale s chybějícím certifikátem, jsem musel přidat neexistujícího vlastníka certifikátu ve tvaru jedné z dostupných proměnných SSl_PROTOCOL (místo SSL_CLIENT_S_DN_CN), více podrobností v dokumentaci:

Modul Apache mod_ssl

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

2. U websocketů můžete vytvořit jedinečné, bezpečné a chráněné připojení pomocí dočasných relací, které jsou generovány během normálního (newebsocket) požadavku prohlížeče.

Na základě předchozích zkušeností musíte do konfigurace přidat další sekci, abyste mohli připravit dočasné tokeny pro připojení webového soketu během běžného požadavku (newebového soketu).

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

Testování ukázalo, že to funguje. Cookies je možné přenést na sebe prostřednictvím prohlížeče uživatele.

3. Dočasné relace lze realizovat pomocí jednoho proxy webového serveru (pouze vestavěné moduly a funkce).

Jak jsme zjistili dříve, Apache má poměrně hodně základních funkcí, které vám umožňují vytvářet podmíněné konstrukce. Potřebujeme však prostředky k ochraně našich informací, když jsou v prohlížeči uživatele, takže stanovíme, co a proč ukládat a jaké vestavěné funkce budeme používat:

  • Potřebujeme token, který nelze snadno dekódovat.
  • Potřebujeme token, který má v sobě zabudovanou zastaralost a schopnost kontrolovat zastaralost na serveru.
  • Potřebujeme token, který bude spojen s vlastníkem certifikátu.

To vyžaduje hašovací funkci, sůl a datum stárnutí tokenu. Na základě dokumentace Výrazy na serveru Apache HTTP Server máme to všechno po vybalení sha1 a %{TIME}.

Výsledkem byl tento design:

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

Cíle bylo dosaženo, ale dochází k problémům se zastaralostí serveru (lze použít rok starý soubor cookie), což znamená, že tokeny, přestože jsou bezpečné pro interní použití, jsou nebezpečné pro průmyslové (masové) použití.

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

4. Dočasné tokeny relace již byly implementovány jako hotové moduly Apache.

Z předchozí iterace zůstal jeden významný problém – nemožnost kontrolovat stárnutí tokenů.

Hledáme hotový modul, který to udělá podle slov: apache token json two factor auth

Ano, existují hotové moduly, ale všechny jsou svázány s konkrétními akcemi a mají artefakty v podobě zahájení relace a dalších cookies. Tedy na chvíli ne.
Hledání nám zabralo pět hodin, což nepřineslo konkrétní výsledek.

5. Dočasné tokeny relace lze implementovat logickým navržením struktury interakcí.

Hotové moduly jsou příliš složité, protože potřebujeme pouze několik funkcí.

Jak již bylo řečeno, problém s datem je v tom, že vestavěné funkce Apache neumožňují generování data z budoucnosti a při kontrole zastaralosti ve vestavěných funkcích neexistuje žádné matematické sčítání/odčítání.

To znamená, že nemůžete napsat:

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

Můžete porovnávat pouze dvě čísla.

Při hledání řešení problému Safari jsem našel zajímavý článek: Zabezpečení HomeAssistant pomocí klientských certifikátů (funguje se Safari/iOS)
Popisuje příklad kódu v Lua pro Nginx, který, jak se ukázalo, velmi opakuje logiku té části konfigurace, kterou jsme již implementovali, s výjimkou použití metody hmac salting pro hašování ( toto nebylo nalezeno v Apache).

Ukázalo se, že Lua je jazyk s jasnou logikou a pro Apache je možné udělat něco jednoduchého:

Po prostudování rozdílu s Nginx a Apache:

A dostupné funkce od výrobce jazyka Lua:
22.1 – Datum a čas

Našli jsme způsob, jak nastavit proměnné env v malém souboru Lua, abychom mohli nastavit datum z budoucnosti pro porovnání s aktuálním.

Takto vypadá jednoduchý skript 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 takto to celé funguje celkově, s optimalizací počtu souborů cookie a výměnou tokenu, když polovina času uplyne před vypršením platnosti starého cookie (tokenu):

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 

Protože LuaHookAccessChecker bude aktivován až po kontrole přístupu na základě těchto informací z Nginx.

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

Odkaz na zdroj Obraz.

Další bod.

Obecně je jedno, v jakém pořadí jsou direktivy napsány v konfiguraci Apache (pravděpodobně i Nginx), protože nakonec se vše seřadí na základě pořadí požadavku od uživatele, což odpovídá schématu zpracování skripty Lua.

Dokončení:

Viditelný stav po implementaci (cíl):
správa služeb a infrastruktury je dostupná z mobilního telefonu na IOS bez dalších programů (VPN), jednotná a bezpečná.

Cíle bylo dosaženo, webové zásuvky fungují a mají úroveň zabezpečení ne menší než certifikát.

Jak jsme v ZeroTech propojili Apple Safari a klientské certifikáty s websockety

Zdroj: www.habr.com

Přidat komentář