Маршрутизация силами Haproxy, DoH, GeoIP, защита сервисов через mTLS и выгрузка метрик в Prometheus, настройка ACME.SH

от автора

Так же будет переосмысление предыдущих статей на тему маршрутизации VLESS+TLS VLESS-REALITY

Данная статья так же будет объединением предыдущих статей про маршрутизацию REALITY методом steal-oneself(укради себя) и переосмысляет их. Первые три можно прочитать тут 1 2 3. Перейдем на unix socket взамен использования портов, установим панель 3X-UI на сервер без докера от пользователя xray, подключим подписку. Так же дам пример как через Haproxy развернуть сертификат для ocserv или любого другого сервиса для сквозной маршрутизации.

Версии используемого ПО Ubuntu 24.04 LTS и Haproxy 2.8 из репозитория, конфиг подготовлен для этой версии, на версиях выше может потребоваться доработка, версия XCA 2.8, на 2.9 идентично.

В первой части статьи установим Haproxy настроим выдачу сертификатов без перезапуска Haproxy через acme.sh, настроим базовую маршрутизацию, базово подготовим Apache2 для работы за обратным прокси, подготовим УЦ, настроим mTLS, и встроенный экспортер Prometheus в Haproxy.

Часть 1. Базовый конфиг и сертификаты LE

Работаю от root командой sudo su
Обновим список пакетов и обновим систему:
apt update && apt upgrade -y
Установим необходимое ПО:
apt install -y haproxy htop socat netcat-traditional apache2
Поменяем стандартный 80 порт apache2 на 8080 и отключим модули SSL:
Отрываем конфиг nano /etc/apache2/ports.conf и приводим к виду как на примере ниже

Listen 8080  #<IfModule ssl_module> #Listen 443 #</IfModule>  #<IfModule mod_gnutls.c> #Listen 443 #</IfModule>

Перезапускаем apache2 systemctl restart apache2

Установим acme.sh и подготовим директорию для сертификатов.
Создадим пользователя acme:
adduser --system --disabled-password --disabled-login --home /var/lib/acme --quiet --force-badname --group acme
Добавим его в группу haproxy:
adduser acme haproxy
Создадим директорию для файлов acme:
mkdir /usr/local/share/acme.sh/
Перейдем в tmp загрузим acme.sh перейдем в директорию:
cd /tmp/ && git clone https://github.com/acmesh-official/acme.sh.git && cd acme.sh/
Установим acme.sh без cron в созданную ранее директорию:
./acme.sh --install --no-cron --no-profile --home /usr/local/share/acme.sh
Сделаем симлинк для удобства:
ln -s /usr/local/share/acme.sh/acme.sh /usr/local/bin/
Изменим права на директорию acme.sh:
chmod 755 /usr/local/share/acme.sh/
Создадим директорию для хранения сертификатов:
mkdir /etc/haproxy/certs
Изменим владельца:
chown haproxy:haproxy /etc/haproxy/certs
Выставим права:
chmod 770 /etc/haproxy/certs

Теперь создадим аккаунт в acme.sh, далее все действия выполняются от пользователя acme:
sudo -u acme -s
acme.sh --register-account --server letsencrypt -m example@mail.com
Изменим УЦ для выдачи по умолчанию ZeroSSL не работает с RU:
acme.sh --set-default-ca --server letsencrypt
Выполним acme.sh --update-account
Полученный ACCOUNT_THUMBPRINT нам потребуется далее для настройки Haproxy.
Завершим работу с пользователем командой exit

Приступим к подготовке конфигурации Haproxy
Сотрем имеющуюся конфигурацию командой > /etc/haproxy/haproxy.cfg
Откроем файл nano /etc/haproxy/haproxy.cfg
Настроим секцию Global, в замен «*ACCOUNT_THUMBPRINT*» вставьте полученный ранее отпечаток на стадии acme.sh —update-account, уровень безопасности выберите сами, для совместимости я использую tls1.2 на фронтенде отдельно можно установить tls1.3

global         log /dev/log    local0         log /dev/log    local1 notice         chroot /var/lib/haproxy         stats socket /var/run/haproxy/admin.sock level admin mode 660         setenv ACCOUNT_THUMBPRINT '*ACCOUNT_THUMBPRINT*' # поменять на полученный из acme         stats timeout 30s         user    haproxy         group   haproxy         daemon     # https://ssl-config.mozilla.org/     # Улучшенная безопастность и совместимость со старыми браузерами ( Intermediate ) Supports Firefox 27, Android 4.4.2, Chrome 31, Edge, IE 11 on Windows 7, Java 8u31, OpenSSL 1.0.1, Opera 20, Safari 9         ssl-default-bind-curves X25519:prime256v1:secp384r1         ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305         ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256         ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.2 no-tls-tickets          ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305         ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256         ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets      # TLS 1.3 современная безопасность без отката к TLS 1.2  ( Modern ) Supports Firefox 63, Android 10.0, Chrome 70, Edge 75, Java 11, OpenSSL 1.1.1, Opera 57, Safari 12.1 #        ssl-default-bind-curves X25519:prime256v1:secp384r1 #        ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 #        ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.3 no-tls-tickets  #        ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 #        ssl-default-server-options ssl-min-ver TLSv1.3 no-tls-tickets          ssl-dh-param-file /etc/haproxy/dh4096.pem # openssl dhparam -out /etc/haproxy/dh4096.pem 4096 #        tune.ssl.default-dh-param 2048       # Тюнинг http/2         tune.h2.initial-window-size 536870912 # Увеличиваем начальный размер окна для входящих и исходящих соединений.         tune.h2.fe.max-concurrent-streams 512 # Установим кол-во одновременных потоков на входящие соединений.         tune.h2.be.max-concurrent-streams 512 # Установим кол-во одновременных потоков на исходящие соединений. 

Сгенерируем DH командой: openssl dhparam -out /etc/haproxy/dh4096.pem

Настроим секции defaults и resolvers для разрешения доменных имен на backend’е

defaults        log global        mode http        option httplog        option dontlognull        timeout connect 40s        timeout client  1m        timeout server  1m        timeout tunnel 1h        timeout http-request 30s        errorfile 400 /etc/haproxy/errors/400.http        errorfile 403 /etc/haproxy/errors/403.http        errorfile 408 /etc/haproxy/errors/408.http        errorfile 500 /etc/haproxy/errors/500.http        errorfile 502 /etc/haproxy/errors/502.http        errorfile 503 /etc/haproxy/errors/503.http        errorfile 504 /etc/haproxy/errors/504.http  resolvers dnsserver         nameserver ns1 127.0.0.1:53         parse-resolv-conf         resolve_retries       3         timeout resolve       1s         timeout retry         1s         hold other           30s         hold refused         30s         hold nx              30s         hold timeout         30s         hold valid           10s         hold obsolete        30s

Настроим http фронтенд (порт 80)

frontend f_http         bind *:80         bind [::]:80         mode http      # отклоним невалидные юзер агенты         http-request reject if { req.hdr(user-agent) -m len le 32 }         http-request reject if { req.hdr(user-agent) -m sub evil }      # ответим на запрос ACME         http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/'}         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         http-request redirect scheme https code 301 unless { ssl_fc }

Теперь настроим TCP фронтенд на 443 порту для SSL Passthrough, а так же сделаем доступным SSH на 443 порту.

frontend f_tcp         bind *:443    # ipv4 SSL Passthrough         bind [::]:443 # ipv6 SSL Passthrough         mode tcp         option tcplog         tcp-request inspect-delay 3s      # Ограничим частоту запросов         stick-table type ip size 1m expire 10s store conn_cur         tcp-request session track-sc0 src         tcp-request session reject if { sc_conn_cur(0) gt 240 }         tcp-request content capture req.ssl_sni len 10         tcp-request content accept if { req_ssl_hello_type 1 } or !{ req_ssl_hello_type 1 }      # Правила сопостовления          use_backend tcp-ssh if !{ req.ssl_hello_type 1 } { payload(0,7) -m bin 5353482d322e30 } or !{ req.ssl_hello_type 1 } { req.len 0 } #use_backend tcp-ssh if !{ req.ssl_hello_type 1 } { req.len 0 }          use_backend tcp_to_https if { req.ssl_sni -i one.example.ru } 

