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

  • обеспечивать отказоустойчивость и избыточность;
  • легко масштабироваться;
  • просто и быстро решать задачу добавления и блокировки пользователей 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.


Читайте также в нашем блоге:

Комментарии (21)


  1. aremdae
    26.09.2017 09:21

    У вас в конфигурации написано, что

    # Определяем MTU
    tun-mtu 1500


    Как при этом вы передаете пакеты больше 1500 через интернет? Или я чего-то не понимаю?


    1. gserge Автор
      26.09.2017 09:40
      +1

      Там Ethernet инкапсулируется в TCP (как в статье) или UDP и эта опция указывает МТU для инкапсулированного кадра.


  1. BasilioCat
    26.09.2017 09:25
    +1

    «Мы засунули твой VPN в VPN, чтобы… ». Не совсем понятна причина разнесения узлов с OpenVPN по каким-то VPS, при том, что «сервис клиента», к которому нужно получать доступ, находится в другом ДЦ/офисе, с одним или несколькими каналами. В это самое место и надо ставить OpenVPN. Если ДЦ несколько, с разными блоками адресов (но предположительно L2 VPN между ними), то ставить в каждый ДЦ OpenVPN.
    Выдавать адреса ДНСом с TTL 0 — ну в принципе не самая плохая затея, но неплохо бы проверять доступность узлов, и выкидывать их при авариях. Но все равно будут перерывы в доступности из-за кэширования ДНС записей.


    1. gserge Автор
      26.09.2017 09:38

      Нет, перерывов не будет, будет лишь задержка во время коннекта на таймаут подключения к недоступной ноде.


    1. gserge Автор
      26.09.2017 09:43

      По поводу разнесения нод на отдельных узлах — это было желание клиента. Сервис клиента в разных ДЦ, серверы также объединены по tinc, поэтому суть настройки не меняется.


      1. BasilioCat
        26.09.2017 10:28
        +1

        Не совсем ясно, зачем нужны дополнительные точки отказа, у клиента могут быть для этого какие-то свои причины, а может быть просто непонимание этого. Может пропасть связанность между VPS и ДЦ клиента, может отвалиться туннель tinc в ДЦ клиента. Обычно стараются терминировать VPN как можно ближе к оборудованию, к которому будет предоставлен доступ.


  1. livelace
    26.09.2017 09:56
    +1

    Статичное количество клиентов на сервер и «балансировка» с помощью DNS — это не самое лучшее решение. В своё время решал аналогичную задачу для 1k+ клиентов. Это долгая история, но стоит упомянуть, что клиент OpenVPN будет переподключаться к следующему сереверу из списка при получении неизвестного control message от сервера (магия), то есть можно организовать централизованное управление подключениями клиентов (со временем доступа, весами/weight для серверов, отозванными сертификатами, гео перераспределением и т.д. и т.п.).


    1. gserge Автор
      26.09.2017 10:07

      А где посмотреть подробности?


  1. livelace
    26.09.2017 10:27
    +1

    Я, в своё время, получил ответ на канале IRC openvpn-dev. Суть всего этого:

    1. Клиент соединяется с первым сервером из списка
    2. На сервере запускается скрипт проверки клиента, если клиент соотвествует — пропускаем, если нет — отлуп.
    3. Если отлуп пришел «понятный», то клиент мужественно пытается переподключиться к этому же серверу, если же отлуп «неизвестен», то клиент отчаявшись переходит к следующему серверу.

    В более старых версиях OpenVPN клиент при получении любого отлупа переходил к следующему серверу, но с какой-то версии разработчики поменяли поведение, поэтому стоит начать с release notes к продукту.


    1. gserge Автор
      26.09.2017 11:39

      спасибо


  1. Sergey78
    26.09.2017 11:24

    А зачем нужен tinc? Почему для связки входных нод с сервисом клиента не использовали тот же openvpn? Что умеет tinc, чего не может openvpn?


    1. gserge Автор
      26.09.2017 11:40
      +3

      tinc позволяет легко строить full-mesh vpn, OpenVPN гораздо хуже с этим справляется


      1. neumeika
        26.09.2017 21:44

        Он уже научился crypt не в user space и SMP и хотя полгигабита? и чё людям OSPF не учится…


  1. Jtu
    26.09.2017 16:02
    +1

    1. gserge Автор
      26.09.2017 16:07

      gcm решает?


      1. Jtu
        26.09.2017 16:13

        Пока обратного не доказали. Плюс разницы между 128/256 по производительности практически нет


  1. AlexGluck
    26.09.2017 17:33

    Флант, а чем вас softether не устроил? Открытый код, поддержка кластеризации, балансировка в кластере, отказоустойчивость. Лёгкое масштабирование, как линейное так и вертикальное. Встроенный функционал по связке с ldap\ad который и отвечает за блокировку юзеров. Клиенты одинаково хорошо работают для всех ОС, поддержка подавляющего числа vpn технологий чтобы встроенными в ос клиентами пользоваться.
    Поддержвка клиентов, которые находятся за NAT, которым заблокировали порты (работает на 443 порте основной протокол). Изобрели вы велосипед. Встроенный dhcp правда только на поиграться, но легко заменяется на любое другое решение(создаём tun устройство и вешаем на него dhcp или dhcp сервер по L2 подключаем к впн).


    1. gserge Автор
      27.09.2017 14:28

      Softether был одним из вариантов. Но в инфраструктуре клиента уже был tinc и было не важно, tinc+OpenVPN или tinc+softether.
      Вторая проблема — процесс конфигурации, который сложно автоматизировать.
      Третья, самая большая проблема: все вынесено в userspace, включая nat. softether — это решение само в себе, без интеграции в систему. Оно плохо управляется стандартными средствами администрирования. Поэтому мы его исключили при выборе.


      1. AlexGluck
        27.09.2017 18:14

        Автоматизировать конфигурацию конечно сложно, я вот из-за необходимости сейчас изучаю варианты, но пока всё очень грустно.
        NAT вынести из юзер спейса нет проблем, создаёте tun\tap устройство и подключаете это устройство как бридж в софтезер. На tun вешаете айпишник и рулите устройством как на опенвпн. tinc в случае с softether тоже лишний от него вообще можно избавиться.
        Хотя раз вы взяли что было тут всё становится на свои места.


  1. SchmeL
    27.09.2017 15:03

    Внесу свои 5 копеек, если часто пользоваться — заводить, отзывать сертификаты — то пользоваться через скрипты быстро надоедает. В свое время перешли на pfsense, где все это доступно через простой web интерфейс.


    1. gserge Автор
      27.09.2017 15:24

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