Cut-through Proxy своими силами

в 21:01, , рубрики: Cisco, iptables, linux, php, Серверное администрирование, метки: , , ,

У нас в компании используется много web-based приложений таких как jira и xwiki. Все они находятся в DMZ и регулярно обновляются для обеспечения должного уровня безопасности. Но на одной из конференций Cisco нашему начальству расказали о способе получения супер защищенной DMZ путем применения технологии Cut-through Proxy в устройствах Cisco ASA. После этого нам пришла команда от начальства включить данную функцию с поддержкой SSL и аутентификацией через LDAP.

Cut-through Proxy был настроен и работал около года. Из того что не понравилось это отсутствие возможности поменять внешний вид странички аутентификации, учетные данные пользователя не всегда передавались по https и отсутствие поддержки Google Chrome. (мы использовали Cisco ASA c прошивкой 8.3)

Но в один прекрасный день встала задача добавить еще один домен и еще один SSL сертификат на ASA и тут возникла очередная проблема: на ASA можно загрузить дополнительный сертификат но в Cut-through Proxy использовать его нельзя. Это привело к тому что при попадании на страницу аутентификации для второго домена браузер ругался на SSL сертификат. Но начальство настаивало на том чтобы Cut-through Proxy был включен для двух доменов.

Поскольку времени и желания возится с Cisco TAC у нас не было, было принято решение разработать собственное решение которое бы соответствовало следующим требованиям:

  • Решение должно дублировать функционал Cut-through Proxy
  • Решение должно поддерживать все популярные браузеры
  • Решение должно поддерживать несколько доменных имен
  • Решение должно поддерживать несколько SSL сертификатов
  • Решение должно работать в прозрачном режиме (чтобы не менять маршрутизацию)
  • Решение должно поддерживать active/standby режим
  • Решение должно поддерживать несколько LDAP серверов для отказоустойчивости
  • Решение должно работать в виртуальной машине
  • Решение должно передавать учетные данные (логин и пароль) по протоколу https

После определения с требований мы приступили к выбору платформы и наш выбор стал следующим:

  • Linux (Centos 5.7)
  • Iptables
  • Ebtables
  • Apache
  • PHP

В результате проделанной работы у нас получилось решение которое соответствовало всем требованиям которые были поставлены в техническом задании. Решение состоит из двух частей сетевой и программной:

  • Сетевая часть представлена связкой ebtables и iptables
  • Программная часть представлена PHP приложением которое выполняет аутентификацию пользователей через LDAP

Начнем описание решения с сетевой части
Она состоит из пара-виртуальной машины с CentOS 5.7 с 2 сетевыми интерфейсами. Для интеграции в существующую сеть мы завели дополнительный VLAN на свиче куда подключили интерфейс eth0 нашего proxy и маршрутизатор, а интерфейс eth0 подключили в VLAN с серверами получив такую схему:

[inet] -- [router]  -- [switch vlan 10] – [proxy] – [switch vlan 20] – [servers]

В данной схеме наш proxy должен выполнять функцию моста между VLAN 10 и VLAN 20 и для этого необходимо настроить bridge. Также при использовании bridge нет необходимости менять схему сети для того чтобы использовать proxy.

Устанавливаем bridge-utils и производим настройку bridge и присваиваем ip адрес для того чтобы можно было ходить на proxy по ssh и был доступ к LDAP серверам.

# yum install bridge-utils

# nano /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
ONBOOT=yes
TYPE=Ethernet
BRIDGE=br0
MTU=1500

# nano /etc/sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1
ONBOOT=yes
TYPE=Ethernet
BRIDGE=br0
MTU=1500

# nano /etc/sysconfig/network-scripts/ifcfg-br0
DEVICE=br0
BOOTPROTO=none
BROADCAST=
IPADDR=
NETMASK=
NETWORK=
GATEWAY=
ONBOOT=yes
TYPE=Bridge
DELAY=0
STP=off
MTU=1500

# /etc/init.d/network restart

# /usr/sbin/brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.762b7cbd8919       no              eth1
                                                        eth0

