From 779bf0d921cff97aa89bf14a13e2981346dd062d Mon Sep 17 00:00:00 2001 From: PTah Date: Sat, 13 Jun 2026 19:06:54 +1000 Subject: [PATCH] =?UTF-8?q?feat(haproxy):=20allowlist=20rds-allowed=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20RDS=20=D0=B8=D0=B7=20ipdeny=20RU=20(0.7.5)?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 6 + README.md | 4 +- docs/haproxy-install.md | 23 ++- haproxy.cfg | 11 +- rds-allowed-static | 4 + scripts/haproxy-rds-allowed-deploy.sh | 178 ++++++++++++++++++++++++ scripts/haproxy-rds-allowed-generate.sh | 7 + scripts/haproxy-rds-allowed.cron | 4 + scripts/haproxy_ru_ipdeny_lib.sh | 101 ++++++++++++++ 9 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 rds-allowed-static create mode 100644 scripts/haproxy-rds-allowed-deploy.sh create mode 100644 scripts/haproxy-rds-allowed-generate.sh create mode 100644 scripts/haproxy-rds-allowed.cron create mode 100644 scripts/haproxy_ru_ipdeny_lib.sh diff --git a/.gitattributes b/.gitattributes index 583f7c7..b20336a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,10 @@ *.md text eol=lf git-sac-allowed text eol=lf 1c-allowed text eol=lf +zabbix-allowed text eol=lf +syno-allowed text eol=lf +rds-allowed-static text eol=lf +scripts/*.sh text eol=lf +scripts/*.cron text eol=lf +scripts/haproxy_ru_ipdeny_lib.sh text eol=lf .gitignore text eol=lf diff --git a/README.md b/README.md index fdab8ab..d8bf7f6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ TLS: HAProxy на **443** в режиме **TCP + SNI**, **не терминирует** TLS — сертификаты на бэкендах. -Доступ снаружи по allowlist: **Git** и **веб-SAC** — `git-sac-allowed`; **1С** — `1c-allowed`; **Zabbix** (HTTP) — `zabbix-allowed`; **Synology** (`heap`, `exchange`) — `syno-allowed`. **Seaca** — **`sac-api.kalinamall.ru`** без IP-фильтра. +Доступ снаружи по allowlist: **Git** и **веб-SAC** — `git-sac-allowed`; **RDS** — `rds-allowed` (RU ipdeny + static); **1С** — `1c-allowed`; **Zabbix** (HTTP) — `zabbix-allowed`; **Synology** (`heap`, `exchange`) — `syno-allowed`. **Seaca** — **`sac-api.kalinamall.ru`** без IP-фильтра. **Основной репозиторий:** [git.kalinamall.ru/PapaTramp/reverse-proxy](https://git.kalinamall.ru/PapaTramp/reverse-proxy) **Резерв (без секретов):** [github.com/PTah/reverse-proxy](https://github.com/PTah/reverse-proxy) — см. [политику репозиториев](docs/repo-policy.md) @@ -15,7 +15,7 @@ TLS: HAProxy на **443** в режиме **TCP + SNI**, **не терминир |--------|------------------------|----------------------|------|-----------| | Microsoft Exchange (OWA и т.д.) | `ext.kalinamall.ru` | fifth.kalinamall.ru — **192.168.160.42** | 443 | нет | | Autodiscover | `autodiscover.kalinamall.ru` | тот же Exchange — **192.168.160.42** | 443 | нет | -| RDS | `rds.kalinamall.ru` | k6a-dc3 — **192.168.160.40**, служба **4430** | 443 → 4430 | нет | +| RDS | `rds.kalinamall.ru` | k6a-dc3 — **192.168.160.40**, служба **4430** | 443 → 4430 | **`rds-allowed`** | | 1С | `erp.kalinamall.ru` | hp-serv — **192.168.160.150** | 443 | **`1c-allowed`** | | Git (HTTPS) | `git.kalinamall.ru` | **192.168.160.129** | 443 | **`git-sac-allowed`** | | SAC (веб) | `sac.kalinamall.ru` | **192.168.160.145** | 443 | **`git-sac-allowed`** | diff --git a/docs/haproxy-install.md b/docs/haproxy-install.md index 1c8966a..220b0d6 100644 --- a/docs/haproxy-install.md +++ b/docs/haproxy-install.md @@ -44,7 +44,10 @@ sudo haproxy -c -f /etc/haproxy/haproxy.cfg ```bash sudo cp ~/reverse-proxy/haproxy.cfg /etc/haproxy/haproxy.cfg - sudo cp ~/reverse-proxy/git-sac-allowed ~/reverse-proxy/1c-allowed ~/reverse-proxy/zabbix-allowed ~/reverse-proxy/syno-allowed /etc/haproxy/ + sudo cp ~/reverse-proxy/git-sac-allowed ~/reverse-proxy/1c-allowed ~/reverse-proxy/zabbix-allowed ~/reverse-proxy/syno-allowed ~/reverse-proxy/rds-allowed-static /etc/haproxy/ + sudo install -m 0755 ~/reverse-proxy/scripts/haproxy-rds-allowed-deploy.sh ~/reverse-proxy/scripts/haproxy_ru_ipdeny_lib.sh /usr/local/bin/ + sudo /usr/local/bin/haproxy-rds-allowed-deploy.sh + sudo cp ~/reverse-proxy/scripts/haproxy-rds-allowed.cron /etc/cron.d/haproxy-rds-allowed ``` 3. Проверка и перезапуск: @@ -105,3 +108,21 @@ curl -kI --resolve exchange.kalinamall.ru:443:ПУБЛИЧНЫЙ_IP https://exch ``` При HTTP/2 coalescing в Chromium см. [sac-access.md](sac-access.md) — отдельная вкладка или `http2 off` на других бэкендах с тем же wildcard. + +## RDS (rds.kalinamall.ru) + +- **`rds.kalinamall.ru`** — RD Gateway / RD Web, бэкенд **192.168.160.40:4430** +- IP-allowlist: **`rds-allowed`** — объединение **RU CIDR** ([ipdeny `ru.zone`](https://www.ipdeny.com/ipblocks/data/countries/ru.zone)) и **`rds-allowed-static`** (фиксированные IP вне RU или исключения) +- Обновление списка: **`/usr/local/bin/haproxy-rds-allowed-deploy.sh`** (+ **`haproxy_ru_ipdeny_lib.sh`** рядом), cron **ежедневно в 01:00** (`/etc/cron.d/haproxy-rds-allowed`) +- Источник логики: [Answer.and.other.shit/scripts/mikrotik-ru-ipdeny](https://git.kalinamall.ru/PapaTramp/Answer.and.other.shit) (`mikrotik-649-ru-ipdeny-deploy.sh`), зона **`ru.zone`**, минимум **8000** подсетей +- Лог деплоя: **`/var/log/haproxy-rds-allowed-deploy.log`**, syslog: **`haproxy-rds-allowed-deploy`** + +Добавить постоянный IP/CIDR вручную — правка **`/etc/haproxy/rds-allowed-static`** (и копия в репозитории `rds-allowed-static`), затем запуск deploy-скрипта. + +Проверка с allowlist IP: + +```bash +curl -kI --resolve rds.kalinamall.ru:443:ПУБЛИЧНЫЙ_IP https://rds.kalinamall.ru/ +``` + +**Примечание:** VPN в LAN (`192.168.160.0/24`) обходит HAProxy; allowlist действует только на вход через периметр (`443`, SNI `rds.kalinamall.ru`). diff --git a/haproxy.cfg b/haproxy.cfg index 8c4dc5a..d3834b2 100644 --- a/haproxy.cfg +++ b/haproxy.cfg @@ -1,11 +1,12 @@ # HAProxy — периметровый reverse-proxy для kalinamall.ru -# Config version: 0.7.4 +# Config version: 0.7.5 # Режим: TCP + SNI на 443 (TLS end-to-end, сертификаты на бэкендах) # HTTP на 80: zabbix.kalinamall.ru → 192.168.160.53 (allowlist zabbix-allowed) # # Проверка синтаксиса: haproxy -c -f haproxy.cfg # Установка: скопируйте в /etc/haproxy/haproxy.cfg -# Allowlist: git-sac-allowed, 1c-allowed, zabbix-allowed, syno-allowed → /etc/haproxy/ +# Allowlist: git-sac-allowed, 1c-allowed, zabbix-allowed, syno-allowed, rds-allowed → /etc/haproxy/ +# rds-allowed: ежедневно scripts/haproxy-rds-allowed-deploy.sh (ipdeny RU + rds-allowed-static) # sac-api.kalinamall.ru — без IP-фильтра (Seaca с любого адреса, auth на SAC) global @@ -28,6 +29,7 @@ defaults # HTTPS (443): маршрутизация по SNI → внутренние серверы # ext, autodiscover (Exchange) / rds / erp / git / sac / sac-api / heap, exchange (Synology) # Git и sac.kalinamall.ru (веб) — allowlist git-sac-allowed +# rds.kalinamall.ru — allowlist rds-allowed (RU ipdeny + static) # heap.kalinamall.ru, exchange.kalinamall.ru (Synology DSM) — syno-allowed # sac-api.kalinamall.ru (мобильное API) — без allowlist # См. docs/sac-access.md — connection coalescing (git + sac на одном IP) @@ -52,10 +54,12 @@ frontend fe_https_sni acl sni_git req_ssl_sni -i git.kalinamall.ru acl sni_sac_web req_ssl_sni -i sac.kalinamall.ru + acl sni_rds req_ssl_sni -i rds.kalinamall.ru acl sni_erp req_ssl_sni -i erp.kalinamall.ru acl sni_syno req_ssl_sni -i heap.kalinamall.ru acl sni_syno req_ssl_sni -i exchange.kalinamall.ru acl src_git_sac_allowed src -f /etc/haproxy/git-sac-allowed + acl src_rds_allowed src -f /etc/haproxy/rds-allowed acl src_1c_allowed src -f /etc/haproxy/1c-allowed acl src_syno_allowed src -f /etc/haproxy/syno-allowed @@ -63,6 +67,7 @@ frontend fe_https_sni tcp-request content reject if !sni_known tcp-request content reject if sni_git !src_git_sac_allowed tcp-request content reject if sni_sac_web !src_git_sac_allowed + tcp-request content reject if sni_rds !src_rds_allowed tcp-request content reject if sni_erp !src_1c_allowed tcp-request content reject if sni_syno !src_syno_allowed @@ -81,7 +86,7 @@ backend be_exchange server exch 192.168.160.42:443 check check-ssl check-sni ext.kalinamall.ru verify none backend be_rds - # k6a-dc3.b26.kalinamall.ru (192.168.160.40), TLS на 4430 + # k6a-dc3.b26.kalinamall.ru (192.168.160.40), TLS на 4430; снаружи только IP из rds-allowed server rds 192.168.160.40:4430 check check-ssl check-sni rds.kalinamall.ru verify none backend be_erp diff --git a/rds-allowed-static b/rds-allowed-static new file mode 100644 index 0000000..61b34f2 --- /dev/null +++ b/rds-allowed-static @@ -0,0 +1,4 @@ +5.100.81.121 +109.109.217.150 +109.110.54.2 +212.19.6.53 diff --git a/scripts/haproxy-rds-allowed-deploy.sh b/scripts/haproxy-rds-allowed-deploy.sh new file mode 100644 index 0000000..4b1f47f --- /dev/null +++ b/scripts/haproxy-rds-allowed-deploy.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# Deploy /etc/haproxy/rds-allowed from ipdeny.com ru.zone + rds-allowed-static. +# +# Адаптация scripts/mikrotik-ru-ipdeny/mikrotik-649-ru-ipdeny-deploy.sh (Answer.and.other.shit) +# для HAProxy на haproxy.kalinamall.ru — вместо MikroTik: merge static, haproxy -c, reload. +# +# Установка: +# install -m 0755 haproxy-rds-allowed-deploy.sh haproxy_ru_ipdeny_lib.sh /usr/local/bin/ +# +# Cron (ежедневно 01:00): scripts/haproxy-rds-allowed.cron -> /etc/cron.d/haproxy-rds-allowed +# 0 1 * * * root /usr/local/bin/haproxy-rds-allowed-deploy.sh >> /var/log/haproxy-rds-allowed-deploy.log 2>&1 +# +# Examples: +# ./haproxy-rds-allowed-deploy.sh +# ./haproxy-rds-allowed-deploy.sh --generate-only -o /tmp/rds-allowed +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=haproxy_ru_ipdeny_lib.sh +source "$SCRIPT_DIR/haproxy_ru_ipdeny_lib.sh" + +ENV_FILE=${HAPROXY_ENV_FILE:-${SCRIPT_DIR}/env} +ALLOWLIST=${HAPROXY_RDS_ALLOWED:-/etc/haproxy/rds-allowed} +STATIC=${HAPROXY_RDS_STATIC:-/etc/haproxy/rds-allowed-static} +HAPROXY_CFG=${HAPROXY_CFG:-/etc/haproxy/haproxy.cfg} +LOCK_FILE=${HAPROXY_RDS_LOCK:-/run/lock/haproxy-rds-allowed-deploy.lock} +LOG_TAG=haproxy-rds-allowed-deploy + +load_env_file() { + local file_path=$1 + [[ -f $file_path ]] || return 0 + set -a + # shellcheck source=/dev/null + source "$file_path" + set +a +} + +log() { + logger -t "$LOG_TAG" "$*" + echo "$*" +} + +usage() { + cat <<'EOF' +Usage: haproxy-rds-allowed-deploy.sh [options] + + --allowlist PATH output allowlist (default: /etc/haproxy/rds-allowed) + --static PATH static IP/CIDR file (default: /etc/haproxy/rds-allowed-static) + --haproxy-cfg PATH haproxy config for -c (default: /etc/haproxy/haproxy.cfg) + --zone-url URL ipdeny zone URL (default: ru.zone) + --min-entries N minimum subnet count from zone (default: 8000) + --env-file PATH optional env (HAPROXY_* overrides) + -o, --output PATH keep generated list at this path + --generate-only download/merge only; no install/reload + -h, --help +EOF +} + +install_allowlist() { + local new_list=$1 + local line_count=${2:-0} + + if [[ -f $ALLOWLIST ]]; then + cp -a "$ALLOWLIST" "${ALLOWLIST}.bak-$(date +%Y%m%d-%H%M%S)" + fi + + install -m 0644 -o root -g root "$new_list" "$ALLOWLIST" + + if ! haproxy -c -f "$HAPROXY_CFG" >/tmp/haproxy-rds-check.log 2>&1; then + log "ERROR: haproxy -c failed: $(tail -3 /tmp/haproxy-rds-check.log | tr '\n' ' ')" + local latest_bak + latest_bak=$(ls -t "${ALLOWLIST}.bak-"* 2>/dev/null | head -1 || true) + if [[ -n $latest_bak ]]; then + cp -a "$latest_bak" "$ALLOWLIST" + fi + die "haproxy config check failed; restored previous allowlist if backup exists" + fi + + systemctl reload haproxy + log "done: allowlist=${ALLOWLIST} total=${line_count} zone_entries=${GENERATED_ZONE_ENTRIES} bytes=${GENERATED_BYTES}" +} + +main() { + local zone_url=$DEFAULT_ZONE_URL + local min_entries=$DEFAULT_MIN_LINES + local output_path= + local generate_only=0 + local temp_list= + local remove_temp_list=0 + + load_env_file "$ENV_FILE" + + while [[ $# -gt 0 ]]; do + case "$1" in + --allowlist) + ALLOWLIST=$2 + shift 2 + ;; + --static) + STATIC=$2 + shift 2 + ;; + --haproxy-cfg) + HAPROXY_CFG=$2 + shift 2 + ;; + --zone-url) + zone_url=$2 + shift 2 + ;; + --min-entries) + min_entries=$2 + shift 2 + ;; + --env-file) + ENV_FILE=$2 + load_env_file "$ENV_FILE" + shift 2 + ;; + -o | --output) + output_path=$2 + shift 2 + ;; + --generate-only) + generate_only=1 + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac + done + + command -v curl >/dev/null 2>&1 || die "curl not found (apt install curl)" + + exec 9>"$LOCK_FILE" + if ! flock -n 9; then + log "already running, exit" + exit 0 + fi + + if [[ -z $output_path ]]; then + if [[ $generate_only -eq 1 ]]; then + die "use -o/--output with --generate-only" + fi + temp_list=$(mktemp) + output_path=$temp_list + remove_temp_list=1 + trap "rm -f '${temp_list}'" EXIT + fi + + echo "downloading zone: ${zone_url}" + generate_allowlist_bundle "$zone_url" "$output_path" "$STATIC" "$min_entries" + echo "generated entries=${GENERATED_ENTRIES} zone_entries=${GENERATED_ZONE_ENTRIES} bytes=${GENERATED_BYTES}" + + if [[ $generate_only -eq 1 ]]; then + echo "wrote ${output_path}" + if [[ $remove_temp_list -eq 1 ]]; then + rm -f "$temp_list" + trap - EXIT + fi + exit 0 + fi + + command -v haproxy >/dev/null 2>&1 || die "haproxy not found" + install_allowlist "$output_path" "$GENERATED_ENTRIES" + + if [[ $remove_temp_list -eq 1 ]]; then + rm -f "$temp_list" + trap - EXIT + fi +} + +main "$@" diff --git a/scripts/haproxy-rds-allowed-generate.sh b/scripts/haproxy-rds-allowed-generate.sh new file mode 100644 index 0000000..6fdce41 --- /dev/null +++ b/scripts/haproxy-rds-allowed-generate.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Build HAProxy rds-allowed from ipdeny ru.zone + static (generate only). +# Аналог mikrotik-649-ru-ipdeny-generate.sh. +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +exec "$SCRIPT_DIR/haproxy-rds-allowed-deploy.sh" --generate-only "$@" diff --git a/scripts/haproxy-rds-allowed.cron b/scripts/haproxy-rds-allowed.cron new file mode 100644 index 0000000..ce32a7a --- /dev/null +++ b/scripts/haproxy-rds-allowed.cron @@ -0,0 +1,4 @@ +# /etc/cron.d/haproxy-rds-allowed — обновление rds-allowed из ipdeny (RU) + static +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +0 1 * * * root /usr/local/bin/haproxy-rds-allowed-deploy.sh >> /var/log/haproxy-rds-allowed-deploy.log 2>&1 diff --git a/scripts/haproxy_ru_ipdeny_lib.sh b/scripts/haproxy_ru_ipdeny_lib.sh new file mode 100644 index 0000000..1dbc6ba --- /dev/null +++ b/scripts/haproxy_ru_ipdeny_lib.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Shared helpers: ipdeny ru.zone -> HAProxy src -f allowlist (bash, no pip). +# Логика из Answer.and.other.shit/scripts/mikrotik-ru-ipdeny/mikrotik_ru_ipdeny_lib.sh +set -Eeuo pipefail + +DEFAULT_ZONE_URL="${IPDENY_ZONE_URL:-https://www.ipdeny.com/ipblocks/data/countries/ru.zone}" +DEFAULT_MIN_LINES="${HAPROXY_MIN_ENTRIES:-8000}" + +die() { + echo "error: $*" >&2 + exit 1 +} + +fetch_zone() { + local url=$1 + local dest=$2 + curl -fsSL --max-time 120 -A "haproxy-ru-ipdeny/1.0" "$url" -o "$dest" +} + +count_subnets_in_zone() { + local zone_file=$1 + local count=0 + local line + while IFS= read -r line || [[ -n $line ]]; do + line="${line//$'\r'/}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z $line || $line == \#* ]] && continue + [[ $line != */* ]] && continue + count=$((count + 1)) + done <"$zone_file" + printf '%s' "$count" +} + +write_zone_allowlist() { + local zone_file=$1 + local output_path=$2 + + { + while IFS= read -r line || [[ -n $line ]]; do + line="${line//$'\r'/}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z $line || $line == \#* ]] && continue + [[ $line != */* ]] && continue + printf '%s\n' "$line" + done <"$zone_file" + } >"$output_path" +} + +append_static_allowlist() { + local static_file=$1 + local output_path=$2 + + [[ -f $static_file ]] || return 0 + while IFS= read -r line || [[ -n $line ]]; do + line="${line//$'\r'/}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z $line || $line == \#* ]] && continue + printf '%s\n' "$line" + done <"$static_file" >>"$output_path" +} + +generate_allowlist_bundle() { + local zone_url=${1:-$DEFAULT_ZONE_URL} + local output_path=${2:-} + local static_file=${3:-} + local min_lines=${4:-$DEFAULT_MIN_LINES} + + [[ -n $output_path ]] || die "output path required" + + local tmp_zone merged + tmp_zone=$(mktemp) + merged=$(mktemp) + trap "rm -f '${tmp_zone}' '${merged}'" RETURN + + fetch_zone "$zone_url" "$tmp_zone" + + local zone_entries + zone_entries=$(count_subnets_in_zone "$tmp_zone") + if [[ $zone_entries -lt $min_lines ]]; then + die "too few subnets: ${zone_entries} (expected at least ${min_lines})" + fi + + write_zone_allowlist "$tmp_zone" "$merged" + append_static_allowlist "$static_file" "$merged" + sort -u "$merged" >"$output_path" + + local total_entries bytes + total_entries=$(wc -l <"$output_path" | tr -d '[:space:]') + bytes=$(wc -c <"$output_path" | tr -d '[:space:]') + + rm -f "$tmp_zone" "$merged" + trap - RETURN + + GENERATED_ZONE_ENTRIES=$zone_entries + GENERATED_ENTRIES=$total_entries + GENERATED_BYTES=$bytes + GENERATED_OUTPUT=$output_path +}