Сразу же добавим https фронтед

frontend f_https #          bind abns@frontendhttps.sock accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 strict-sni tfo         http-request reject if { req.hdr(user-agent) -m sub evil }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         http-response set-header X-Content-Type-Options nosniff         http-response set-header X-XSS-Protection 1;mode=block         http-response set-header X-Frame-Options SAMEORIGIN         http-after-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;" # Устанавливаем HSTS         stick-table type ip size 100k expire 5m store http_req_rate(10s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 300 }         http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path /https-status-443 }
Справка *_sni

req.ssl_sni — анализирует заголовок SNI для маршрутизации без SSL-терминации трафика и сквозной передачи зашифрованного трафика на бекенд.
ssl_fc_sni — Проводит SSL-терминацию трафика для передачи на http бекенды.

Теперь настало время для backend секции

backend tcp-ssh # Бекенд для SSH         mode tcp         option http-keep-alive         timeout http-keep-alive 30s         server ssh 127.0.0.1:22 check port 22 backend tcp_to_https #          mode tcp         server s1 abns@frontendhttps.sock send-proxy-v2-ssl-cn tfo check          retry-on conn-failure empty-response response-timeout 

Проверим конфигурацию haproxy haproxy -f /etc/haproxy/haproxy.cfg -c
Если ответ Configuration file is valid перезапустим командой systemctl restart haproxy

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

Начнем получать сертификаты, переключимся на пользователя acme:
sudo -u acme -s
Создадим первый запрос на сертификат для домена one:
acme.sh --issue -d one.example.ru --stateless --ecc
Произведем развертывания сертификата через deploy-hook выполним:

DEPLOY_HAPROXY_HOT_UPDATE=yes \ DEPLOY_HAPROXY_STATS_SOCKET=/var/run/haproxy/admin.sock \ DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs \ acme.sh --deploy -d one.example.ru --deploy-hook haproxy

Таким методом развертываете необходимое кол-во сертификатов, теперь можно установить задачу в crontab на обновление сертификатов командой acme.sh --install-cronjob .
Проверить сертификат можно командой:
echo "show ssl cert /etc/haproxy/certs/one.example.ru" | nc -U /var/run/haproxy/admin.sock | grep Status
Если при выполнении вы видите ‘Status: Used’ значит все прошло успешно, теперь можно перейти по адресу one.example.ru/https-status-443 и увидеть Status 200 OK! Your IP (Ваш IP), значит все работает, и будем приступать к следующему этапу, защите по списками FireHol и GeoIP по GeoIP так же можно маршрутизировать трафик для клиентов из разных стран или континентов.

Часть 2. Списки FireHol, GeoIP и настройка rate limit

Подготовим директории для хранения списков
mkdir /etc/haproxy/geoip /etc/haproxy/firehol /etc/haproxy/scripts
Напишем скрипт для загрузки firehol, и преобразуем его в чистый список IP и подсетей,
nano /etc/haproxy/scripts/firehol.sh

#!/bin/bash  # Определяем директории TMP_DIR="/tmp/firehol" DEST_DIR="/etc/haproxy/firehol"  # Создаем временную директорию mkdir -p "$TMP_DIR"  # Скачиваем файлы urls=(     "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset"     "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset"     "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level3.netset"     "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_abusers_1d.netset" )  files=("level1.acl" "level2.acl" "level3.acl" "abusers_1d.acl")  for i in "${!urls[@]}"; do     curl -s "${urls[$i]}" -o "$TMP_DIR/${files[$i]%.acl}.netset" done  # Очищаем файлы от комментариев и пустых строк for file in "${files[@]}"; do     netset_file="$TMP_DIR/${file%.acl}.netset"     acl_file="$TMP_DIR/$file"     grep -vE '^\s*#|^\s*$' "$netset_file" > "$acl_file" done  # Генерируем md5 для файлов declare -A md5s for file in "${files[@]}"; do     md5s["$file"]=$(md5sum "$TMP_DIR/$file" | awk '{ print $1 }') done  # Проверяем наличие файлов в целевой директории и обновляем их при необходимости for file in "${files[@]}"; do     dest_file="$DEST_DIR/$file"     if [[ ! -f "$dest_file" ]] || [[ "$(md5sum "$dest_file" | awk '{ print $1 }')" != "${md5s[$file]}" ]]; then         cp "$TMP_DIR/$file" "$dest_file"         echo "Обновлен файл: $dest_file"     fi done  chown -R haproxy:haproxy /etc/haproxy/firehol/ # Удаляем временную директорию rm -rf "$TMP_DIR" 

Данный скрипт сформирует 4 файла в директории /etc/haproxy/firehol

Теперь добавим GeoIP списки IPv4+IPv6
nano /etc/haproxy/scripts/geoip.sh

#!/bin/bash # https://github.com/Loyalsoldier/geoip/blob/release/GeoLite2-Country-Locations-en.csv  GEOIP_ACL="/etc/haproxy/geoip" GEOIP_TEMP="/tmp"   function DownloadDB () {         # Очистим старые файлы:         rm -rf  $GEOIP_TEMP/{*.csv,*.zip}          curl -o $GEOIP_TEMP/geip_csv.zip https://raw.githubusercontent.com/Loyalsoldier/geoip/release/GeoLite2-Country-CSV.zip && \         unzip -j $GEOIP_TEMP/geip_csv.zip -d $GEOIP_TEMP "*IPv4.csv" "*en.csv" "*IPv6.csv"          # Переименуем csv:         find $GEOIP_TEMP -depth -type f -name '*IPv4.csv' -exec mv {} $GEOIP_TEMP/ips4.csv \;         find $GEOIP_TEMP -depth -type f -name '*IPv6.csv' -exec mv {} $GEOIP_TEMP/ips6.csv \;         cat $GEOIP_TEMP/ips4.csv $GEOIP_TEMP/ips6.csv > $GEOIP_TEMP/ips.csv         find $GEOIP_TEMP -depth -type f -name '*en.csv' -exec mv {} $GEOIP_TEMP/countres.csv \; }  function CreateAcl () {         if [ ! -d $GEOIP_ACL ]; then                 mkdir -p $GEOIP_ACL         fi          while read line; do                 COUNTRY_CODE=$(echo $line | cut -d, -f5)                 COUNTRY_ID=$(echo $line | cut -d, -f1)                  CONTINENT_CODE=$(echo $line | cut -d, -f3)                 CONTINENT_ID=$(echo $line | cut -d, -f1)                  # Создадим списки                 cat $GEOIP_TEMP/ips.csv | grep $COUNTRY_ID | cut -d, -f1 > $GEOIP_ACL/$COUNTRY_CODE.list                 cat $GEOIP_TEMP/ips.csv | grep $CONTINENT_ID | cut -d, -f1 > $GEOIP_ACL/$CONTINENT_CODE.acl         done <$GEOIP_TEMP/countres.csv }  DownloadDB CreateAcl rm -rf  $GEOIP_TEMP/{*.csv,*.zip} chown -R haproxy:haproxy /etc/haproxy/geoip/ systemctl restart haproxy 

Данный скрипт сформирует файлы со списком IP/подсетей по странам и континентам в директории /etc/haproxy/geoip

Данные скрипты добавим в crontab для обновления например раз в сутки в 4ч утра:
0 4 * * * /etc/haproxy/scripts/firehol.sh && /etc/haproxy/scripts/geoip.sh && systemctl restart haproxy >/dev/null 2>&1

