Один из наших клиентов попросил разработать отказоустойчивое решение для организации защищенного доступа к его корпоративному сервису. Решение должно было:
- обеспечивать отказоустойчивость и избыточность;
- легко масштабироваться;
- просто и быстро решать задачу добавления и блокировки пользователей 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 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.
Читайте также в нашем блоге:
- «Наш рецепт отказоустойчивого Linux-роутера»;
- «Настройка основного и двух резервных операторов на Linux-роутере с NetGWM».
Автор: gserge