Так же будет переосмысление предыдущих статей на тему маршрутизации 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 -sacme.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+IPv6nano /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.
Создадим корневой сертификат для подписи, для этого перейдите в "Сертификаты" и "Новый сертификат"
Инструкция в картинках
Создадим корневой сертификат, которым подпишем промежуточный.
Настройте первоисточник, по умолчанию будет самозаверенный, алгоритм подписи 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
Теперь необходимо получить файл 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_mtlsuse_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
Часть 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: включить по умолчанию.
Настроим 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: включить по умолчанию.
Пример в картинках
Добавим во фронтенд 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