Настроим rate limit и подключим списки firehol, в секцию f_http добавим следующие строки после mode http, rate limit выставим на значение 30, после 30 запросов в течении 10 секунд, будет отклонять трафик с IP который превысил ограничение, в зависимости от сложности сайта limit нужно будет корректировать, для 80 порта ставлю меньше, т.к. там только acme и redirect на https.
через tcp-request connection silent-drop if подключим списки firehol, content будет сохранять для дальнейшего анализа, connection оборвет соединение без записи в журнал.
frontend f_http должен теперь выглядеть вот так:

Справка по tcp-request

tcp-request connection — работает на этапе установления соединения, прежде чем оно будет установлено, и если выполняется reject условие, немедленно завершает попытку подключения это полезно для защиты от явных IP адресов скамеров/спамеров и для защиты от DDoS, должно идти первым правилом.
tcp-request session — работает с установленной сессией TCP, если определенные условия выполняются. Например, для отклонения о превышении rate limit, немедленно завершает установленную сессию.
tcp-request content — используется для отклонения конкретного содержимого, которое передается в рамках уже установленных соединений. Это может быть полезно, для фильтрации определенных данных, например, запреты на конкретные запросы или паттерны.

Так же отличаются и действия:
reject — отклоняет соединение но отправляет клиенту ответ о завершении.
silent-drop — делает тоже что и reject только «тихо» без ответа клиенту.
accept — соответственно разрешает подключение по указанному правилу.
Установив ‘!‘ перед правилом например { src -f /etc/haproxy/geoip/RU.list }, оно будет инвертировано.

frontend f_http         bind *:80         bind [::]:80         mode http      # FireHOL IP List         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level1.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level2.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level3.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/abusers_1d.acl }       # Ограничение запросов, дропаем на раннем этапе для http фронта         stick-table type ip size 1m expire 10s store conn_cur         tcp-request session track-sc0 src         tcp-request session reject if { sc_conn_cur(0) gt 30 } 

Теперь добавим в TCP фронтенд ограничение в 240 запросов за 10секунд, и списки firehol
frontend f_tcp должен теперь выглядеть вот так:

frontend f_tcp         bind *:443    # ipv4 SSL Passthrough         bind [::]:443 # ipv6 SSL Passthrough         mode tcp         option tcplog         tcp-request inspect-delay 3s      # FireHOL IP List         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level1.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level2.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level3.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/abusers_1d.acl }       # Ограничим частоту запросов         stick-table type ip size 1m expire 10s store conn_cur         tcp-request session track-sc0 src         tcp-request session reject if { sc_conn_cur(0) gt 240 }         tcp-request session capture req.ssl_sni len 10         tcp-request content capture req.ssl_sni len 10         tcp-request content accept if { req_ssl_hello_type 1 } or !{ req_ssl_hello_type 1 } 

Про GeoIP блокировку ее добавить можно так, в секцию f_http и f_tcp внести следующую строку после firehol списков:
tcp-request connection reject if { src -f /etc/haproxy/geoip/AF.acl } тем самым в данном примере заблокировав все подсети Африканского континента, полный список континентов и стран доступен тут, установив ‘!’ перед { src -f /etc/haproxy/geoip/AF.acl } разрешите трафик только от данного региона, и запретите весь остальной, в скрипте есть разделение, континенты с расширением acl, а файлы стран с расширением list.

Маршрутизация по странам настраивается аналогичным образом пример для TCP:
use_backend tcp_ru { req.ssl_sni -i example.ru } { src -f /etc/haproxy/geoip/RU.list }
В данном примере на домен example.ru смогут войти только клиенты с IP входящих в файл RU.list. можно пойти иным путем, например:
use_backend tcp_ru if { src -f /etc/haproxy/geoip/RU.list }
В данном варианте если клиент подключается с IP из Российского списка он будет отправлен на соответствующий backend tcp.

use_backend tcp_by if { src -f /etc/haproxy/geoip/BY.list }
А в данном примере клиенты с IP Белоруссии уйдут на соответствующий backend tcp.
Всех остальных которые не прошли по правилам отправим на дефолтный бекенд:
default_backend default_tcp

Для работы соответственно потребуется создать соответствующие бэкенды перед запуском.

Все тоже самое можно проделать на HTTP фронтэнде, на примере заблокируем клиентов из Белоруссии явно сообщив им об этом ошибкой 403 и текстом и их IP адресом:
http-request deny deny_status 403 content-type text/plain lf-string "Access denied, IP not from whitelist. Your IP %[src]." if { src -f /etc/haproxy/geoip/BY.list }

Или отправим клиентов из России на один бэкенд, а клиентов из Белоруссии на второй, и установим default_backend на тот случай если их IP нет в списках стран, пример:
Для России use_backend http_ru if { src -f /etc/haproxy/geoip/RU.list }
Для Белоруссии use_backend http_by if { src -f /etc/haproxy/geoip/BY.list }
И для тех кто не прошел по правилам стран:
default_backend http_default

Для Haproxy Enterprise (HAPEE) данные действия не требуются, есть модули MaxMind и Digital Element, которые упрощают работу с GeoIP.

Часть 3. mTLS и настройка УЦ для клиентских сертификатов

Первым делом загрузим XCA для удобства управления и выдачи сертификатов, я использую портативную версию, подготовим ROOTCA и SUBCA

При запуске XCA создайте БД, после открытия базы, сообщит что шифрование не безопасно, перейдите в «изменить» и поменяйте на «AES-256-CBC», так же в настройках измените алгоритм подписи по умолчанию на «SHA384» и отключите устаревшие расширения Netscape.

Настройка базы данных XCA

Настройка базы данных XCA

Создадим корневой сертификат для подписи, для этого перейдите в «Сертификаты» и «Новый сертификат»

Инструкция в картинках

Создадим корневой сертификат, которым подпишем промежуточный.

Первоисточник

Первоисточник

Настройте первоисточник, по умолчанию будет самозаверенный, алгоритм подписи SHA384 и шаблон выберите «CA»

Субъект

Субъект

Заполните субъект, в данном случае только внутреннее имя и CN нажмите сгенерировать ключ

Создание ключа

Создание ключа

Тип ключа выберете EC, и кривую prime256v1 или secp384r1

Расширения

Расширения

Настроим расширения, Установим длину цепочки т.е. сколько может быть промежуточных сертификатов, становим Key identifier, настроим срок действия, и SAN, через «редактировать» добавить «Copy CN»

Применения ключа

Применения ключа

Настроим применения ключа, выставим Critical. Можно сохранять

Теперь выдадим промежуточный

Первоисточник

Первоисточник

Теперь в «Подписание» укажем что хотим подписать данный сертификат нашим только что созданным корневым сертификатом ROOTCA.

Субъект

Субъект

Заполните субъект, создайте ключ по аналогии с корневым сертификатом.

Расширения

Расширения

Настройте расширения, идентично как в ROOTCA, с единственным отличием, промежуточный ЦС не будет выдавать сертификаты УЦ, для этого установите длину цепочки как 0

Применение ключа

Применение ключа

Аналогично ROOTCA

Теперь создадим ключ клиента и подпишем созданным SUBCA

Первоисточник

Первоисточник

Заполняем первоисточник, выбираем для подписи SUBCA и шаблон TLS_client

Субъект

Субъект

Заполним субъект, в CN пишем наименование клиента, в данном примере client, а во внутреннее имя добавлю имя домена, для удобства поиска, создаем ключ.

Расширения

Расширения

Обязательно в расширениях ставим все галочки Key indetifier, и так же SAN копируем CN

Применение ключа

Применение ключа

Установим значения critical.

Теперь можно экспортировать сертификат клиента в формате цепочки PKCS#12 *.pfx

Экспорт

Экспорт

Теперь необходимо установить сертификат в «Личное» для Windows, и для Android в «Сертификаты VPN и приложений»

Теперь необходимо сохранить ROOTCA и SUBCA в файл pem по отдельности, и в один файл, назовем его fullca.pem

CRL

CRL

Теперь необходимо получить файл revoked.crl, для этого, на SUBCA кликаем ПКМ>ЦС>Сгенерировать CRL, для удобства выставляйте пару лет.

Аналогичные действия и для ROOTCA

Экспортируем SUBCA и ROOTCA в файл revoked.crl именно в таком порядке.

Теперь когда у нас есть файлы ROOTCA.pem, SUBCA.pem, fullca.pem и revoked.crl можем приступать к настройке фронтенда.

В секцию f_tcp под tcp_to_https добавим:
use_backend tcp_to_mtls if { req.ssl_sni -i auth.example.ru }
Теперь добавим backend

backend tcp_to_mtls         mode tcp         server mtls abns@frontendmtls.sock send-proxy-v2-ssl-cn tfo check         retry-on conn-failure empty-response response-timeout

Настроим фронтенд

frontend f_mtls         bind abns@frontendmtls.sock tfo accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 ssl-min-ver TLSv1.3 strict-sni verify required ca-file /etc/haproxy/ca/SUBCA.pem ca-verify-file /etc/haproxy/ca/fullca.pem crl-file /etc/haproxy/ca/revoked.crl crt-ignore-err 10,23          http-request reject if { req.hdr(user-agent) -m sub evil }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         stick-table type ip size 100k expire 5m store http_req_rate(30s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 60 }         http-after-response set-header Strict-Transport-Security "max-age=16000000;"         http-request return status 403 content-type text/plain lf-string "Your certificate has expired, please contact the administrator" if { ssl_c_verify 10 } #text/html file /certexpired.html         http-request return status 403 content-type text/plain lf-string "Your certificate has been revoked, please contact the administrator" if { ssl_c_verify 23 } #text/html file /certrevoked.html         http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path /mtls-status-443 }
frontendmtls

verify required — отвечает за обязательную проверку сертификата клиента.
ca-file — файл промежуточного сертификата которым подписаны клиенты.
ca-verify-file — включает в себя корневой сертификат и промежуточный (добавлен в HAProxy 2.2 ), отправляет клиенту более короткий список CA в сообщении SERVER HELLO, который будет использоваться для проверки.
crt-ignore-err — позволяет отвечать клиенту кодом ошибки если код «10» — сертификат истек или еще не наступил, «23» — сертификат отозван.
ssl-min-ver TLSv1.3 — явно указываем TLSv1.3 для защиты клиентского сертификата без отката к TLSv1.2

Создадим для хранения наших файлов директорию:
mkdir /etc/haproxy/ca
Переместим файлы SUBCA.pem fullca.pem и revoked.crl в директорию удобным для вас способом.
Выставим права и владельца:
chmod 700 /etc/haproxy/ca/
chown -R haproxy:haproxy /etc/haproxy/ca/

Проверяем конфигурацию командой haproxy -f /etc/haproxy/haproxy.cfg -c
Если все ок, перезапускаем сервис systemctl restart haproxy

Проверяем через браузер, заходим на auth.example.ru и если у вас появился запрос сертификата а после предоставления сертификата получаете «Status 200 OK! Your IP» значит все сделано правильно. можете добавлять свои пути/домены для маршрутизации, домены соответственно нужно будет добавить и f_tcp например так:
use_backend tcp_to_mtls if { req.ssl_sni -i auth.example.ru } || { req.ssl_sni -i auth2.example.ru }
Или добавить под tcp_to_mtls аналогичную строку только с другим доменом.

Часть 4. Метрики

Подключим выгрузку метрик в Prometheus через встроенный экспортер в Haproxy, для этого есть два пути, 1. через путь в виде { path_beg /path/ } и один домен или через доп домен, так же защитим наш сервис паролем либо mTLS создав дополнительный SUBCA для метрик.

Выгрузка через второй домен, добавляем в f_tcp под tcp_to_mtls
use_backend tcp_to_metrics if { req.ssl_sni -i metrics.example.ru }

Теперь добавим frontend для метрик и секцию с паролем и разрешим подключаться только с Российских IP, в идеале разрешить только подсети вашего провайдера или если есть белый статичный IP разрешить его (метрики так же можно скрыть за mTLS):

frontend f_metrics         bind abns@metricshttps.sock accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 strict-sni tfo          tcp-request connection accept if { src -f /etc/haproxy/geoip/RU.list }          http-request reject if { req.hdr(user-agent) -m sub evil }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         stick-table type ip size 100k expire 5m store http_req_rate(30s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 60 }         http-after-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;"         http-response set-header X-Content-Type-Options nosniff         http-response set-header X-XSS-Protection 1;mode=block         http-response set-header X-Frame-Options SAMEORIGIN         stats enable         stats uri /path/stats         stats realm Haproxy\ Statistics         stats refresh 10s         stats show-legends         stats hide-version         http-request use-service prometheus-exporter if { path_beg /haproxy-exporter-path/ } userlist metrics         user metrics insecure-password secretpassword

Метрики будут доступны по адресу https://metrics.example.ru/haproxy-exporter-path/metrics
с логином metrics и паролем secretpassword, для Grafana использую панель Haproxy 2 Full (ID 12693)

Так же, легким движением рук можем изменить доступ по паролю на доступ по сертификату для этого подготовим промежуточный УЦ и сделаем клиента для Prometheus по аналогии с mTLS frontend, пример конфигурации:

frontend f_metrics_mtls         bind abns@metricshttps.sock tfo accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 ssl-min-ver TLSv1.3 strict-sni verify required ca-file /etc/haproxy/ca/metrics.pem ca-verify-file /etc/haproxy/ca/metricsfull.pem crl-file /etc/haproxy/ca/metrics.crl crt-ignore-err 10,23         tcp-request connection accept if { src -f /etc/haproxy/geoip/RU.list }          http-request reject if { req.hdr(user-agent) -m sub evil }          http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         stick-table type ip size 100k expire 5m store http_req_rate(30s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 90 }         http-after-response set-header Strict-Transport-Security "max-age=16000000;"         http-request return status 403 content-type text/plain lf-string "Your certificate has expired, please contact the administrator" if { ssl_c_verify 10 }          http-request return status 403 content-type text/plain lf-string "Your certificate has been revoked, please contact the administrator" if { ssl_c_verify 23 }          http-request use-service prometheus-exporter if { path_beg /haproxy-exporter-path/ } # exporter haproxy, ссылка для prometheus ( /path_beg/metrics )  #        use_backend node-exporter if { path_beg -i /node-exporter-path/ } # exporter сервера

Пример конфигурации Prometheus для авторизации по сертификату:

  - job_name: 'Haproxy'     scheme: https     static_configs:       - targets: ['mdata.example.ru:443']     metrics_path: /mypath/metrics     tls_config: #       ca_file: 'ca.crt'        cert_file: 'client.crt'        key_file: 'client.key'

Добавить node-exporter со стандартной установкой на 9100 порту можно так:
В фронтенд метрик добавить use_backend node-exporter if { path_beg -i /node-exporter-path }
Теперь добавим backend, перед передачей на бекенд удалим префикс пути:

backend node-exporter         mode http         http-request replace-path /node-exporter-path(/)?(.*) /\2         server s1 127.0.0.1:9100

Теперь добавим Web статистику вставим следующий например перед фронтедом метрик:

frontend f_stats         bind abns@stats.sock         stats enable         stats uri /stats         stats realm Haproxy\ Statistics         stats refresh 10s         stats show-legends         stats hide-version         stats show-modules backend stats         mode http         http-request replace-path /haproxy(/)?(.*) /\2         server s1 abns@stats.sock

Теперь добавим во фронтенд mTLS правило:
use_backend stats if { path_beg -i /haproxy/ }
Статистика будет доступна по адресу https://auth.example.ru/haproxy/stats

Примеры из Grafana
HTTP

HTTP
Connections

Connections

Часть 5. 3X-UI, Reality TCP, XHTTP и DoH

Для начала создадим пользователя и группу для работы панели
adduser --system --disabled-password --disabled-login --home /usr/local/x-ui --quiet --force-badname --group xray

Для установки выполним:
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
При установке меняем порт на 49004 или удобный вам.
После завершения скрипт покажет текущую конфигурацию для подключения, адрес пути и логин с паролем, сохраняем пригодиться позже.

После установки проведем настройку:
1. Откроем юнит x-ui на редактирование nano /etc/systemd/system/x-ui.service
Необходимо после [Service] добавить следующие строки:
User=xray
Group=haproxy

2. Создадим директорию для Socket: mkdir /var/lib/haproxy/xui
3. Изменим владельца файлов x-ui и директории для Socket:
chown -R xray:xray /etc/x-ui/ chown -R xray:xray /usr/local/x-ui/ chown -R xray:haproxy /var/lib/haproxy/xui
4. Перечитаем конфигурацию systemd: systemctl daemon-reload
5. Перезапустим сервис x-ui и сразу проверим статус работы:
systemctl restart x-ui.service && systemctl status x-ui.service

Перейдем к настройке Haproxy:
В mTLS фронтенд добавляем
use_backend http_3xui if { path_beg /defaultpath/ } || { path_beg /3xui-path }
В defaultpath вставляем путь выданный панелью при установке, можно оставить как есть, во второе правило, которое придумаете сами.
Добавим backend:

backend http_3xui # Админ панель 3X-UI         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-Proto https if { ssl_fc }         server s1 127.0.0.1:49004 check port 49004

Перезапускаем haproxy и проверяем, если панель открылась перейдем к настройке подключений, создадим TCP REALITY:

Пример настроек inbound

1.Укажем удобное примечание например: tcp_to_reality
2.Протокол: VLESS
3.Порт не меняем
4.Протокол(транспорт): TCP(RAW)
5.Proxy Protocol: Enable
6.Sockopt: Enable
7.TCP Fast Open: Enable
8.TCP Congestion: bbr
9.Tproxy: off
10.External Proxy: enable
[Тот же] [drive.example.ru] [443]Безопасность: Reality
11.Xver: 1
12.uTLS на ваш выбор, у меня firefox
13.Dest (Target): /var/lib/haproxy/fakehttps1.sock
14.SNI: drive.example.ru
15.Sniffing: включить по умолчанию.

Конфигурация1

Конфигурация1
Конфигурация 2

Конфигурация 2

Настроим HAProxy, в f_tcp добавим следующую строку под use_backend:
use_backend tcp_to_reality if { req.ssl_sni -i drive.example.ru }

Добавим бекенд:

backend tcp_to_reality         mode tcp         server r1 /x-ui/reality.sock send-proxy tfo check         retry-on conn-failure empty-response response-timeout

Теперь добавим фронтенд для маскировки например под nextcloud:

frontend f_https_reality         bind /var/lib/haproxy/fakehttps1.sock accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 ssl-min-ver TLSv1.3 strict-sni mode 660 user haproxy group haproxy         http-request reject if { req.hdr(user-agent) -m sub evil }         acl url_discovery path /.well-known/caldav /.well-known/carddav         http-request redirect location /remote.php/dav/ code 301 if url_discovery         http-request deny if { path -m sub /. }         stick-table type ip size 100k expire 2m store http_req_rate(10s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 240 }         http-response set-header X-Content-Type-Options nosniff         http-response set-header X-XSS-Protection 1;mode=block         http-response set-header X-Frame-Options SAMEORIGIN         http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;" # Устанавливаем HSTS         http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path / } #        default_backend nextcloud

Сохраняем и проверяем конфигурацию:
haproxy -f /etc/haproxy/haproxy.cfg -c
Если ошибок нет, перезапускаем и проверяем подключение. После разворачиваем nextcloud убираем http-request return status 200 и раскомментируем default_backend nextcloud
Создаем бекенд для nextcloud:

backend nextcloud         mode http         option forwardfor         http-request set-header X-Forwarded-Proto https if { ssl_fc }         http-request set-header X-Forwarded-Proto http if !{ ssl_fc }         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-For %[src]         http-request add-header X-Real-Ip %[src]         server s1 127.0.0.1:49009

Таким образом можно добавить неограниченной количество inbounds

Настроим XHTTP:

Пример

Создаем inbound со следующими настройками:
1. Примечание на ваш выбор.
2. Протокол: VLESS
3. Мониторинг IP: /var/lib/haproxy/x-ui/xhttp1.sock,0660
4. Протокол(транспорт): XHTTP
5. Хост: ваш домен пример: corp.example.ru
6. Путь: /secretpath
7. Mode: auto
8. Sockopt: enable
9. TCP Fast Open: enable
10. External Proxy: enable
[TLS] [corp.example.ru] [443]
11. Безопасность: пусто
12. Sniffing: включить по умолчанию.

Пример в картинках
XHTTP 1

XHTTP 1
XHTTP 2

XHTTP 2

Добавим во фронтенд f_https:
use_backend xhttp if { req.hdr(host) -i corp.example.ru } { path_beg /secretpathbeg/ } || { path /secretpath }
Добавим бекенд:

backend xhttp # XHTTP: Beyond REALITY         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         retry-on conn-failure empty-response response-timeout         server x /x-ui/xhttp1.sock send-proxy tfo check

Аналогично проверяем конфигурацию HAProxy, и перезапускаем.

Настроим DoH, есть два пути, в docker и без, пойдем вариантом без docker.
Выполняем скрипт:
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v
Установиться с правами root, остановим сервис systemctl stop AdGuardHome.service,
Настроим конфигурацию: nano /opt/AdGuardHome/AdGuardHome.yaml

AdGuardHome.yaml
http:   pprof:     port: 6060     enabled: false   address: 127.0.0.1:49005   session_ttl: 720h users:   - name: demo     password: $2y$10$9ho8XPNKtOBVRlfgQV8CFOVCOsFVpjXAHZRzICh0PDBA.dsaCbioO auth_attempts: 5 block_auth_min: 15 http_proxy: "" language: "" theme: auto dns:   bind_hosts:     - 127.0.0.2   port: 53   anonymize_client_ip: false   ratelimit: 20   ratelimit_subnet_len_ipv4: 24   ratelimit_subnet_len_ipv6: 56   ratelimit_whitelist: []   refuse_any: true   upstream_dns:     - 127.0.0.53   upstream_dns_file: ""   bootstrap_dns:     - 9.9.9.10     - 149.112.112.10     - 2620:fe::10     - 2620:fe::fe:10   fallback_dns: []   upstream_mode: load_balance   fastest_timeout: 1s   allowed_clients: []   disallowed_clients: []   blocked_hosts:     - version.bind     - id.server     - hostname.bind   trusted_proxies:     - 127.0.0.0/8     - ::1/128   cache_size: 4194304   cache_ttl_min: 0   cache_ttl_max: 0   cache_optimistic: false   bogus_nxdomain: []   aaaa_disabled: false   enable_dnssec: false   edns_client_subnet:     custom_ip: ""     enabled: false     use_custom: false   max_goroutines: 300   handle_ddr: true   ipset: []   ipset_file: ""   bootstrap_prefer_ipv6: false   upstream_timeout: 10s   private_networks: []   use_private_ptr_resolvers: true   local_ptr_upstreams: []   use_dns64: false   dns64_prefixes: []   serve_http3: false   use_http3_upstreams: false   serve_plain_dns: true   hostsfile_enabled: true tls:   enabled: false   server_name: ""   force_https: false   port_https: 0   port_dns_over_tls: 0   port_dns_over_quic: 0   port_dnscrypt: 0   dnscrypt_config_file: ""   allow_unencrypted_doh: true   certificate_chain: ""   private_key: ""   certificate_path: ""   private_key_path: ""   strict_sni_check: false querylog:   dir_path: ""   ignored: []   interval: 2160h   size_memory: 1000   enabled: true   file_enabled: true statistics:   dir_path: ""   ignored: []   interval: 24h   enabled: true filters:   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_1_Russian/filter.txt     name: Ru filter     id: 1736931102   - enabled: false     url: https://raw.githubusercontent.com/rebelion76/bankiru_plus_adblock_list/master/bankiru_plus.txt     name: bankiru_plus     id: 1736931124   - enabled: false     url: https://raw.githubusercontent.com/deathbybandaid/piholeparser/master/Subscribable-Lists/CountryCodesLists/Russia.txt     name: Подписки RU     id: 1736931125   - enabled: false     url: https://easylist-downloads.adblockplus.org/ruadlist.txt     name: ruadlist     id: 1736931126   - enabled: false     url: https://raw.githubusercontent.com/deathbybandaid/piholeparser/master/Subscribable-Lists/ParsedBlacklists/RU-AdList.txt     name: RU-AdList     id: 1736931127   - enabled: false     url: https://easylist-downloads.adblockplus.org/cntblock.txt     name: RU Adlist Counters     id: 1736931128   - enabled: false     url: https://gist.githubusercontent.com/drewpayment/4a316423f7ff7df9dce63a041c478486/raw/6a509d262d7dd687c645349be3fc6217c0764768/AdGuard%2520Russian%2520filter%2520-%2520Rules.txt      name: AdGuard Russian filter     id: 1736931118   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt     name: AdGuard DNS filter     id: 1   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt     name: AdAway Default Blocklist     id: 2   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_4.txt     name: Dan Pollock's List     id: 1736931097   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_30.txt     name: Phishing URL Blocklist (PhishTank and OpenPhish)     id: 1736931098   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_50.txt     name: uBlock₀ filters – Badware risks     id: 1736931099   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_33.txt     name: Steven Black's List     id: 1736931100   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_59.txt     name: AdGuard DNS Popup Hosts filter     id: 1736931101   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_3_Spyware/filter.txt     name: Spyware     id: 1736931103   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_17_TrackParam/filter.txt     name: Trackers     id: 1736931104   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_15_DnsFilter/filter.txt     name: Ad Guard filter     id: 1736931107   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_11_Mobile/filter.txt     name: Mobile     id: 1736931108   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_21_Annoyances_Other/filter.txt     name: Раздражающие элементы     id: 1736931109   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_22_Annoyances_Widgets/filter.txt     name: Widgets     id: 1736931110   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_20_Annoyances_MobileApp/filter.txt     name: Widgets_Mobile     id: 1736931111   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_19_Annoyances_Popups/filter.txt     name: Всплывайки сайты     id: 1736931112   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_23.txt     name: Windows Spy list     id: 1736931113   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_8.txt     name: nocoin     id: 1736931114   - enabled: false     url: https://blocklistproject.github.io/Lists/smart-tv.txt     name: smart-tv     id: 1736931115   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt     name: HaGeZi's Ultimate Blocklist     id: 1736931116   - enabled: false     url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_27.txt     name: OISD Blocklist Big     id: 1736931117   - enabled: false     url: https://raw.githubusercontent.com/hant0508/uBlock-filters/master/filters.txt     name: uBlock-filters     id: 1736931119   - enabled: false     url: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_10_Useful/filter.txt     name: AdguardTeam     id: 1736931120   - enabled: false     url: https://filters.adtidy.org/extension/chromium-mv3/filters/24.txt     name: AdGuard Quick Fixes filter MV3     id: 1736931121   - enabled: false     url: https://gitlab.com/eyeo/anti-cv/abp-filters-anti-cv/-/raw/master/russian.txt     name: Russian anti-cv     id: 1736931122   - enabled: false     url: https://filters.adtidy.org/extension/ublock/filters/1_optimized.txt     name: ublock optimized     id: 1736931123 whitelist_filters: [] user_rules: [] dhcp:   enabled: false   interface_name: ""   local_domain_name: lan   dhcpv4:     gateway_ip: ""     subnet_mask: ""     range_start: ""     range_end: ""     lease_duration: 86400     icmp_timeout_msec: 1000     options: []   dhcpv6:     range_start: ""     lease_duration: 86400     ra_slaac_only: false     ra_allow_slaac: false filtering:   blocking_ipv4: ""   blocking_ipv6: ""   blocked_services:     schedule:       time_zone: Local     ids: []   protection_disabled_until: null   safe_search:     enabled: false     bing: true     duckduckgo: true     ecosia: true     google: true     pixabay: true     yandex: true     youtube: true   blocking_mode: default   parental_block_host: family-block.dns.adguard.com   safebrowsing_block_host: standard-block.dns.adguard.com   rewrites: []   safe_fs_patterns:     - /opt/AdGuardHome/userfilters/*   safebrowsing_cache_size: 1048576   safesearch_cache_size: 1048576   parental_cache_size: 1048576   cache_time: 30   filters_update_interval: 24   blocked_response_ttl: 10   filtering_enabled: true   parental_enabled: false   safebrowsing_enabled: false   protection_enabled: true clients:   runtime_sources:     whois: true     arp: true     rdns: true     dhcp: true     hosts: true   persistent: [] log:   enabled: true   file: ""   max_backups: 0   max_size: 100   max_age: 3   compress: false   local_time: false   verbose: false os:   group: ""   user: ""   rlimit_nofile: 0 schema_version: 29

Основное изменение это включение allow_unencrypted_doh: true
для данного примера, логин и пароль demo, и используется системный резолвер, порт панели 49005, добавлены списки для блокировки, по умолчанию отключены.

Подготовим HAProxy, во фронтенд f_tcp добавим следующую строку:
use_backend tcp_to_mtls if { req.ssl_sni -i ns1.example.ru }

Для понижения прав, необходимо или понизить привилегированный порт, или поднять порт dns например на 5353.

В f_https добавим:
use_backend http_adh if { ssl_fc_sni -i corp.example.ru } { path_beg -i /secretpathdns/ }
Добавим бэкенд:

backend http_adh # DOH AdGuardHome         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-Proto https if { ssl_fc }         http-request replace-path /secretpath(/)?(.*) /\2         server dns 127.0.0.1:49005 check port 49005

{ path_beg -i /secretpathdns/ } нужен для защиты dns-query, перед отправкой на бекенд путь будет удален, и запрос дойдет без ошибок к DoH, адрес для DoH следующий https://corp.examle.ru/secretpath/dns-query

Теперь же пришло добавить панель управления AdGuardHome, в f_tcp добавим следующую строку:
use_backend tcp_to_mtls if { req.ssl_sni -i ns1.example.ru }
Теперь внесем в f_mtls следующую строку:
use_backend http_adh if { ssl_fc_sni -i ns1.example.ru }

Снова проверяем конфигурацию и перезагружаем HAProxy.

Основное изменение это включение allow_unencrypted_doh: true для данного примера, логин и пароль demo, и используется системный резолвер, порт панели 49005

Подготовим HAProxy, во фронтенд f_tcp добавим следующую строку:
use_backend tcp_to_mtls if { req.ssl_sni -i ns1.example.ru }

Для понижения прав, необходимо или понизить привилегированный порт, или поднять порт dns например на 53535, создать пользователя, и необходимо изменить следующие:
os:
group: ""
user: ""

На созданного пользователя: например adguard:
os:
group: adguard
user: adguard

В f_https добавим:
use_backend http_adh if { ssl_fc_sni -i corp.example.ru } { path_beg -i /secretpathdns/ }
Добавим бэкенд:

backend http_adh # DOH AdGuardHome         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-Proto https if { ssl_fc }         http-request replace-path /secretpath(/)?(.*) /\2         server dns 127.0.0.1:49005 check port 49005

{ path_beg -i /secretpathdns/ } нужен для защиты dns-query, перед отправкой на бекенд путь будет удален, и запрос дойдет без ошибок к DoH, адрес для DoH следующий https://corp.examle.ru/secretpath/dns-query Теперь же пришло добавить панель управления AdGuardHome, в f_tcp добавим следующую строку: use_backend tcp_to_mtls if { req.ssl_sni -i ns1.example.ru } Теперь внесем в f_mtls следующую строку: use_backend http_adh if { ssl_fc_sni -i ns1.example.ru } Снова проверяем конфигурацию и перезагружаем HAProxy.

Итог по 3X-UI панель работает за mTLS, кроме сервиса подписки, она доступна из вне по длинному пути, сами inbound работают через unix socket что должно обеспечить более высокую производительность в связке с TFO, REALITY развернут методом steal-oneself, при котором мы не зависим от чужого сайта/сертификата, ответ более быстрый, следовательно работать будет быстрее.

Часть 6. Установка сертификата

Для выпуска сертификата для сервиса, потребуется сделать следующие на примере ocserv:
1. Создаем директорию: mkdir /etc/ocserv/ssl
2. Добавляем пользователя acme в группу ocserv: adduser acme ocserv
3. Меняем владельца: chown ocserv:ocserv /etc/ocserv/ssl
4.Изменяем права: chmod 770 /etc/ocserv/ssl

Переключаемся на пользователя acme: sudo -u acme -s

Получим сертификат: acme.sh --issue -d example.com --stateless
Установим сертификат и перезапустим ocserv:

acme.sh --install-cert -d example.ru \ --cert-file /etc/ocserv/ssl/example.ru.pem \ --key-file /etc/ocserv/ssl/example.ru.key \ --fullchain-file /etc/ocserv/ssl/fullchain.ru.pem \ --reloadcmd "systemctl restart ocserv"

На этом все, проделываем аналогичным способом для ваш сервисов.

Заключение

В данной статье показал пример как можно защитить сервисы за mTLS, пример работы с GeoIP выгрузка в Prometheus из HAProxy exporter, и обновил информацию по REALITY.

В статье используется множество поддоменов, поменяйте на свои соответственно, аналогично и с path.

Полный конфигурации haproxy.cfg
global         log /dev/log    local0         log /dev/log    local1 notice         chroot /var/lib/haproxy         stats socket /var/run/haproxy/admin.sock level admin mode 660         setenv ACCOUNT_THUMBPRINT '*ACCOUNT_THUMBPRINT*' # поменять на полученный из acme         stats timeout 30s         user    haproxy         group   haproxy         daemon     # https://ssl-config.mozilla.org/     # Улучшенная безопастность и совместимость со старыми браузерами ( Intermediate ) Supports Firefox 27, Android 4.4.2, Chrome 31, Edge, IE 11 on Windows 7, Java 8u31, OpenSSL 1.0.1, Opera 20, Safari 9         ssl-default-bind-curves X25519:prime256v1:secp384r1 #:secp521r1:X448         ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305         ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256         ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.2 no-tls-tickets          ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305          ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256         ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets      # TLS 1.3 современная безопасность без отката к TLS 1.2  ( Modern ) Supports Firefox 63, Android 10.0, Chrome 70, Edge 75, Java 11, OpenSSL 1.1.1, Opera 57, Safari 12.1 #        ssl-default-bind-curves X25519:prime256v1:secp384r1 #        ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 #        ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.3 no-tls-tickets  #        ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 #        ssl-default-server-options ssl-min-ver TLSv1.3 no-tls-tickets          ssl-dh-param-file /etc/haproxy/dh4096.pem # openssl dhparam -out /etc/haproxy/dh4096.pem 4096 #        tune.ssl.default-dh-param 2048       # Тюнинг http/2         tune.h2.initial-window-size 536870912 # Увеличиваем начальный размер окна для входящих и исходящих соединений.         tune.h2.fe.max-concurrent-streams 512 # Установим кол-во одновременных потоков на входящие соединений.         tune.h2.be.max-concurrent-streams 512 # Установим кол-во одновременных потоков на исходящие соединений. #------------------------------------------ defaults        log global        mode http        option httplog        option dontlognull        timeout connect 40s        timeout client  1m        timeout server  1m        timeout tunnel 1h        timeout http-request 30s        errorfile 400 /etc/haproxy/errors/400.http        errorfile 403 /etc/haproxy/errors/403.http        errorfile 408 /etc/haproxy/errors/408.http        errorfile 500 /etc/haproxy/errors/500.http        errorfile 502 /etc/haproxy/errors/502.http        errorfile 503 /etc/haproxy/errors/503.http        errorfile 504 /etc/haproxy/errors/504.http #------------------------------------------ resolvers dnsserver         nameserver ns1 127.0.0.1:53         parse-resolv-conf         resolve_retries       3         timeout resolve       1s         timeout retry         1s         hold other           30s         hold refused         30s         hold nx              30s         hold timeout         30s         hold valid           10s         hold obsolete        30s #----------------------------------- frontend f_http         bind *:80         bind [::]:80         mode http      # FireHOL IP List         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level1.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level2.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level3.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/abusers_1d.acl }      # Ограничение запросов, дропаем на раннем этапе для http фронта         stick-table type ip size 1m expire 10s store conn_cur         tcp-request session track-sc0 src         tcp-request session reject if { sc_conn_cur(0) gt 60 }      # отклоним невалидные юзер агенты         http-request reject if { req.hdr(user-agent) -m len le 32 }         http-request reject if { req.hdr(user-agent) -m sub evil }      # ответим на запрос ACME         http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         http-request redirect scheme https code 301 unless { ssl_fc } #------------------------------------------ frontend f_tcp         bind *:443    # ipv4 SSL Passthrough         bind [::]:443 # ipv6 SSL Passthrough         mode tcp         option tcplog         tcp-request inspect-delay 3s      # FireHOL IP List         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level1.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level2.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/level3.acl }         tcp-request connection silent-drop if { src -f /etc/haproxy/firehol/abusers_1d.acl }      # Ограничим частоту запросов         stick-table type ip size 1m expire 10s store conn_cur         tcp-request content track-sc0 src         tcp-request content reject if { sc_conn_cur(0) gt 240 }         tcp-request content capture req.ssl_sni len 10         tcp-request content accept if { req_ssl_hello_type 1 } or !{ req_ssl_hello_type 1 }      # Правила сопостовления          use_backend tcp-ssh if !{ req.ssl_hello_type 1 } { payload(0,7) -m bin 5353482d322e30 } or !{ req.ssl_hello_type 1 } { req.len 0 }          use_backend tcp_to_https if { req.ssl_sni -i corp.example.ru }          use_backend tcp_to_reality if { req.ssl_sni -i drive.example.ru }          use_backend tcp_to_metrics if { req.ssl_sni -i metrics.example.ru }          use_backend tcp_to_mtls if { req.ssl_sni -i auth.example.ru } || { req.ssl_sni -i ns1.example.ru }  #        use_backend tcp_to_ocserv if { req.ssl_sni -i example.ru } #------------------------------------------ frontend f_https #          bind abns@frontendhttps.sock accept-proxy ssl crt /etc/haproxy/certs/ ssl crt /etc/haproxy/ssl/ alpn h2,http/1.1 strict-sni tfo         http-request reject if { req.hdr(user-agent) -m sub evil }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         http-response set-header X-Content-Type-Options nosniff         http-response set-header X-XSS-Protection 1;mode=block         http-response set-header X-Frame-Options SAMEORIGIN         http-after-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;" # Устанавливаем HSTS         stick-table type ip size 100k expire 5m store http_req_rate(10s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 300 }         http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path /https-status-443 }          use_backend http_3xui_sub if { ssl_fc_sni -i corp.example.ru } { path_beg /sub-path/ } || { path_beg /jsub-path/ }         use_backend xhttp if { req.hdr(host) -i corp.example.ru } { path_beg -i /secretpath/ } || { path_beg -i /secretpath }          use_backend http_adh if { ssl_fc_sni -i corp.example.ru } { path_beg -i /secretpathdns/ } # { path_beg /dns-query/ }  #        use_backend websocket if { req.hdr(Host) -i corp.example.ru } { path /ws } || { path_beg /ws/ }  frontend f_mtls # Административный доступ         bind abns@frontendmtlsadm.sock accept-proxy alpn h3,h2,http/1.1 tfo ssl-min-ver TLSv1.3 ssl crt /etc/haproxy/certs/ verify required ca-file /etc/haproxy/ca/ca.pem ca-verify-file /etc/haproxy/ca/fullca.pem crl-file /etc/haproxy/ca/revoked.crl crt-ignore-err 10,23         http-request silent-drop if { path_end /dns-query } || { path_beg /dns-query/ }         http-request reject if { req.hdr(user-agent) -m sub evil }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         http-after-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains;" # Устанавливаем HSTS         http-request return status 403 content-type text/plain lf-string "Your certificate has expired, please contact the administrator" if { ssl_c_verify 10 }         http-request return status 403 content-type text/plain lf-string "Your certificate has been revoked, please contact the administrator" if { ssl_c_verify 23 }         http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path /https-status-443 }          use_backend http_3xui if { ssl_fc_sni -i corp.example.ru } { path_beg /defaultpath/ } || { path_beg /3xui-path }         use_backend stats if { path_beg -i /haproxy/ }         use_backend http_adh if { ssl_fc_sni -i ns1.example.ru }  #-- Statistics ---------------------------- frontend f_metrics_mtls         bind abns@metricshttps.sock tfo accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 ssl-min-ver TLSv1.3 strict-sni verify required ca-file /etc/haproxy/ca/metrics.pem ca-verify-file /etc/haproxy/ca/metricsfull.pem crl-file /etc/haproxy/ca/metrics.crl crt-ignore-err 10,23         tcp-request connection accept if { src -f /etc/haproxy/geoip/RU.list }         http-request reject if { req.hdr(user-agent) -m sub evil }         http-request deny if { path -m sub /. } # запрет доступа к скрытым файлам         stick-table type ip size 100k expire 5m store http_req_rate(30s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 90 }         http-after-response set-header Strict-Transport-Security "max-age=16000000;"         http-request return status 403 content-type text/plain lf-string "Your certificate has expired, please contact the administrator" if { ssl_c_verify 10 }         http-request return status 403 content-type text/plain lf-string "Your certificate has been revoked, please contact the administrator" if { ssl_c_verify 23 }         http-request use-service prometheus-exporter if { path_beg /haproxy-exporter-0835n-rm05v-g1nry-r8h-h7dn-gu8v/ } # exporter haproxy, ссылка для prometheus ( /path_beg/metrics )          use_backend node-exporter if { path_beg -i /node-exporter-4umx-2nf46x3-qghy-7b3o-zxca-jte/ } # exporter сервера backend node-exporter         mode http         http-request replace-path /node-exporter-4umx-2nf46x3-qghy-7b3o-zxca-jte(/)?(.*) /\2         server s1 127.0.0.1:9100 frontend f_stats         bind abns@stats.sock         stats enable         stats uri /stats         stats realm Haproxy\ Statistics         stats refresh 10s         stats show-legends         stats hide-version         stats show-modules backend stats         mode http         http-request replace-path /haproxy(/)?(.*) /\2         server s1 abns@stats.sock #------------------------------------------ frontend f_https_reality         bind /var/lib/haproxy/fakehttps1.sock accept-proxy ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 strict-sni mode 660 user haproxy group haproxy ssl-min-ver TLSv1.3         http-request reject if { req.hdr(user-agent) -m sub evil }         acl url_discovery path /.well-known/caldav /.well-known/carddav         http-request redirect location /remote.php/dav/ code 301 if url_discovery         http-request deny if { path -m sub /. }         stick-table type ip size 100k expire 2m store http_req_rate(10s)         http-request track-sc0 src         http-request deny deny_status 429 if { sc_http_req_rate(0) gt 240 }         http-response set-header X-Content-Type-Options nosniff         http-response set-header X-XSS-Protection 1;mode=block         http-response set-header X-Frame-Options SAMEORIGIN         http-response set-header Strict-Transport-Security "max-age=16000000;" # Устанавливаем HSTS         http-request return status 200 content-type text/plain lf-string "Status 200 OK! Your IP %[src]." if { path / } #        default_backend nextcloud #------- BACKEND -------------------------- #[ TCP Passthrough backend tcp-ssh # Бекенд для SSH         mode tcp         option http-keep-alive         timeout http-keep-alive 30s         server ssh 127.0.0.1:22 check port 22  #backend tcp_to_ocserv #  #        mode tcp #        server oc 127.0.0.1:48658 check port 48658 send-proxy  backend tcp_to_https #          mode tcp         server s1 abns@frontendhttps.sock send-proxy-v2-ssl-cn tfo check          retry-on conn-failure empty-response response-timeout  backend tcp_to_reality          mode tcp         server reality1 /x-ui/reality1.sock send-proxy tfo check         retry-on conn-failure empty-response response-timeout  backend tcp_to_mtls         mode tcp         server mtls abns@frontendmtlsadm.sock send-proxy-v2-ssl-cn tfo check         retry-on conn-failure empty-response response-timeout  backend tcp_to_metrics         mode tcp         server metrics abns@metricshttps.sock send-proxy-v2 tfo check         retry-on conn-failure empty-response response-timeout  backend http_3xui # Админ панель 3X-UI         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-Proto https if { ssl_fc }         server 3xui 127.0.0.1:49004 check port 49004  backend http_3xui_sub # подписка 3х         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-Proto https if { ssl_fc }         server 3xsub 127.0.0.1:49007 check port 49007  backend xhttp # XHTTP: Beyond REALITY         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         retry-on conn-failure empty-response response-timeout         server xhttp1 /x-ui/xhttp1.sock send-proxy tfo check  backend http_adh # DOH AdGuardHome         mode http         option forwardfor if-none         http-request add-header X-Real-Ip %[src]         http-request set-header Host %[req.hdr(Host)]         http-request set-header X-Forwarded-For %[src]         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-Proto https if { ssl_fc }         http-request replace-path /secretpathdns(/)?(.*) /\2         server dns 127.0.0.1:49005 check port 49005  #backend http_site1 # Apache2 #        mode http #        option httpchk #        http-check expect status 200 #        option forwardfor if-none #        http-request add-header X-Real-Ip %[src] #        http-request set-header Host %[req.hdr(Host)] #        http-request set-header X-Forwarded-For %[src] #        http-request set-header X-Forwarded-Host %[req.hdr(host)] #        http-request set-header X-Forwarded-Proto https if { ssl_fc } #        server ap2 127.0.0.1:8080 check  backend nextcloud #         mode http         option forwardfor         http-request set-header X-Forwarded-Proto https if { ssl_fc }         http-request set-header X-Forwarded-Proto http if !{ ssl_fc }         http-request set-header X-Forwarded-Host %[req.hdr(host)]         http-request set-header X-Forwarded-For %[src]         http-request add-header X-Real-Ip %[src]         http-response set-header Strict-Transport-Security max-age=157680000;         server nextcloud 127.0.0.1:49009 #check port 49009  # --- IP List ------------------------------- # Range 48658—48999 - TCP // 49001—49100 - http сервисы // 49101-49150 - WS 


ссылка на оригинал статьи https://habr.com/ru/articles/904038/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *