Русский
Русский
English
Статистика
Реклама

Блог компании zerotech

Магия 2-х строк на Lua или как донести исходные заголовки HTTP Authorization header-авторизации до web-сервиcа

17.08.2020 10:13:59 | Автор: admin
Статья будет полезна тем:

  • кому необходимо задействовать несколько видов авторизации в одном запросе к серверу;
  • кто хочет открывать сервисы мира Kubernetes/Docker в общий интернет, не задумываясь о способах защиты конкретного сервиса;
  • думает, что всё уже кем-то сделано, и хотел бы сделать мир немного удобнее и безопаснее.


Предисловие

Сервисы, которые становятся доступны через Kubernetes, имеют богатый набор способов авторизации. Один из наиболее модных это заголовок Authorization: Bearer это, например: JWT-авторизация (JSON Web Token) с передачей множества ключей, а следовательно, и значений, в одном заголовке. Встречаются и Basic-авторизации, например для Registry (хранилище образов Docker). Данная авторизация не использует Cookie и автоматически добавляется браузером (кроме Safari там есть нюансы, которые мы пока не решаем) ко всем запросам к серверу.



Проблема 1:

Не получается авторизоваться через Firefox & Safari в интерфейсе Storage OS, показывается Loader, и на этом всё.

Мини-гипотеза:

Проблема в проксировании. Быстрая проверка показала результат: если авторизация без использования проксирования (универсальный безопасный доступ по сертификату), то всё работает. Так в чём же дело?

Проанализировав сетевой стек, мы поняли, что используется заголовок Authorization, однако ранее, в ходе настройки проксирования сервисов Rancher, было выяснено, что этот заголовок передаётся проксируемому сервису и содержит данные авторизации по сертификату, поэтому было решено его просто удалять по завершении процесса авторизации (FakeBasicAuth).

Проблема 2:

Во многих web-серверах авторизация по персональному сертификату реализована через эмуляцию Basic-авторизации (по сути, вмешательство в запрос пользователя), вероятно с целью уменьшения изменений в основном коде web-сервера. Называется такой способ FakeBasicAuth. После установления такого заголовка web-сервером затирается заголовок Authorization, который приходит от пользователя.

Гипотезы:

1. Область видимости заголовка FakeBasicAuth поддаётся ещё большему ограничению, так что восстанавливается исходный заголовок для передачи на проксируемый ресурс таким образом, что будет передаваться только оригинальный заголовок, если он был.

2. Область видимости заголовка Authorization может быть сконструирована так, что заголовок будет сохранён до активации механизма FakeBasicAuth и восстановлен после.

Видимое состояние цель:

Storage OS авторизует, можно настраивать этот сервис, сохраняя единый подход к открытию доступности сервисов во внешний интернет.

Дополнительная цель:

Унифицированный, быстрый и безопасный доступ по http с сохранением функциональности всех возможных сервисов, работающих по стандарту http (например, REST API для мобильного приложения или Registry Docker).

Как проверить?

  • docker login registry-rancher.xxx.ru используя ключи и логин/пароль.
  • storageos-rancher.xxx.ru/#/login используя логин и пароль из конфигов secret rancher.xxx.ru/p/c-84bnv:p-qj9qm/secrets/kube-system:init-secr (не работает в Safari).
  • registry-ui-rancher.xxx.ru используя браузер и логин/пароль от Registry. Для внимательно читающих фишка: можно вместо стандартного docker login registry-rancher.xxx.ru использовать этот интерфейс там встроено проксирование к Registry.


Проверка гипотез:

1. Исходя из предыдущего опыта, попробуем найти способ в интернете по таким запросам: apache authentification external basic via cert.
Нашлась более-менее адекватная статья про ldap
вот таким образом:

RewriteEngine onRewriteCond %{IS_SUBREQ} ^false$RewriteCond %{LA-U:REMOTE_USER} (.+)RewriteRule . - [E=RU:%1]RequestHeader set REMOTE_USER %{RU}e


И далее прокинуть заголовок в дополнительных заголовках через конструкцию

RequestHeader add Authorization "expr=%{env:zt-auth-before}" "expr=%{env:zt-auth-before} =~/.{1,}/"


Но, к сожалению, эта конструкция не подразумевает создания идентичного заголовка, заголовку запроса пользователя, да и env формируется некорректно.
Поэтому способ на основе стандартных rewrite-ов оказался бесполезным и сложным.