После выполнения данных настроек трафик начнет проходить через наш proxy. Но в такой конфигурации он выполняет функцию обычного свича, а нам необходимо настроить его таким образом чтобы при прохождении трафика на 80 и 443 порты по http перехватывалось подключение и обрабатывалось при помощи iptables. Но сделать перехват трафика проходящего через bridge средствами iptables нельзя поэтому для этих целей мы используем ebtables.

Устанавливаем ebtbles (пакет ebtables есть в репозитарии rpmforce):

# yum install ebtables

Настраиваем перехват трафика в bridge идущего на потры 80 и 443

# /sbin/ebtables -t broute -p IPv4 --ip-proto tcp --ip-dport 80 -j redirect
# /sbin/ebtables -t broute -p IPv4 --ip-proto tcp --ip-dport 443 -j redirect

Сохраняем конфигурацию ebtables и добавляем его в автозапуск

# /etc/init.d/ebtables save
# /sbin/chkconfig ebtables on

При данных настройка трафик идущий на порты 80 и 443 будет перехвачен и мы сможем производить с ним манипуляции помощи iptables.

Для дальнейшего понимания принципа работы нашего proxy небольшая теоретическая часть по iptables
Предположим нам нужно ограничить доступ для всех сервисов в DMZ по протоколу https. В iptables есть следующие три правила. Эти правила обрабатывают пакет на входе, еще до принятия решения о дальнейшей маршрутизации пакета:

# /sbin/iptables -t nat -I PREROUTING 1 --protocol tcp --dport 443 -m recent --update --seconds 3600 --name active --rsource -j ACCEPT
# /sbin/iptables -t nat -I PREROUTING 2 -j LISTIP
# /sbin/iptables -t nat -I PREROUTING 3 --protocol tcp -d <DMZ сеть> --dport 443 -j REDIRECT --to-ports 443

Первое правило пропускает пакеты без изменений, если source адрес находиться в специальном списке под названием «active». Адреса в этом списке хранятся только определенное время. При добавлении адреса в этот список, счетчик времени отсчитывает 3600 секунд и удаляет адрес со списка. Но если на протяжении 3600 секунд, пакет повторно придет с тем же source адресом, счетчик возвращается в первоначальное положение, и начинает отсчитывать время заново. Таким образом, если адрес источника есть в этом списке, и через наш сервер периодически приходят пакеты от него, то он будет в этом списке все время. Но, как только прекратится трафик от этого источника, на протяжении 1-го часа он будет удален со списка «active».
Если пакеты пришли от источника которого нет в списке «active», пакет передается второму правилу iptables. Второе правило ничего особенного не делает, кроме как пропускает пакет через дополнительную цепочку LISTIP. Назначение ее будет описано ниже.

Третье правило iptables перенаправляет все пакеты которые направляются в наш DMZ на 443 порт, на localhost нашего сервера, тоже на 443 порт. На localhost запущен Apache, который отдает страничку аутентификации. Если попытка аутентификация прошла неудачно все остается на своих местах, пакеты от этого источника все время будут редиректиться на localhost и в DMZ не попадут. Если же аутентификация прошла успешно, то IP адрес источника, с которого была произведена попытка, добавляется как раз в тот самый список «active», описанный выше. Но добавить его напрямую в список «active» нельзя. Для того чтобы он оказался в списке «active», нужно указать критерий, при выполнении которого он только тогда добавится. Этим критерием и есть простое правило, которое «ловит» следующие пакеты этого источника и тогда он оказывается в списке «active»:

# /sbin/iptables -t nat -A LISTIP --source <IP адресс> -m recent --name active --set -j ACCEPT

Другими словами когда мы добавляем это правило, мы как будто говорим:«Начиная с этого момента, если придут еще пакеты от заданного источника — поместить их в список „active“. Данное правило добавлется приложением которое выполняет аутентификацию пользователей. После того как source адрес попадет в список „active“ цепочку LISTIP нужно очистить для того чтобы нормально работал обратный отсчет времени и доступ блокировался по timeout.

Беря за основу изложенную выше теоретическую базу настраиваем наш proxy. Первым делом устанавливает и настраиваем web сервер исходя из требований:

  • Apache должен потреблять минимум ресурсов
  • Apache должен слушать адрес 127.0.0.1
  • Apache должен не использовать привязку в именам доменов
  • Apache должен в случае любых ошибок перенаправлял на /index.php
  • Apache должен для каждого домена с ssl иметь отдельный virtualhost на отдельном порту (это необходимо потому что мы не используем имена доменов)
  • Apache должен иметь обший DocumentRoot для всех virtualhost
  • Apache должен выполнять перенаправление на https

При такой конфигурации apache сможет отвечать на запросы для любых доменов в всегда показывать страничку аутентификации.

Например для всех url apache будет выдавать содержимое /index.php:
one.com/bla
one.com/blabla/bla/
two.com

Устанавливаем apache и php

# yum install httpd
# yum install php
# yum install php-ldap
# yum install mod_ssl

Настраиваем apache исходя из требований описаных выше

# nano /etc/httpd/conf/httpd.conf

ServerTokens Prod
ServerSignature Off

ServerRoot "/etc/httpd"
PidFile run/httpd.pid
Timeout 120
KeepAlive Off
MaxKeepAliveRequests 100
KeepAliveTimeout 15
HostnameLookups Off
UseCanonicalName Off

StartServers 8
MinSpareServers 5
MaxSpareServers 20
ServerLimit 256
MaxClients 256
MaxRequestsPerChild 4000

Listen 80

LoadModule authz_host_module modules/mod_authz_host.so
LoadModule include_module modules/mod_include.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule mime_module modules/mod_mime.soп
LoadModule vhost_alias_module modules/mod_vhost_alias.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule dir_module modules/mod_dir.so
LoadModule rewrite_module modules/mod_rewrite.so

Include conf.d/ssl.conf
Include conf.d/php.conf

User apache
Group apache

ServerAdmin noreply@domain.com

DocumentRoot "/var/www/html"

<Directory />
   Options FollowSymLinks
   AllowOverride None
</Directory>

<Directory "/var/www/html">
   Options Indexes FollowSymLinks
   AllowOverride None
   Order allow,deny
   Allow from all
</Directory>

<FilesMatch "^.ht">
   Order allow,deny
   Deny from all
   Satisfy All
</FilesMatch>

<FilesMatch "^.php">
   Order allow,deny
   Deny from all
   Satisfy All
</FilesMatch>

DirectoryIndex index.php

TypesConfig /etc/mime.types

DefaultType text/plain

<IfModule mod_mime_magic.c>
   MIMEMagicFile conf/magic
</IfModule>

HostnameLookups Off

ErrorLog logs/error_log
LogLevel warn
LogFormat "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"" combined
LogFormat "%h %l %u %t "%r" %>s %b" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
CustomLog logs/access_log combined

ErrorDocument 404 /index.php

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}


# nano /etc/httpd/conf.d/php.conf

<IfModule prefork.c>
 LoadModule php5_module modules/libphp5.so
</IfModule>

<IfModule worker.c>
 LoadModule php5_module modules/libphp5-zts.so
</IfModule>

AddHandler php5-script .php
AddType text/html .php
DirectoryIndex index.php

# nano /etc/httpd/conf.d/ssl.conf

LoadModule ssl_module modules/mod_ssl.so

Listen 443
Listen 444

AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl    .crl

SSLPassPhraseDialog  exec:/etc/httpd/ssl/startssl.pl
SSLSessionCache shmcb:/var/cache/mod_ssl/scache(512000)
SSLSessionCacheTimeout  300
SSLMutex default
SSLRandomSeed startup file:/dev/urandom  256
SSLRandomSeed connect builtin
SSLCryptoDevice builtin

<VirtualHost *:443>
      SSLEngine on

      SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
      SSLCertificateFile "/etc/httpd/ssl/one.com/server.crt"
      SSLCertificateKeyFile "/etc/httpd/ssl/one.com/server.key"
      SSLCertificateChainFile "/etc/httpd/ssl/one.com/gd_bundle.crt"
</VirtualHost>

<VirtualHost *:444>
      SSLEngine on

      SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
      SSLCertificateFile "/etc/httpd/ssl/two.com/server.crt"
      SSLCertificateKeyFile "/etc/httpd/ssl/two.com/server.key"
      SSLCertificateChainFile "/etc/httpd/ssl/two.com/gd_bundle.crt"
</VirtualHost>

После настройки запускаем apache и добавляем его в автозапуск

# /etc/init.d/httpd start
# /sbin/chkconfig httpd on

При такой конфигурации apache будет слушать порты 80 (http общий для всех доменов) 443 (https для домена one.com) и 444 (https для домена two.com)

После настройки apache можно приступить к настройке iptables. Допустим у нас web сервер которой обслуживает домен one.com имеет адрес 8.8.8.8, a домен two.com – 8.8.8.9

Конфигурация iptables будет иметь следующий вид:

# /sbin/iptables -t nat -A PREROUTING -j ACTIVE
# /sbin/iptables -t nat -A PREROUTING -d 8.8.8.8 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 443
# /sbin/iptables -t nat -A PREROUTING -d 8.8.8.8 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 80
# /sbin/iptables -t nat -A PREROUTING -d 8.8.8.9 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 444
# /sbin/iptables -t nat -A PREROUTING -d 8.8.8.9 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 80
# /sbin/iptables -t nat -A ACTIVE -p tcp -m tcp --dport 443 -m recent --update --seconds 3600 --name active --rsource -j ACCEPT
# /sbin/iptables -t nat -A ACTIVE -p tcp -m tcp --dport 80 -m recent --update --seconds 3600 --name active --rsource -j ACCEPT
# /sbin/iptables -t nat -A ACTIVE -j LISTIP

Сохраняем конфигурацию iptables и добавляем его в автозапуск

# /etc/init.d/iptables save
# /sbin/chkconfig iptables on

Добавляем в cron задание на очистку цепочки LISTIP

# nano /etc/crontab
*/1  * * * * root /sbin/iptables -t nat -F LISTIP

Посте окончания настройки наш proxy будет перенаправлять все подключения к нашим серверам по http и https на https страничку аутентификации до тех пор пока ip пользователя не попадет в цепочку LISTIP путем выполнения команды:

# /sbin/iptables -t nat -A LISTIP --source <IP адресс> -m recent --name active --set -j ACCEPT

На данном этапе настройка сетевой части закончена. Напоследок хотелось бы добавить что конфигурация active/standby осуществляется за счет приминения еще одной копии даного решения и использования Spanning Tree протокола. Который по умолчанию блокирует порт одного из proxy, а в случае выхода из строя рабочего proxy, будет задействован второй.

Приступим к программной части нашего proxy
Для начала необходимо определить требования требования к php приложению:

  • Приложение должно запомнить url по которому пытался пойти пользователь
  • Приложение должно запомнить ip адрес пользователя
  • Приложение должно поддерживать работу с несколькими LDAP серверами
  • Приложение должно в случае успешной аутентификации добавить ip пользователя в цепочку LISTIP и сделать перетаправление на сохраненый url

Для того чтобы не раздувать топик исходный код программной части выложен на Google project hosting.

Логика работы программной части заключается в следующем: при запросе на аутентификацию пользователю показывается форма для ввода логина и пароля данные с которой методом POST
передаются приложению поля:

username — имя пользователя
password — пароль
url — url по которому пытался пойти пользователь но был перехвачен.

<input name="username" type="text" maxlength="100" value="">
<input name="password" type="password" maxlength="100" value="">
<input name="url" type="hidden" value="https://one.com/">

И в случае успешной аутентификации ip пользователя добавляется на ~1 минуту в цепочку LISTIP, а потом и в список „active“, и производится перенаправление на url по которому пытался пойти пользователь до перехвата.

Перенаправление производится при помощи JavaScript потому что некоторые браузеры иногда
не реагируют на тег

 <meta>

На этом всё.
Спасибо за внимание!

P.S. На самом деле вначале все было сделано на FreeBSD 8.2 с использованием pf но она вела себя нестабильно на XenServer 5.6 поэтому пришлось портировать решение под Linux

Автор: sniffer

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js