
Один из наших клиентов попросил разработать отказоустойчивое решение для организации защищенного доступа к его корпоративному сервису. Решение должно было:
- обеспечивать отказоустойчивость и избыточность;
- легко масштабироваться;
- просто и быстро решать задачу добавления и блокировки пользователей VPN;
- балансировать нагрузку между входными нодами;
- одинаково хорошо работать для клиентов на GNU/Linux, Mac OS X и Windows;
- поддерживать клиентов, которые находятся за NAT.
Готовых решений, удовлетворяющих всем поставленным условиям, не нашлось. Поэтому мы собрали его на базе популярных Open Source-продуктов, а теперь с удовольствием делимся полученным результатом в этой статье.
Разработка концепции
В качестве базовой VPN-технологии со стороны клиента мы выбрали OpenVPN: он прекрасно работает через NAT и поддерживает все требуемые платформы.
OpenVPN было решено развернуть в режиме TLS-сервера, а добавление и блокировку пользователей в нем сделать с помощью пакета easy-rsa, который позволяет создавать ключ и сертификат, а затем отзывать их при необходимости.
Сложнее всего было решить вопрос масштабирования, избыточности и отказоустойчивости.
Итоговое решение вышло простым и изящным. Мы решили использовать N входных нод, адреса которых с помощью round-robin DNS выдаются клиентам. Все ноды и узлы сервиса клиентов включены в единое L2-пространство tinc VPN. Клиентские подключения (тоже L2) объединяются с tinc-интерфейсом в мост. Таким образом, получается, что, подключаясь по OpenVPN, клиент попадает на случайную ноду и оказывается в единой L2-сети со всеми остальными клиентами, нодами и сервисом клиента.

Для реализации этой схемы были выделены 3 VPS в различных дата-центрах, на которых и требовалось развернуть «точки входа» в сеть (
ep1, ep2 и ep3). Кроме того, в сети присутствовал гипервизор с сервисами клиента (hpv1). На всех машинах установили Ubuntu Server 16.04.Строим tinc VPN
Для начала устанавливаем пакеты:
$ sudo apt-get update && sudo apt-get install tinс
На этом этапе нам нужно определиться с названием сети — пусть будет
l2vpnnet. Создаем структуру каталогов:$ sudo mkdir -p /etc/tinc/l2vpnnet/hosts
В каталоге
/etc/tinc/l2vpnnet создаем файл tinc.conf и наполняем его следующим содержимым:# Имя текущей машины Name = ep1 # Тип сети, в нашем случае — L2 Mode = switch # Интерфейс, который мы будем использовать Interface = tap0 # По умолчанию используется протокол UDP Port = 655 # Записываем имена всех остальных хостов, к которым мы будем подключаться ConnectTo = ep2 ConnectTo = ep3 ConnectTo = hpv1
Создаем файл
/etc/tinc/l2vpnnet/ep1 и вносим в него параметры:# Публичный адрес и порт Address = 100.101.102.103 655 # Используемые алгоритмы шифрования и аутентификации Cipher = aes-128-cbc Digest = sha1 # Для уменьшения задержек рекомендуем также выключать сжатие Compression = 0
Производим генерацию ключей. Традиционно мы используем ключи длиной 2 килобита: такая длина ключа обеспечивает хороший баланс между уровнем приватности и задержек (из-за накладных расходов на шифрование).
$ cd /etc/tinc/l2vpnnet && sudo tincd -n l2vpnnet -K2048 Generating 2048 bits keys: ............................................+++ p .................................+++ q Done. Please enter a file to save private RSA key to [/etc/tinc/l2vpnnet/rsa_key.priv]: Please enter a file to save public RSA key to [/etc/tinc/l2vpnnet/hosts/ep1]:
На остальных машинах проделываем аналогичные действия. Файлы с открытым ключом и параметрами подключения (
/etc/tinc/l2vpnnet/hosts/ep1|ep2|ep3|hpv1) необходимо разместить у всех участников сети в каталоге /etc/tinc/l2vpnnet/hosts.Название сети необходимо внести в файл
/etc/tinc/nets.boot, чтобы tinc запускал VPN к нашей сети автоматически при загрузке:$ sudo cat nets.boot #This file contains all names of the networks to be started #on system startup. l2vpnnet
При настройке как tinc VPN, так и OpenVPN в нашей компании принято использовать стандартные механизмы управления сетью Ubuntu. Добавим в
/etc/network/interfaces описание параметров устройства tap0:# Устройство запускается автоматически при старте системы auto tap0 # Указываем режим конфигурации manual, так как IP мы назначим уже на bridge iface tap0 inet manual # Создание устройства перед запуском tinc pre-up ip tuntap add dev $IFACE mode tap # ... и его удаление после остановки post-down ip tuntap del dev $IFACE mode tap # Собственно, запуск tinc с настроенной нами сетью tinc-net l2vpnnet
Такая настройка позволит нам управлять tinc с помощью ifup/ifdown-скриптов.
Для единого L2-пространства нужно выбрать и L3-пространство. Для примера мы будем использовать сеть
10.10.10.0/24. Настроим bridge-интерфейс и назначим ему IP — для этого внесем в /etc/network/interfaces такую информацию:auto br0 iface br0 inet static # Естественно, IP должен быть разным для хостов address 10.10.10.1 netmask 255.255.255.0 # Указываем, что в бридже наш интерфейс tinc vpn bridge_ports tap0 # Отключаем протокол spanning tree для bridge-интерфейса bridge_stp off # Максимальное время ожидания готовности моста bridge_maxwait 5 # Отключаем задержку при форвардинге bridge_fd 0
После этого последовательно стартуем оба устройства на всех серверах и проверяем связанность любым средством диагностики (ping, mtr и т.п.):
$ sudo ifup tap0 && sudo ifup br0 $ ping -c3 10.10.10.2 PING 10.10.10.2 (10.10.10.2) 56(84) bytes of data. 64 bytes from 10.10.10.2: icmp_seq=1 ttl=64 time=3.99 ms 64 bytes from 10.10.10.2: icmp_seq=2 ttl=64 time=1.19 ms 64 bytes from 10.10.10.2: icmp_seq=3 ttl=64 time=1.07 ms --- 10.10.10.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 1.075/2.087/3.994/1.349 ms
Отлично: L2-пространство для входных нод и целевого сервера построено. Теперь нужно добавить в него удаленных клиентов.
Настраиваем OpenVPN
Для начала устанавливаем необходимые пакеты на всех серверах:
$ sudo apt-get update && sudo apt-get install openvpn easy-rsa
Настроим DNS-зону, добавим 3 A-записи с одинаковым именем VPN-сервиса:
vpn.compa.ny. IN A 100.101.102.103 vpn.compa.ny. IN A 50.51.52.53 vpn.compa.ny. IN A 1.1.1.1
DNS будет выступать первым механизмом балансировки нагрузки в нашей системе. Согласно документации, OpenVPN разрешает имя точки подключения и будет последовательно совершать попытки подключиться ко всем IP, в которые разрешается имя. DNS при этом будет отдавать список IP в случайном порядке.
Вторым механизмом распределения нагрузки будет служить ограничение максимального количества подключений на один сервер. Предположим, у нас порядка 50 пользователей. С учетом избыточности, мы поставим ограничение в 30 пользователей на сервер и распределим пулы IP-адресов следующим образом:
Node 1 10.10.10.100-10.10.10.129 Node 2 10.10.10.130-10.10.10.159 Node 2 10.10.10.160-10.10.10.189
Создадим окружение для CA:
$ cd /etc/openvpn $ sudo -s # make-cadir ca # mkdir keys # chmod 700 keys # exit
Теперь отредактируем файл с переменными
vars, установив следующие значения:# Каталог с easy-rsa export EASY_RSA="`pwd`" # Путь к openssl, pkcs11-tool, grep export OPENSSL="openssl" export PKCS11TOOL="pkcs11-tool" export GREP="grep" # Конфиг openssl export KEY_CONFIG=`$EASY_RSA/whichopensslcnf $EASY_RSA` # Каталог с ключами export KEY_DIR="$EASY_RSA/keys" export PKCS11_MODULE_PATH="dummy" export PKCS11_PIN="dummy" # Размер ключа export KEY_SIZE=2048 # CA-ключ будет жить 10 лет export CA_EXPIRE=3650 # Описываем нашу организацию: страна, регион, # город, наименование, e-mail и подразделение export KEY_COUNTRY="RU" export KEY_PROVINCE="Magadan region" export KEY_CITY="Susuman" export KEY_ORG="Company" export KEY_EMAIL="info@compa.ny" export KEY_OU="IT" export KEY_NAME="UnbreakableVPN"
Сохраняем и начинаем генерацию ключей:
# . vars # ./clean-all # ./build-ca Generating a 2048 bit RSA private key ..........................+++ .+++ writing new private key to 'ca.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [RU]: State or Province Name (full name) [Magadan region]: Locality Name (eg, city) [Susuman]: Organization Name (eg, company) [Company]: Organizational Unit Name (eg, section) [IT]: Common Name (eg, your name or your server's hostname) [Company CA]: Name [UnbreakableVPN]: Email Address [info@compa.ny]: # ./build-dh Generating DH parameters, 2048 bit long safe prime, generator 2 This is going to take a long time … # ./build-key-server server # openvpn --genkey --secret keys/ta.key
Создадим тестового пользователя и сразу отзовем его сертификат, чтобы создать список отзыва:
# ./build-key testuser # ./revoke-full testuser
Скопируем все необходимые для настройки сервера ключи в каталог с ключевой информацией OpenVPN:
# cd keys # mkdir /etc/openvpn/.keys # cp ca.crt server.crt server.key dh2048.pem ta.key crl.pem /etc/openvpn/.keys # exit
Подготовим конфигурацию OpenVPN-сервера, для чего создадим файл
/etc/openvpn/server.conf:# Устанавливаем подробность ведения журнала verb 4 # Порт и протокол подключения port 1194 proto tcp-server # Режим и способ аутентификации mode server tls-server # Определяем MTU tun-mtu 1500 # Определяем имя и тип интерфейса, который будет обслуживать клиентов dev ovpn-clients dev-type tap # Указываем, что TA-ключ используется в режиме сервера key-direction 0 # Описываем ключевую информацию cert /etc/openvpn/.keys/server.crt key /etc/openvpn/.keys/server.key dh /etc/openvpn/.keys/dh2048.pem tls-auth /etc/openvpn/.keys/ta.key crl-verify /etc/openvpn/.keys/crl.pem # Определяем протоколы аутентификации и шифрования auth sha1 cipher AES-128-CBC # Опция, указывающая, что устройство будет создаваться единожды # на все время работы сервера persist-tun # Указываем тип топологии и пул topology subnet server-bridge 10.10.10.1 255.255.255.0 10.10.10.100 10.10.10.129 # Указываем маршрут по умолчанию через туннель и определяем # внутренние DNS push "redirect-gateway autolocal" push "dhcp-option DNS 10.10.10.200" push "dhcp-option DNS 10.20.20.200" # Проверяем доступность подключенного клиента раз в 10 секунд, # таймаут подключения — 2 минуты keepalive 10 120 # То самое ограничение в 30 клиентов max-clients 30 # Локальные привилегии демона openvpn user nobody group nogroup # Позволяет удаленному клиенту подключаться с любого IP и порта float # Путь к файлу журнала log /var/log/openvpn-server.log
Для второго и третьего сервера будем использовать тот же набор ключевой информации — конфигурационные файлы будут отличаться только пулом выдаваемых IP-адресов.
По аналогии с tinc настроим управление OpenVPN через стандартные ifup/ifdown-скрипты, добавив описание устройства в
/etc/network/interfaces:auto ovpn-clients iface ovpn-clients inet manual pre-up ip tuntap add dev $IFACE mode tap post-up systemctl start openvpn@server.service pre-down systemctl stop openvpn@server.service post-down ip tuntap del dev $IFACE mode tap
Включим интерфейс в мост вместе с tinc, изменив настройки интерфейса
br0:... netmask 255.255.255.0 bridge_ports tap0 bridge_ports ovpn_clients bridge_stp off ...
Приведем все в рабочее состояние:
$ sudo ifup ovpn-clients && sudo ifdown br0 && sudo ifup br0
Серверная конфигурация готова. Теперь создадим клиентские ключи и ovpn-файл:
$ sudo -s # cd /etc/openvpn/ca # ./build-key PetrovIvan # exit
Для упрощения использования мы создадим клиентский ovpn-файл c ключевой информацией INLINE:
$ vim PetrovInan.ovpn # Указываем тип подключения, тип устройства и протокол client dev tap proto tcp # Определяем MTU такой же, как и на сервере tun-mtu 1500 # Указываем узел и порт подключения remote vpn.compa.ny 1194 # Отказываемся от постоянного прослушивания порта nobind # Опция, которая позволяет не перечитывать ключи для каждого соединения persist-key persist-tun # Корректируем MSS mssfix # Указываем, что будем использовать TA как TLS-клиент key-direction 1 ns-cert-type server remote-cert-tls server auth sha1 cipher AES-128-CBC verb 4 keepalive 10 40 <ca> ### Сюда вставляем содержимое ca.crt </ca> <tls-auth> ### Сюда вставляем содержимое ta.key </tls-auth> <cert> ### Сюда вставляем содержимое PetrovIvan.crt </cert> <key> ### Сюда вставляем содержимое PetrovIvan.key </key>
Сохраняем и отдаем клиенту, который просто подключается к VPN, используя ovpn-файл. На этом настройка OpenVPN закончена.
Блокировка клиента
В случае, когда нам необходимо запретить подключение к VPN одному из клиентов (например, при увольнении сотрудника), мы просто отзываем сертификат:
$ ./revoke-all PetrovIvan
После отзыва обновляем на всех серверах
crl.pem и выполняем:$ sudo service openvpn reload
Обратите внимание, что в
server.conf отсутствует опция persist-key. Это позволяет обновить ключевую информацию во время выполнения reload — иначе бы для этого потребовался рестарт демона.Для распространения списка отзыва и выполнения действия
reload для OpenVPN мы используем Chef. Очевидно, для этой цели подойдут любые другие средства автоматического развертывания конфигураций (Ansible, Puppet…) или даже простой shell-скрипт.Кроме того, мы поместили каталог с CA в Git, что позволило и нам, и клиенту совместно работать с ключевой информацией, избегая коллизий.
Заключение
Конечно, описанное решение в ходе эксплуатации развивается. В частности, мы дописали простые скрипты, которые автоматически создают клиентские ovpn-файлы во время генерации ключей, а также работаем над системой мониторинга VPN.
Если у вас есть мысли о слабых местах в этом решении или идеи/вопросы по дальнейшему развитию конфигурации — буду рад увидеть их комментариях!
P.S.
Читайте также в нашем блоге:

