Programming, electronics, lifestyle

04 Feb 2022

Настройка OpenVPN и CA (центра сертификации) и отказ от docker-openvpn от kylemanna

docker-openvpn от kylemanna

И так вначале я использовал широко распространенное решение от kylemanna.

Оно представляет из себя Docker образ на базе Alpine состоящий из:

  1. OpenVPN собственно в качестве VPN сервера
  2. openvpn-auth-pam, google-authenticator, pamtester - дополнительные модули для авторизации в OpenVPN
  3. easy-rsa как инструмент для создания и работы центра сертификации (CA)
  4. Средства подготовки конфигурации сервера (ovpn_initpki, ovpn_genconfig, ovpn_copy_server_files, ovpn_run, ovpn_status)
  5. Ряд скриптов-оберток для управления пользователями (ovpn_getclient, ovpn_getclient_all, ovpn_listclients, ovpn_revokeclient, ovpn_otp_user)

Тут нужно сделать важное замечание, что я вначале не понимал как работает CA, сертификаты пользователей, OpenVPN и easy-rsa вместе. И, наверное, многим будет полезно тоже в этом разобраться.

Начнем с базового, что такое сертификат, центр сертификации и для чего они нужны.

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

Давайте разберем на примере ВУЗов. Скажем мы знаем топовые университеты с хорошей репутацией. И если на работу приходит выпускник с дипломом такого ВУЗа, то мы понимаем, что это хороший специалист, потому что мы доверяем конкретному университету.

В этом случае центром сертификации является университет, а сертификатом - диплом выпускника.

На этом принципе и построен алгоритм работы авторизации в OpenVPN. То-есть по сути OpenVPN сервер может ничего не знать о клиенте, тк даже не он “регистрирует” его, а доверяет он потому, что сертификат клиента подписан тем же CA (Certificate Authority), что и сертификат самого сервера. На мой взгляд это красивое решение.

OpenVPN использует ряд сертификатов и ключей:

  1. ca.crt – сертификат центра сертификации, которым у нас все подписано.
  2. server.crt – сертификат самого сервера, с помощью него клиенты могут удостовериться, что подключаются именно к тому серверу.
  3. server.key – ключ от сертификата выше, с помощью которого сервер и подписывает сообщения.
  4. dh.pem – специальный ключ для осуществления алгоритма Diffie Hellman, для создания нужен файл openssl-easyrsa.cnf генерируемый вместе с CA.
  5. ta.key – дополнительный симметричный ключ шифрования, который есть и на клиенте и на сервере (HMAC firewall). Создается с помощью OpenVPN, для защиты от DoS атак и флуда в UDP порт сервера.
  6. crl.pem – база данных отозванных сертификатов. Тк в общем случае любой сертификат подписанный CA, будет валиден на OpenVPN, для отключения доступа используется база данных отозванных сертификатов. После операции revoke в CA, в этот файл вносятся изменения (зависит от CA). Может быть задан с помощью параметра crl-verify.

В решении kylemanna в одном образе находится и центр сертификации, и сам VPN-сервер. Что подразумевает выпуск клиентских сертификатов на той же машине, что честно говоря убивает всю красоту и главное нарушает безопасность. Тк ключ от сертификата центра сертификации (crt.key) хранится вместе со всеми другими файлами, собственно как и файлы сертификатов и ключей клиентов.

Для работы с центром сертификации используется утилита easy-rsa. Данная утилита является надстройкой над openssl, которая упрощает операции с центром сертификации. Она использует директорию /etc/openvpn/pki для хранения всей информации по работе с центром сертификации: все сертификаты и их ключи. Как мы уже поняли, быть этого на самом сервере не должно.

В механизме подписей, есть возможность отделить создание клиентов от их подписи центром сертификации, делается это с помощью CSR-запросов. Поэтому при необходимости easy-rsa можно оставить в этом образе для создания и управления клиентами. В данном случае я не вижу необходимости в этом в данном решении.

Отдельную путаницу в /etc/openvpn создают директории:

  • /etc/openvpn/clients – здесь хранятся сертификаты, ключи клиентов и собранные воедино .ovpn файлы необходимые для подключения. Это директория не используются сервером.
  • /etc/openvpn/ccd – это специальный каталог, в котором хранятся файлы совпадающие по имени с именами клиентов, в каждом из файлов хранятся отдельные конфигурации и опции необходимые для работы клиента, например опция ifconfig-push 192.168.1.10 255.255.255.0, которая выдает конкретному клиенту IP адрес 192.168.1.10. Эта директория используется сервером. Задается с помощью параметра client-config-dir.

Моя реализация

Образ OpenVPN

Собственно основная идея заключается в том, чтобы вынести центр сертификации и обвязки для разворачивания и конфигурирования сервера из образа.

Получился следующий Dockerfile:

FROM alpine:latest

# Unused: openvpn-auth-pam google-authenticator pamtester easy-rsa
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
    apk add --update openvpn iptables bash && \
    rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*

Я убрал все лишнее. И оставил только необходимое:

  • openvpn.conf;
  • директорию ccd;
  • entrypoint.sh;
  • certs - папка с сертификатом и ключом сервера, сертификатом CA, ta.key, dh.key и листом отозванных сертификатов.

Отдельно пару слов стоит сказать про файл openvpn.conf. Тк я отказался от ovpn_genconfig я взял за основу базовый файл предлагаемый OpenVPN и внес туда только необходимые мне изменения, из плюсов теперь весь файл хорошо комментирован.

И еще отдельного внимания заслуживает файл entrypoint.sh. Дело в том, что в нём делается пару важных вещей:

  1. Создается /dev/net/tun устройство.

  2. Прописываются дополнительные маршруты и настройки iptables. Здесь все достаточно специфично и зависит от конкретного применения OpenVPN. У меня получился следующий файл:

    #!/bin/bash
    
    set -e
    
    mkdir -p /dev/net
    if [ ! -c /dev/net/tun ]; then
        mknod /dev/net/tun c 10 200
    fi
    
    ########################## ROUTES ##########################
    iptables -C POSTROUTING -t nat -s 192.168.255.0/24 -o eth0 -j MASQUERADE || {
    iptables -A POSTROUTING -t nat -s 192.168.255.0/24 -o eth0 -j MASQUERADE
    }
    
    iptables -A FORWARD -i tun0 -o tun0 -j ACCEPT
    iptables -A POSTROUTING -o tun0 -t nat -j MASQUERADE
    
    # Route to the docker-compose network
    iptables -A FORWARD -i tun0 -o eth0 -j ACCEPT
    iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT
    ip addr add 192.168.1.0/24 dev eth0
    ###################### END / ROUTES ########################
    
    echo "Running '${@}'"
    exec ${@}
    

Все вышеперечисленный файлы я храню в отдельной директории которую монтирую в контейнер. Для деплоя я использую Docker Compose, так я могу определить ряд сервисов помимо Openvpn и задать адресацию локальной сети через функционал Docker Compose.

version: "2.4"
services:
    openvpn:
        build:
            dockerfile: Dockerfile
            context: ./openvpn/docker
        restart: unless-stopped
        entrypoint: /etc/openvpn/entrypoint.sh
        command: openvpn --config /etc/openvpn/openvpn.conf
        cap_add:
            - NET_ADMIN
        volumes:
            - ./openvpn/bin:/usr/lib/openvpn/bin:ro
            - ./openvpn/etc:/etc/openvpn:ro
            - openvpn-logs:/tmp:rw
        ports:
            - 33896:33896/udp
        logging:
            driver: "json-file"
            options:
                max-size: "100m"
        networks:
            intranet:
                ipv4_address: 192.168.250.4
        cpus: 0.5
        mem_limit: 100m
        memswap_limit: 100m
volumes:
    openvpn-logs:
networks:
    intranet:
        driver: bridge
        ipam:
            config:
                - subnet: 192.168.250.0/24

Все файлы сертификатов и ключей генерируются и задаются один раз извне, из другого с центром сертификации. О чём – ниже.

Центр сертификации

Для этого я создал отдельный Docker образ:

FROM alpine:latest

RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
    apk add --update bash easy-rsa && \
    ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
    rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*

И Makefile для работы с этим образом:

default:
	docker run --rm -it -v $$(pwd)/mnt:/mnt -w /mnt -e EASYRSA=/mnt -e EASYRSA_PKI=/mnt/pki -e EASYRSA_EXT_DIR=/usr/share/easy-rsa/x509-types urpylka/easy-rsa:local

build:
	docker build ./docker --tag urpylka/easy-rsa:local

clean:
	docker image rm urpylka/easy-rsa:local

По сути три команды: make build – собирает образ, make – включает окружение внутри образа, ну и make clean – удаляет образ.

make – монтирует текущую директорию в /mnt контейнера.

Дальнейшие команды исполняются внутри этого окружения.

Инициализация pki директории для easy-rsa:

easyrsa init-pki

Далее создаем файл vars, в моём случае со следующим содержанием:

set_var EASYRSA_REQ_COUNTRY    "RU"
set_var EASYRSA_REQ_PROVINCE   "Samara Region"
set_var EASYRSA_REQ_CITY       "Samara"
set_var EASYRSA_REQ_ORG        "urpylka՚s projects!"
set_var EASYRSA_REQ_EMAIL      "[email protected]"
set_var EASYRSA_REQ_OU         "myworld"
set_var EASYRSA_REQ_CN         "urpylka՚s projects!"
set_var EASYRSA_BATCH          "yes"
set_var EASYRSA_ALGO           "ec"
set_var EASYRSA_DIGEST         "sha512"

Я использовал относительно новый алгоритм подписи основанный на эллиптических кривых ec вместо rsa.

Генерируем CRL файл:

easyrsa gen-crl

Создание клиента

  1. В начале нам нужно собрать и зайти в образ с easy-rsa:

    make build
    make
    
  2. Затем нужно задать CLIENTNAME.

    Если вы используете DNS, я советую создавать названия клиентов, соответствующие их доменным именам, это позволит избежать путаницы.

    CLIENTNAME="client.example.com"
    

    Это специальное название клиента используемое в сертификате, а также используется OpenVPN для задания отдельных правил через директорию /etc/openvpn/ccd.

    # /etc/openvpn/ccd/CLIENTNAME
    ifconfig-push 192.168.255.10 255.255.255.0
    
  3. Затем генерируем сертификат для клиента:

    easyrsa build-client-full ${CLIENTNAME} nopass
    
  4. Далее сохраним сгенерированный сертификат в файл с расширением ovpn.

    VPN_SERVER="vpn.example.com 1194"
    
    cat > ${CLIENTNAME}.ovpn << EOF
    client
    nobind
    dev tun
    remote-cert-tls server
    
    # Forward all traffic through VPN
    redirect-gateway def1
    
    # Address of the VPN server
    remote ${VPN_SERVER} udp
    
    <key>
    $(cat $EASYRSA_PKI/private/${CLIENTNAME}.key)
    </key>
    <cert>
    $(openssl x509 -in $EASYRSA_PKI/issued/${CLIENTNAME}.crt)
    </cert>
    <ca>
    $(cat $EASYRSA_PKI/ca.crt)
    </ca>
    key-direction 1
    <tls-auth>
    $(cat $EASYRSA_PKI/ta.key)
    </tls-auth>
    EOF
    

Отзыв сертификата

Для отзыва клиентского сертификата необходимо вызвать revoke. Затем скопировать файл crl.pem с сервера центра сертификации на OpenVPN сервер.

easyrsa revoke <CLIENTNAME>

Подробнее openvpn.net.

Итоги

В конце нужно заметить, что я пока не перенес функционал средств подготовки конфигурации сервера в образ easy-rsa. Это нужно прежде всего для удобства.

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

Источники

  1. How To Set Up and Configure an OpenVPN Server on Ubuntu 20.04
  2. How To Set Up and Configure a Certificate Authority (CA) On Ubuntu 20.04
  3. openvpn/wiki/HOWTO
  4. openvpn/wiki/EasyRSA3-OpenVPN-Howto
  5. Руководство по установке и настройке OpenVPN
  6. Простая настройка OpenVPN с фиксированными адресами клиентов
  7. Easy-RSA 3 Quickstart README
  8. Генерирование сертификатов для OpenVPN с помощью Easy-RSA 3