2. Если не можем стандартно, значит, надо обратиться к lua, ранее видели, что есть блоки обработки запросов, которые выполняются до обработки сертификатов, снова посмотрим на блок-схему из статьи lua_load_resty_core и инструкцию module_lua_writinghooks c конструкцией early.

Получается, что мы можем задействовать тот же самый скрипт (Как мы в ZeroTech подружили Apple Safari и клиентские сертификаты с websocket-ами), чтобы сохранить заголовок Authorization до замены им на FakeBasicAuth.



LuaHookAccessChecker /usr/local/etc/apache24/sslincludes/websocket_token.lua handler early


В Lua это теперь выглядит так:

require 'apache2'function handler(r)        local fmt = '%Y%m%d%H%M%S'        local timeout = 3600 -- 1 hour        local auth = r.headers_in['Authorization']        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())        if auth ~= nil then                r.notes['zt-auth-before'] = auth        end        return apache2.OKend


Примечание:

Жирным шрифтом отмечены новые конструкции. И раз мы знаем, что env, полученный из Lua, доступен только для expr-выражений, добавляем конструкцию рядом с шифрованием zt-cert токена:

#исходящие Cookie пользователю

Header set Set-Cookie "expr=zt-cert=%{sha1:...


#передаём заголовки проксируемому сервису

RequestHeader add Authorization "expr=%{env:zt-auth-before}" "expr=%{env:zt-auth-before} =~/.{1,}/"


Доступность данных для передачи сервису проверяли через передачу данных обратно пользователю в браузер:

Header add Authorization "expr=%{env:zt-auth-before}" "expr=%{env:zt-auth-before} =~/.{1,}/"


Самое интересное здесь это способ проверки наличия данных, чтобы не передавать заголовок проксируемому сервису, если он не пришёл снаружи от браузера пользователя. За это отвечает вторая часть конструкции:

"expr=%{env:zt-auth-before} =~/.{1,}/"


Завершение:

Готовых решений в интернете на текущий момент нет, потрачено около трех часов на поиск и попытки проверить вариации, так как не хотелось изобретать велосипед.

Добавлено 5 строк, 3 из которых можно смело удалить.
Как вы думаете, какие? Пишите ваши варианты ответов в комментариях к статье.

Я не хотел писать об этом опыте, так как по-сути это всего 2 строки и заголовок Authorization дойдёт до адресата, но решил всё же поделиться информацией, так как здесь используется хороший багаж знаний предыдущих исследований о сертификатах (речь об этой статье habr.com/ru/company/zerotech/blog/509130). К тому же вряд ли найдутся смельчаки написать что-то своё и настолько простое на неизвестном языке.

Подробнее..

Как мы в ZeroTech подружили Apple Safari и клиентские сертификаты с websocket-ами

03.07.2020 10:08:54 | Автор: admin
Статья будет полезна тем, кто:
знает, что такое Client Cert, и понимает для чего ему websocket-ы на мобильном Safari;
хотел бы публиковать web-сервисы ограниченному кругу лиц или только себе;
думает, что всё уже кем-то сделано, и хотел бы сделать мир немного удобнее и безопаснее.

История веб-сокетов началась примерно 8 лет назад. Ранее использовались методы вида долгих http-запросов (на самом деле ответов): браузер пользователя отправлял запрос на сервер и ждал, пока он ему что-то ответит, после ответа подключался вновь и ждал. Но потом появились веб-сокеты.



Несколько лет назад мы разработали собственную реализацию на чистом php, которая не умеет использовать запросы https, так как это канальный уровень. Не так давно практически все web-серверы научились проксировать запросы по https и поддерживать connection:upgrade.

Когда это случилось, веб-сокеты стали практически сервисом по умолчанию у SPA-приложений, ведь как удобно предоставлять пользователю контент по инициативе сервера (передать сообщение от другого пользователя или загрузить новую версию изображения, документа, презентации, которую сейчас кто-то ещё редактирует).

Хотя Сlient Сert появился уже довольно давно, он всё ещё остаётся мало поддерживаемым, так как создаёт массу проблем с попытками его обойти. И (возможно :slightly_smiling_face: ) поэтому IOS-браузеры (все, кроме Safari) не хотят его использовать и запрашивать у локального хранилища сертификатов. Сертификаты обладают массой преимуществ по сравнению с ключами login/pass или ssh или закрытием через firewall нужных портов. Но речь не об этом.

На IOS процедура установки сертификата довольно проста (не без специфики), но в общем делается по инструкциям, которых в сети очень много и которые доступны только для браузера Safari. К сожалению, Safari не умеет использовать Сlient Сert для веб-сокетов, но в интернете есть множество инструкций, как сделать такой сертификат, но на практике это недостижимо.



Чтобы разобраться в веб-сокетах, мы использовали следующий план: проблема/гипотеза/решение.

Проблема: отсутствует поддержка веб-сокетов при проксировании запросов к ресурсам, которые защищены клиентским сертификатом на мобильном браузере Safari для IOS и других приложений, которые включили у себя поддержку сертификатов.

Гипотезы:
1. Возможно настроить такое исключение для использования сертификатов (зная, что их не будет) к веб-сокетам внутренних/внешних проксируемых ресурсов.
2. Для веб-сокетов можно сделать уникальное безопасное и защищаемое соединение с помощью временных сессий, которые генерируются при обычном (не веб-сокет) запросе браузера.
3. Временные сессии можно реализовать с помощью одного proxy web-сервера (только встроенные модули и функции).
4. Временные сессии-токены уже были реализованы в качестве готовых модулей apache.
5. Временные сессии-токены можно реализовать, логически спроектировав структуру взаимодействий.

Видимое состояние после внедрения.
Цель работы: управление сервисами и инфраструктурой должно быть доступно с мобильного телефона на IOS без дополнительных программ (таких как VPN), унифицировано и безопасно.

Дополнительная цель: экономия времени и ресурсов/трафика телефона (некоторые сервисы без веб-сокетов генерируют лишние запросы) с ускорением отдачи контента на мобильном интернете.

Как проверить?
1. Открытие страниц:
 например, 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. Или в консоли разработчика:


Проверка гипотез:
1. Возможно настроить такое исключение для использования сертификатов (зная, что их не будет) к веб-сокетам внутренних/внешних проксируемых ресурсов.

Тут было найдено 2 решения:
а) На уровне

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

менять уровень доступа.

У данного метода возникли такие нюансы:
Проверка сертификата происходит после запроса к проксируемому ресурсу, то есть post request handshake. Это означает, что прокси сначала нагрузит, а потом отсечёт запрос к защищаемому сервису. Это плохо, но не критично;
В протоколе http2. Он ещё находится в draft-е, и производители браузеров не знают, как его реализовать #info about tls1.3 http2 post handshake (not working now) Implement RFC 8740 Using TLS 1.3 with HTTP/2;
Непонятно, как унифицировать эту обработку.

б) На базовом уровне разрешить ssl без сертификата.
SSLVerifyClient require => SSLVerifyClient optional, но это снижает уровень защиты proxy-сервера, так как такое соединение будет обработано без сертификата. Однако можно далее запретить доступ к проксируемым сервисам такой директивой:

RewriteEngine        onRewriteCond     %{SSL:SSL_CLIENT_VERIFY} !=SUCCESSRewriteRule     .? - [F]ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"


Более подробная информация в статье о ssl: Apache Server Client Certificate Authentication

Оба варианта были проверены, выбран вариант б за универсальность и совместимость с протоколом http2.

Для завершения проверки этой гипотезы потребовалось немало экспериментов с конфигурацией, были проверены конструкции:
if = require = rewrite
Apache Core Features
Expressions in Apache HTTP Server

Получилась следующая базовая конструкция:
SSLVerifyClient optionalRewriteEngine onRewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESSRewriteCond %{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>


С учётом существующей авторизации по владельцу сертификата, но при отсутствующем сертификате пришлось добавить несуществующего владельца сертификата в виде одной из доступных переменных SSl_PROTOCOL (вместо SSL_CLIENT_S_DN_CN), подробнее в документации:
Apache Module mod_ssl



2. Для веб-сокетов можно сделать уникальное безопасное и защищаемое соединение с помощью временных сессий, которые генерируются при обычном (не веб-сокет) запросе браузера.

Исходя из предыдущего опыта нужно добавить дополнительную секцию в конфигурацию, чтобы при обычном (не веб-сокет) запросе готовить временные токены для веб-сокет соединений.

#подготовка передача себе С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 checkSetEnvIf Cookie "websocket-allowed=(.*)" env-var-name=$1#or rewrite ruleRewriteCond %{HTTP_COOKIE} !^.*mycookie.*$#or if<If "%{HTTP_COOKIE} =~ /(^|; )cookie-name\s*=\s*some-val(;|$)/ ></If</If></If>


Проверка показала, что это работает. Возможно через пользовательский браузер передавать cебе Cookie.

3. Временные сессии можно реализовать с помощью одного proxy web-сервера (только встроенные модули и функции).
Как мы выяснили ранее, у Apache довольно много core-функциональности, которая позволяет создавать условные конструкции. Однако нам нужны средства защиты нашей информации, пока она находится в пользовательском браузере, поэтому устанавливаем, что и для чего хранить, и какие встроенные функции будем задействовать:
Нужен такой токен, который не поддаётся простому декодированию.
Нужен такой токен, в котором зашито устаревание и возможность проверки устаревания на сервере.
Нужен такой токен, который будет связан с владельцем сертификата.

Для этого нужна функция хеширования, соль и дата для устаревания токена. Исходя из документации Expressions in Apache HTTP Server у нас есть всё это из коробки sha1 и %{TIME}.

Получилась такая конструкция:
#нет сертификата, и обращение к 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>


Цель достигнута, но есть проблемы с серверным устареванием (можно использовать Cookie годичной давности), а значит токены, хоть и безопасны для внутреннего использования, но небезопасны для промышленного (массового).



4. Временные сессии-токены уже были реализованы в качестве готовых модулей Аpache.

С предыдущей итерации осталась одна существенная проблема невозможность контролировать устаревание токена.

Ищем готовый модуль, который это делает, по словам: apache token json two factor auth
Client authentication using tokens based on JSON Web Tokens
Apache Two-Factor (2FA) Authentication
How to Add Two-Factor Authentication to Apache
Bring two-factor authentication to your Apache instance with a simple module install

Да, готовые модули есть, но все привязаны к конкретным действиями и обладают артефактами в виде старта сессии и дополнительных Cookie. То есть не на время.
У нас ушло пять часов на поиск, который не дал конкретного результата.

5. Временные сессии-токены можно реализовать, логически спроектировав структуру взаимодействий.

Готовые модули слишком сложны, ведь нам нужно только пару функций.
При этом проблема с датой в том, что встроенные функции Apache не позволяют генерировать дату из будущего, а при проверке устаревания во встроенных функциях нет математического сложения/вычитания.
То есть нельзя написать:

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

Можно сравнивать только два числа.

При поиске обхода проблемы Safari нашлась интересная статья: Securing HomeAssistant with client certificates (works with Safari/iOS)
В ней описан пример кода на Lua для Nginx, и который, как оказалось, очень повторяет логику той части конфигурации, которую мы уже ранее реализовали, за исключением использования hmac-метода расстановки соли для хеширования (такого в Apache не нашлось).

Стало понятно, что Lua это язык, с понятной логикой, возможно сделать что-то простенькое и для Apache:
LuaHookAccessChecker Directive
UnsetEnv Directive

Изучив разницу с Nginx и Apache:
modules_lua
lua_load_resty_core

И доступные функции от производителя языка Lua:
22.1 Date and Time

Найден способ задания переменных env в небольшом Lua-файле для того, чтобы установить дату из будущего для сверки с текущей.

Вот так выглядит простенький 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.OKend


И вот так это всё работает в сумме, c оптимизацией числа Cookie и заменой токена при наступлении половинного времени до истечения старых Cookie (токена):
SSLVerifyClient optional#LuaScope thread#generate event variables zt-cert-date-nextLuaHookAccessChecker /usr/local/etc/apache24/sslincludes/websocket_token.lua handler early#запрещаем без сертификата что-то ещё, кроме webscoketRewriteEngine onRewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESSRewriteCond %{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 


Потому что LuaHookAccessChecker будет активирован только после проверок доступа исходя из этой информации от Nginx.


Cсылка на источник изображения.

Ещё один момент.
В целом неважно, в какой последовательности в конфигурации Аpache (вероятно и Nginx) написаны директивы, так как в итоге всё будет отсортировано исходя из очерёдности прохождения запроса от пользователя, который соответствует схеме для отработки Lua-скриптов.

Завершение:
Видимое состояние после внедрения (цель):
Управление сервисами и инфраструктурой доступно с мобильного телефона на IOS без дополнительных программ (VPN), унифицировано и безопасно.

Цель достигнута, веб-сокеты работают и обладают не меньшим уровнем безопасности, чем сертификат.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru