diff --git a/.gitattributes b/.gitattributes index b20336a..b5dbc7b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,8 @@ zabbix-allowed text eol=lf syno-allowed text eol=lf rds-allowed-static text eol=lf scripts/*.sh text eol=lf +scripts/security/*.sh text eol=lf +scripts/security/*.example 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 d8bf7f6..70c611a 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,4 @@ DNS: **A** для `ext`, `autodiscover`, `rds`, `erp`, `git`, `sac`, **`sac-api` - [Работа с Git: clone, pull, push, ключи и PAT](docs/git.md) - [Политика: Gitea — основной, GitHub — резерв без секретов](docs/repo-policy.md) - [Let's Encrypt / ACME через HAProxy (порт 80)](docs/letsencrypt-acme.md) +- [Безопасность: UFW, бэкенды, план внедрения](docs/security/README.md) diff --git a/docs/haproxy-install.md b/docs/haproxy-install.md index 220b0d6..0518140 100644 --- a/docs/haproxy-install.md +++ b/docs/haproxy-install.md @@ -65,6 +65,8 @@ sudo haproxy -c -f /etc/haproxy/haproxy.cfg На хосте с HAProxy с интернета обычно открывают только то, что слушает конфиг: **25**, **80** (Zabbix HTTP), **443** (и при использовании — **8404** для stats). Остальной трафик — по политике безопасности. +**Полный план:** [docs/security/README.md](security/README.md) — UFW, бэкенды, чеклист, скрипты `scripts/security/`. + Порт **80** используется для **`http://zabbix.kalinamall.ru`** (allowlist `zabbix-allowed`). Для ACME на других именах см. [letsencrypt-acme.md](letsencrypt-acme.md) — потребуется расширить `fe_http_zabbix` маршрутизацией по Host/пути. ## SAC и Seaca (sac-api) diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 0000000..ebf99e3 --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,54 @@ +# Безопасность периметра HAProxy (kalinamall.ru) + +План защиты хоста **haproxy.kalinamall.ru** и **бэкендов** за ним. HAProxy слушает **25**, **80**, **443**; фильтрация по SNI/Host и allowlist уже в `haproxy.cfg`, но **UFW на хосте inactive** — ниже поэтапное включение **defense in depth**. + +## Модель угроз (кратко) + +| Уровень | Что закрывает | +|---------|----------------| +| **MikroTik / провайдер** | DDoS, geo, лишние порты до DMZ | +| **UFW на HAProxy** | Вход только 25/80/443 + SSH с доверенных IP | +| **HAProxy ACL** | Неизвестный SNI, allowlist по сервисам | +| **UFW на бэкендах** | Сервисы доступны **только с IP HAProxy** (`192.168.160.117`) | +| **Приложение** | Auth OWA, RD Gateway, Gitea, SAC, 1С | + +Обход HAProxy (VPN в `192.168.160.0/24`, прямой RDP) — отдельная политика; см. [backend-hosts.md](backend-hosts.md). + +## Документы + +| Файл | Содержание | +|------|------------| +| [architecture.md](architecture.md) | Схема потоков, зоны, принципы | +| [haproxy-host.md](haproxy-host.md) | Hardening Ubuntu, UFW, SSH, fail2ban, HAProxy | +| [backend-hosts.md](backend-hosts.md) | Правила для каждого бэкенда | +| [deployment-plan.md](deployment-plan.md) | **Пошаговый план** внедрения (фазы 0–5) | +| [checklist.md](checklist.md) | Проверки после внедрения | + +## Скрипты + +| Скрипт | Назначение | +|--------|------------| +| [scripts/security/haproxy-ufw-apply.sh](../../scripts/security/haproxy-ufw-apply.sh) | UFW на HAProxy (dry-run по умолчанию) | +| [scripts/security/haproxy-ufw.env.example](../../scripts/security/haproxy-ufw.env.example) | Пример env для HAProxy | +| [scripts/security/backend-ufw-from-haproxy.sh](../../scripts/security/backend-ufw-from-haproxy.sh) | UFW на бэкенде: порт только с HAProxy | +| [scripts/security/backend-ufw.env.example](../../scripts/security/backend-ufw.env.example) | Пример env для бэкенда | + +## Быстрый старт (после чтения deployment-plan) + +```bash +# на HAProxy — сначала dry-run +sudo cp ~/reverse-proxy/scripts/security/haproxy-ufw.env.example /etc/haproxy/security/haproxy-ufw.env +sudo nano /etc/haproxy/security/haproxy-ufw.env # SSH_ALLOWED, при необходимости порты +sudo bash ~/reverse-proxy/scripts/security/haproxy-ufw-apply.sh +sudo bash ~/reverse-proxy/scripts/security/haproxy-ufw-apply.sh --apply +``` + +На каждом бэкенде — [backend-hosts.md](backend-hosts.md) и `backend-ufw-from-haproxy.sh`. + +## Связь с конфигом HAProxy + +Allowlist-файлы в `/etc/haproxy/` (**не** заменяют UFW): + +- `git-sac-allowed`, `rds-allowed`, `1c-allowed`, `zabbix-allowed`, `syno-allowed` + +Сервисы **без** allowlist на 443: **Exchange** (`ext`, `autodiscover`), **sac-api**, **SMTP 25** — усилить на уровне приложения и мониторинга; при необходимости добавить allowlist или rate-limit (см. [haproxy-host.md](haproxy-host.md)). diff --git a/docs/security/architecture.md b/docs/security/architecture.md new file mode 100644 index 0000000..e55e58e --- /dev/null +++ b/docs/security/architecture.md @@ -0,0 +1,68 @@ +# Архитектура защиты + +## Поток трафика + +```mermaid +flowchart TB + Internet[Интернет] + MT[MikroTik / периметр] + HAP[HAProxy 192.168.160.117
25 80 443] + ACL[HAProxy ACL
SNI Host allowlist] + BE[Бэкенды 192.168.160.0/24] + + Internet --> MT + MT -->|DNAT публичный IP| HAP + HAP --> ACL + ACL --> BE +``` + +## Два контура + +### 1. Публичный вход (через HAProxy) + +Клиент → публичный IP → **HAProxy** → (проверки) → один бэкенд. + +На HAProxy уже есть: + +- **443/TCP**: отклонение неизвестного SNI; allowlist для git, sac-web, rds, erp, syno. +- **80/HTTP**: только `Host: zabbix.kalinamall.ru` + `zabbix-allowed`. +- **25/SMTP**: passthrough на KSMG без IP-фильтра на frontend. + +### 2. Внутренняя сеть (LAN / VPN) + +Трафик **мимо HAProxy** возможен, если: + +- есть VPN в `192.168.160.0/24`; +- бэкенд слушает на LAN без host firewall; +- RDP/RDP Gateway по внутренним адресам. + +Защита: **UFW на бэкендах** + политика «сервисный порт только с `.117`», админ-доступ — отдельные правила. + +## IP HAProxy (источник для бэкендов) + +| Имя | IP | +|-----|-----| +| haproxy.kalinamall.ru | **192.168.160.117** | + +Health-check HAProxy и прокси идут **с этого IP**. Правила на бэкендах должны разрешать его явно. + +## Матрица: кто фильтрует что + +| Угроза | MikroTik | UFW HAProxy | HAProxy cfg | UFW бэкенд | Приложение | +|--------|----------|-------------|-------------|------------|------------| +| Сканирование портов | ✓ | ✓ | — | ✓ | — | +| Неверный SNI / Host | — | — | ✓ | — | — | +| Доступ не из allowlist | — | — | ✓ | — | ✓ | +| Прямой удар в бэкенд | ✓ | — | — | ✓ | — | +| Brute SSH на HAProxy | ✓ | ✓ | — | — | fail2ban | +| DDoS L4 | ✓ | частично | conn limits* | — | — | + +\* Опционально: `maxconn`, rate-limit на frontend (Enterprise / custom); на 2.8 — sysctl, MikroTik, внешний scrubbing. + +## Принципы + +1. **Default deny** на вход (UFW `incoming deny`, бэкенды — только HAProxy + admin). +2. **Минимум открытых портов** с интернета: 25, 80, 443 на одном хосте. +3. **Не дублировать** смысл правил без нужды: allowlist в HAProxy для HTTP/SNI; UFW — транспорт и админ. +4. **Сначала dry-run**, потом `--apply`; для SSH — **console/IPMI** или сессия, которую UFW не оборвёт. +5. **Логи**: HAProxy + UFW + fail2ban; алерты на аномалии (Zabbix уже в инфраструктуре). diff --git a/docs/security/backend-hosts.md b/docs/security/backend-hosts.md new file mode 100644 index 0000000..a2388e8 --- /dev/null +++ b/docs/security/backend-hosts.md @@ -0,0 +1,88 @@ +# Firewall на бэкендах + +Цель: сервисный порт доступен **с HAProxy** (`192.168.160.117`) и при необходимости с **admin**-подсетей; **не** с произвольного LAN/Internet напрямую. + +Скрипт: [backend-ufw-from-haproxy.sh](../../scripts/security/backend-ufw-from-haproxy.sh). + +## Таблица бэкендов + +| Сервис | IP | Порт | Протокол | HAProxy backend | Примечание | +|--------|-----|------|----------|-----------------|------------| +| Exchange | 192.168.160.42 | 443 | TCP | be_exchange | OWA, ActiveSync | +| RDS Gateway | 192.168.160.40 | **4430** | TCP | be_rds | не 443 | +| 1С ERP | 192.168.160.150 | 443 | TCP | be_erp | | +| Git (Gitea) | 192.168.160.129 | 443 | TCP | be_git | SSH Gitea 2222 — отдельно | +| SAC | 192.168.160.145 | 443 | TCP | be_sac | | +| Zabbix | 192.168.160.53 | **80** | TCP | be_zabbix | HTTP | +| Synology heap | 192.168.160.185 | **5001** | TCP | be_syno_heap | DSM HTTPS | +| Synology exchange | 192.168.160.35 | **5001** | TCP | be_syno_exchange | | +| KSMG | 192.168.160.57 | 25 | TCP | be_ksmg_smtp | см. ниже | + +## Шаблон правил (логика) + +``` +allow from 192.168.160.117 to +allow from ADMIN_ALLOWED to 22/tcp # SSH, если на этом хосте +allow from LAN to ... # только если нужен прямой доступ (минимизировать) +deny incoming (default) +``` + +### Пример env для Git (192.168.160.129) + +```bash +HAPROXY_IP=192.168.160.117 +BACKEND_PORT=443 +ADMIN_ALLOWED="192.168.160.0/24" +UFW_EXTRA_SERVICES="2222/tcp" # если Gitea SSH с admin LAN +``` + +## Особые случаи + +### KSMG (192.168.160.57:25) + +Почта с интернета приходит на **HAProxy:25** → KSMG. На KSMG в `EXTRA_ALLOWED` укажите **192.168.160.117**. Если другие MTA шлют **напрямую** на `.57` (минуя HAProxy), их IP тоже нужно явно разрешить — иначе после UFW почта встанет. + +### RDS (192.168.160.40) + +Порт **4430**, не 443. VPN-клиенты с прямым RDP на `.3` / `.40` обходят HAProxy — ограничивать **3389/4430** на session hosts отдельной GPO/файрволом. + +### Synology + +DSM может требовать доп. порты (5000, 5001, relay). Для доступа **только через HAProxy** достаточно **5001** с `.117`. Прямой доступ к NAS с LAN — отключить или ограничить в DSM **Панель → Безопасность → Брандмауэр**. + +### Exchange + +Внутренние клиенты Outlook могут ходить **напрямую** на `.42` по LAN — UFW не сломать без списка подсетей Exchange. Варианты: + +- разрешить `192.168.160.0/24` на 443 (шире); +- или только `.117` + подсеть Exchange DAG/clients (уже). + +### Windows vs Linux + +Скрипт UFW — для **Linux**. Для Windows (Exchange, RDS, 1С) — **Windows Firewall with Advanced Security**: inbound allow **Remote IP** `192.168.160.117`, порт сервиса. + +## Развёртывание на Linux-бэкенде + +```bash +sudo mkdir -p /etc/haproxy/security +sudo cp backend-ufw.env.example /etc/haproxy/security/backend-ufw.env +sudo nano /etc/haproxy/security/backend-ufw.env +sudo bash backend-ufw-from-haproxy.sh +sudo bash backend-ufw-from-haproxy.sh --apply +``` + +## Проверка + +С **HAProxy** (или симуляция): + +```bash +curl -kI --resolve git.kalinamall.ru:443:ПУБЛИЧНЫЙ_IP https://git.kalinamall.ru/ +``` + +С **другого** LAN-хоста (не .117): + +```bash +curl -kI --connect-timeout 3 https://192.168.160.129/ # ожидается timeout/refused +``` + +В логе HAProxy — backend **UP**. diff --git a/docs/security/checklist.md b/docs/security/checklist.md new file mode 100644 index 0000000..57a9170 --- /dev/null +++ b/docs/security/checklist.md @@ -0,0 +1,44 @@ +# Чеклист после внедрения защиты + +## HAProxy (192.168.160.117) + +- [ ] `ufw status verbose` — active; есть 22 из admin, 25/80/443 open +- [ ] SSH с admin-IP работает; с random IP — **нет** (или только ключ + fail2ban) +- [ ] `ss -tlnp` — нет лишних listen (8404, 3000, …) +- [ ] `haproxy -c -f /etc/haproxy/haproxy.cfg` — OK +- [ ] С allowlist IP: git, rds, zabbix, erp, syno — **OK** +- [ ] С чужого IP: git/rds/erp — **reject** (нет TCP reset или обрыв на ACL) +- [ ] `fail2ban-client status sshd` — active +- [ ] Логи: `grep -E 'SC|NOSRV|reject' /var/log/haproxy.log | tail` + +## MikroTik / периметр + +- [ ] С интернета на публичный IP: открыты только **25, 80, 443** +- [ ] Нет DNAT 3389, 5001, 4430 на внутренние хосты +- [ ] HAProxy health-check не блокируется + +## Бэкенды (каждый) + +- [ ] UFW/firewall active +- [ ] С `.117` на сервисный порт — OK +- [ ] С другого LAN IP на сервисный порт — **closed** (если так задумано) +- [ ] HAProxy backend **UP** в stats/log +- [ ] Прикладной smoke-test (OWA, RD Web, Gitea login, …) + +## KSMG (.57) + +- [ ] Почта inbound через HAProxy:25 — OK +- [ ] Прямые отправители (если есть) — в `EXTRA_ALLOWED` + +## Документация и runbook + +- [ ] `/etc/haproxy/security/*.env` — backup в password store +- [ ] Откат UFW: `ufw disable` + процедура задокументирована +- [ ] Ответственный и дата внедрения фикс + +## Регулярно (ежемесячно) + +- [ ] `apt upgrade` на HAProxy +- [ ] Проверка актуальности allowlist / `rds-allowed` cron +- [ ] Review fail2ban / auth.log +- [ ] Тест restore конфига из git diff --git a/docs/security/deployment-plan.md b/docs/security/deployment-plan.md new file mode 100644 index 0000000..579f213 --- /dev/null +++ b/docs/security/deployment-plan.md @@ -0,0 +1,100 @@ +# План развёртывания защиты + +Оценка: **2–4 рабочих дня** с окнами обслуживания на бэкендах (по одному хосту). + +## Фаза 0 — Подготовка (без изменений на проде) + +- [ ] Зафиксировать **публичный IP** HAProxy и правила DNAT на MikroTik. +- [ ] Убедиться, что **SSH** на `192.168.160.117` доступен с admin-IP (не только с одного ноутбука). +- [ ] Снимок/backup: `haproxy.cfg`, allowlist, `/etc/ssh/sshd_config`. +- [ ] Клонировать репозиторий на HAProxy: `git pull kalinamall main`. +- [ ] Прочитать [checklist.md](checklist.md). + +**Критерий готовности:** есть консоль или второй admin-IP для отката UFW. + +--- + +## Фаза 1 — HAProxy: базовый hardening (без UFW enable) + +На **haproxy.kalinamall.ru**: + +- [ ] `apt update && apt upgrade` (или unattended-upgrades). +- [ ] SSH: `PasswordAuthentication no`, `PermitRootLogin no`, только ключи (`/etc/ssh/sshd_config.d/`). +- [ ] Установить **fail2ban** (`sshd` jail), см. [haproxy-host.md](haproxy-host.md). +- [ ] Убедиться, что **stats :8404** не включены в prod (закомментированы в `haproxy.cfg`). +- [ ] Права: `/etc/haproxy/*-allowed` — `0644 root:root`; скрипты в `/usr/local/bin` — `0755`. +- [ ] `sysctl`: syncookies, rp_filter (см. [haproxy-host.md](haproxy-host.md)). + +**Проверка:** `ss -tlnp` — только 22, 25, 80, 443 (+ systemd-resolved если локально). + +--- + +## Фаза 2 — UFW на HAProxy + +- [ ] `cp scripts/security/haproxy-ufw.env.example /etc/haproxy/security/haproxy-ufw.env` +- [ ] Заполнить **`SSH_ALLOWED`** (офис, VPN, admin; минимум ваш текущий IP + LAN). +- [ ] Dry-run: `bash scripts/security/haproxy-ufw-apply.sh` +- [ ] **Не закрывая SSH-сессию**, в second tab: `bash scripts/security/haproxy-ufw-apply.sh --apply` +- [ ] Проверить с admin-IP: `ssh haproxy.kalinamall.ru`, `curl` на 80/443 с allowlist. +- [ ] Проверить SMTP 25 с внешнего MTA (если используется). + +**Откат:** `ufw disable` (временно) или `ufw delete` по номерам правил; env-файл сохранить. + +--- + +## Фаза 3 — UFW на бэкендах (по одному хосту) + +Порядок рекомендуемый (от менее критичных к более): + +1. Zabbix `.53:80` +2. Git `.129:443` +3. SAC `.145:443` +4. Synology `.185:5001`, `.35:5001` +5. ERP `.150:443` +6. RDS `.40:4430` +7. Exchange `.42:443` +8. KSMG `.57:25` — **особый случай** (почта может идти не только с HAProxy); см. [backend-hosts.md](backend-hosts.md) + +На каждом хосте: + +- [ ] `cp backend-ufw.env.example` → `/etc/haproxy/security/backend-ufw.env` (или `/etc/security/`) +- [ ] Задать `BACKEND_PORT`, при необходимости `ADMIN_ALLOWED`, `EXTRA_ALLOWED` +- [ ] Dry-run → `--apply` +- [ ] Проверить сервис через HAProxy снаружи и health-check в логе HAProxy + +**Критерий:** с другого LAN-хоста (не `.117`) `curl https://:` **не** отвечает; с HAProxy — работает. + +--- + +## Фаза 4 — MikroTik и мониторинг + +- [ ] На MikroTik: с интернета на HAProxy только **25, 80, 443** (не пробрасывать 3389, 5001, 4430 на внутренние IP). +- [ ] Raw/firewall: опционально drop obvious scanners (согласовать с `rds-allowed` / RU geo). +- [ ] Zabbix: триггеры на `ufw status`, `fail2ban`, рост `fe_https_sni` reject, backend DOWN. +- [ ] Ротация логов `/var/log/haproxy.log`. + +--- + +## Фаза 5 — Усиление (по приоритету) + +| Задача | Зачем | +|--------|--------| +| Allowlist или rate-limit для **Exchange** | Сейчас 443 без IP-фильтра | +| Ограничить **sac-api** (если бизнес позволяет) | Сейчас без allowlist | +| TLS verify на бэкендах HAProxy | Вместо `verify none` — внутренняя CA | +| Отдельный VLAN для DMZ / HAProxy | Сегментация | +| WAF / reverse proxy HTTP для Zabbix → HTTPS | Порт 80 только HTTP | +| 2FA на Git, OWA, RD Gateway | Учётные записи | + +--- + +## Окна и коммуникация + +| Фаза | Риск | Окно | +|------|------|------| +| 1 | Низкий | любое | +| 2 | **SSH lockout** | рабочее время + console | +| 3 | Обрыв сервиса при ошибке порта | вечер / по сервису | +| 4 | Ошибка NAT | согласование с сетью | + +После каждой фазы — отметки в [checklist.md](checklist.md). diff --git a/docs/security/haproxy-host.md b/docs/security/haproxy-host.md new file mode 100644 index 0000000..243c393 --- /dev/null +++ b/docs/security/haproxy-host.md @@ -0,0 +1,138 @@ +# Hardening хоста HAProxy + +Хост: **haproxy.kalinamall.ru**, **192.168.160.117**, Ubuntu 24.04, HAProxy 2.8.x. + +## Слушающие порты (prod) + +| Порт | Frontend | Назначение | +|------|----------|------------| +| 22 | sshd | Администрирование (не в haproxy.cfg) | +| 25 | fe_smtp | SMTP → KSMG | +| 80 | fe_http_zabbix | HTTP Zabbix | +| 443 | fe_https_sni | TLS passthrough по SNI | + +Не включать **8404/stats** на `0.0.0.0` без VPN и пароля. + +## UFW + +Скрипт: [haproxy-ufw-apply.sh](../../scripts/security/haproxy-ufw-apply.sh). + +Политика: + +``` +default incoming: deny +default outgoing: allow +allow 22/tcp from SSH_ALLOWED +allow 25, 80, 443/tcp from anywhere # сервисы периметра +``` + +**SSH_ALLOWED** — несколько CIDR/ IP через пробел в env. Минимум: + +- подсеть админов / VPN; +- LAN `192.168.160.0/24` (если SSH только изнутри); +- **не** `0.0.0.0/0` без крайней необходимости. + +Перед `--apply` обязательно dry-run и **вторая SSH-сессия**. + +## SSH + +Пример drop-in `/etc/ssh/sshd_config.d/99-hardening.conf`: + +``` +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes +MaxAuthTries 3 +AllowUsers papatramp +``` + +После правок: `sshd -t && systemctl reload sshd`. + +## fail2ban + +```bash +sudo apt install -y fail2ban +``` + +`/etc/fail2ban/jail.d/sshd.local`: + +```ini +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 5 +findtime = 600 +bantime = 3600 +``` + +`systemctl enable --now fail2ban`, проверка: `fail2ban-client status sshd`. + +## HAProxy + +### Уже в конфиге + +- Reject неизвестного SNI. +- Allowlist по сервисам. +- Длинные timeout для RDP/Exchange — осознанно; не занижать без теста. + +### Рекомендуется добавить (отдельный PR / окно) + +1. **Лимит соединений** (global уже `maxconn 50000`; при атаке снизить или ограничить на frontend): + + ``` + # frontend fe_https_sni + # maxconn 10000 + ``` + +2. **Exchange / sac-api без allowlist** — рассмотреть: + - отдельный allowlist; + - или `tcp-request content reject` при превышении conn rate (HAProxy 2.8+: stick-table / sc1 — сложнее в TCP+SNI). + +3. **Логирование отказов** — grep `SC` / `` / короткие сессии в SIEM. + +4. **`ssl verify required`** на бэкендах после выпуска внутренней CA. + +### systemd + +```bash +systemctl enable haproxy +# после reload: systemctl status haproxy +``` + +## sysctl (опционально) + +`/etc/sysctl.d/99-haproxy-security.conf`: + +``` +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +``` + +`sysctl --system` + +## Обновления + +```bash +sudo apt install -y unattended-upgrades +sudo dpkg-reconfigure -plow unattended-upgrades +``` + +Исключить автоперезапуск HAProxy в неудобное окно — настроить `Unattended-Upgrade::Automatic-Reboot` по политике. + +## Права и секреты + +- Allowlist и `haproxy.cfg` — root, без world-readable секретов (в репо секретов нет). +- `/etc/haproxy/security/` — `0750 root:root`. +- Не коммитить `haproxy-ufw.env` с реальными IP в публичный GitHub (в kalinamall — по политике repo). + +## Мониторинг + +- HAProxy backend **DOWN** / **L4CON**. +- Рост отклонённых TCP (`SC`, reject по ACL). +- `ufw status numbered`, диск под логами. +- fail2ban bans. diff --git a/scripts/security/backend-ufw-from-haproxy.sh b/scripts/security/backend-ufw-from-haproxy.sh new file mode 100644 index 0000000..17df52f --- /dev/null +++ b/scripts/security/backend-ufw-from-haproxy.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# UFW на бэкенде: сервисный порт только с HAProxy (+ admin/extra). +# +# cp backend-ufw.env.example /etc/haproxy/security/backend-ufw.env +# ./backend-ufw-from-haproxy.sh --apply +set -Eeuo pipefail + +ENV_FILE="${BACKEND_UFW_ENV:-/etc/haproxy/security/backend-ufw.env}" +APPLY=0 + +die() { echo "error: $*" >&2; exit 1; } +log() { echo "[*] $*"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) APPLY=1; shift ;; + --env-file) ENV_FILE=$2; shift 2 ;; + -h | --help) + echo "Usage: $0 [--apply] [--env-file PATH]" + exit 0 + ;; + *) die "unknown option: $1" ;; + esac +done + +[[ -f $ENV_FILE ]] || die "missing env: $ENV_FILE" + +# shellcheck source=/dev/null +source "$ENV_FILE" + +HAPROXY_IP=${HAPROXY_IP:-192.168.160.117} +BACKEND_PORT=${BACKEND_PORT:-} +ADMIN_ALLOWED=${ADMIN_ALLOWED:-} +EXTRA_ALLOWED=${EXTRA_ALLOWED:-} +UFW_EXTRA_SERVICES=${UFW_EXTRA_SERVICES:-} +UFW_IPV6=${UFW_IPV6:-no} + +[[ -n $BACKEND_PORT ]] || die "BACKEND_PORT required" + +command -v ufw >/dev/null 2>&1 || die "ufw not installed" + +run() { + log "$*" + [[ $APPLY -eq 1 ]] && "$@" +} + +[[ $APPLY -eq 0 ]] && echo "=== DRY-RUN (pass --apply) ===" + +if [[ $APPLY -eq 1 ]]; then + sed -i "s/^IPV6=.*/IPV6=${UFW_IPV6}/" /etc/default/ufw 2>/dev/null || true +fi + +run ufw --force reset +run ufw default deny incoming +run ufw default allow outgoing + +run ufw allow from "$HAPROXY_IP" to any port "$BACKEND_PORT" proto tcp comment 'haproxy' + +for cidr in $EXTRA_ALLOWED; do + [[ -z $cidr ]] && continue + run ufw allow from "$cidr" to any port "$BACKEND_PORT" proto tcp comment 'extra' +done + +for cidr in $ADMIN_ALLOWED; do + [[ -z $cidr ]] && continue + run ufw allow from "$cidr" to any port 22 proto tcp comment 'admin ssh' + for svc in $UFW_EXTRA_SERVICES; do + [[ -z $svc ]] && continue + run ufw allow from "$cidr" to any port "$svc" comment 'admin extra' + done +done + +if [[ $APPLY -eq 1 ]]; then + ufw --force enable + ufw status verbose + log "done port=$BACKEND_PORT haproxy=$HAPROXY_IP" +else + echo "=== end dry-run ===" +fi diff --git a/scripts/security/backend-ufw.env.example b/scripts/security/backend-ufw.env.example new file mode 100644 index 0000000..a5b0713 --- /dev/null +++ b/scripts/security/backend-ufw.env.example @@ -0,0 +1,14 @@ +# UFW на бэкенде — пример для Git 192.168.160.129 +HAPROXY_IP=192.168.160.117 +BACKEND_PORT=443 + +# Admin SSH / мониторинг (пробел), опционально +ADMIN_ALLOWED="192.168.160.0/24" + +# Доп. порты с ADMIN_ALLOWED, напр. 2222/tcp для Gitea SSH +UFW_EXTRA_SERVICES="" + +# Доп. источники на BACKEND_PORT (напр. MTA для KSMG) +EXTRA_ALLOWED="" + +UFW_IPV6=no diff --git a/scripts/security/haproxy-ufw-apply.sh b/scripts/security/haproxy-ufw-apply.sh new file mode 100644 index 0000000..ffd614e --- /dev/null +++ b/scripts/security/haproxy-ufw-apply.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# UFW для HAProxy: default deny, SSH из SSH_ALLOWED, inbound 25/80/443. +# Dry-run по умолчанию; --apply для включения. +# +# cp haproxy-ufw.env.example /etc/haproxy/security/haproxy-ufw.env +# ./haproxy-ufw-apply.sh +# ./haproxy-ufw-apply.sh --apply +set -Eeuo pipefail + +ENV_FILE="${HAPROXY_UFW_ENV:-/etc/haproxy/security/haproxy-ufw.env}" +APPLY=0 + +die() { echo "error: $*" >&2; exit 1; } +log() { echo "[*] $*"; } + +usage() { + cat <<'EOF' +Usage: haproxy-ufw-apply.sh [--apply] [--env-file PATH] + + --apply выполнить ufw enable/reset (иначе только показ команд) + --env-file путь к env (default: /etc/haproxy/security/haproxy-ufw.env) +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) APPLY=1; shift ;; + --env-file) ENV_FILE=$2; shift 2 ;; + -h | --help) usage; exit 0 ;; + *) die "unknown option: $1" ;; + esac +done + +[[ -f $ENV_FILE ]] || die "missing env: $ENV_FILE (copy from haproxy-ufw.env.example)" + +# shellcheck source=/dev/null +source "$ENV_FILE" + +SSH_ALLOWED=${SSH_ALLOWED:-} +INBOUND_TCP=${INBOUND_TCP:-25 80 443} +UFW_IPV6=${UFW_IPV6:-no} + +[[ -n $SSH_ALLOWED ]] || die "SSH_ALLOWED is empty — lockout risk" + +command -v ufw >/dev/null 2>&1 || die "ufw not installed (apt install ufw)" + +run() { + log "$*" + [[ $APPLY -eq 1 ]] && "$@" +} + +if [[ $APPLY -eq 0 ]]; then + echo "=== DRY-RUN (pass --apply to execute) ===" +fi + +if [[ $APPLY -eq 1 ]]; then + sed -i "s/^IPV6=.*/IPV6=${UFW_IPV6}/" /etc/default/ufw 2>/dev/null || true +fi + +run ufw --force reset +run ufw default deny incoming +run ufw default allow outgoing + +for cidr in $SSH_ALLOWED; do + run ufw allow from "$cidr" to any port 22 proto tcp comment 'admin ssh' +done + +for port in $INBOUND_TCP; do + run ufw allow "$port/tcp" comment "haproxy service $port" +done + +if [[ $APPLY -eq 1 ]]; then + ufw --force enable + ufw status verbose + log "done" +else + echo "=== end dry-run ===" +fi diff --git a/scripts/security/haproxy-ufw.env.example b/scripts/security/haproxy-ufw.env.example new file mode 100644 index 0000000..e8c78e7 --- /dev/null +++ b/scripts/security/haproxy-ufw.env.example @@ -0,0 +1,11 @@ +# UFW для haproxy.kalinamall.ru — пример. +# Копия: /etc/haproxy/security/haproxy-ufw.env (не коммитить в GitHub с prod-IP при необходимости) + +# IP/CIDR для SSH (через пробел). Обязательно перед --apply. +SSH_ALLOWED="192.168.160.0/24 5.100.81.121" + +# Публичные сервисы HAProxy (пробел) +INBOUND_TCP="25 80 443" + +# IPv6: yes | no +UFW_IPV6=no