docs: add HARDENING.md — step-by-step DPI bypass and server hardening guide

Covers 7 issues: WireGuard tunnel, zapret/nfqws for DPI, SNI protection,
dnscrypt-proxy, HSTS, SSE timeout, wstunnel fallback. Includes commands,
verification checklist, and rollback for each step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
delta-cloud-208e
2026-02-21 15:25:18 +00:00
parent 812b1f19c6
commit c3f5f94477

728
HARDENING.md Normal file
View File

@@ -0,0 +1,728 @@
# HARDENING.md — Защита Gitea от DPI/ТСПУ и повышение стабильности
> **Серверы:** sensey24.ru (45.132.50.182) + core-gitlab-01 (1.1.1.122)
> **Дата аудита:** 2026-02-21
> **Gitea:** 1.24.5, порт 3000 на core-gitlab-01
---
## Текущее состояние
| Компонент | Статус | Примечание |
|-----------|--------|------------|
| TCP tuning (BBR, буферы) | OK | sysctl настроен на обоих серверах |
| MSS clamping (1200) | OK | iptables TCPMSS на sensey24.ru |
| TCP keepalive | OK | net.ipv4.tcp_keepalive_time=60 |
| Apache ProxyPass | OK | Gitea проксируется через ppp0 (20.0.0.2:3000) |
| IPsec (strongSwan) | ПРОБЛЕМА | SA = 0, туннель без шифрования |
| DPI/ТСПУ защита | НЕТ | zapret, obfs4, stunnel не установлены |
| SNI скрытие | НЕТ | TLS ClientHello в открытом виде |
| DNS шифрование | НЕТ | 8.8.8.8 plaintext |
| HSTS | НЕТ | Заголовок не настроен |
| WireGuard | ЧАСТИЧНО | Установлен на sensey24.ru, не настроен |
---
## Проблема 1: IPsec SA = 0 → Замена на WireGuard
### Почему WireGuard вместо IPsec
- strongSwan показывает 0 Security Associations — туннель фактически не шифрует
- WireGuard проще в настройке, быстрее, меньше attack surface
- WireGuard уже установлен на sensey24.ru
- UDP 51820 меньше подвержен DPI-блокировкам, чем ESP/IKE
### 1.1 Генерация ключей
**На sensey24.ru:**
```bash
ssh -p 31216 robot@sensey24.ru
# Ключи уже могут быть — проверяем
ls /etc/wireguard/
# Генерация ключей сервера
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key
```
**На core-gitlab-01:**
```bash
ssh -p 7913 robot@sensey24.ru # это проброс на core-gitlab-01
# Установка WireGuard
sudo apt update && sudo apt install -y wireguard
# Генерация ключей клиента
wg genkey | tee /etc/wireguard/client_private.key | wg pubkey > /etc/wireguard/client_public.key
chmod 600 /etc/wireguard/client_private.key
```
### 1.2 Настройка на sensey24.ru (WG-сервер)
```bash
# Подставить реальные ключи
SERVER_PRIVATE=$(cat /etc/wireguard/server_private.key)
CLIENT_PUBLIC=$(cat /etc/wireguard/client_public.key) # скопировать с core-gitlab-01
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = 10.10.10.1/24
ListenPort = 51820
PrivateKey = ${SERVER_PRIVATE}
# Разрешить форвардинг
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
# core-gitlab-01
PublicKey = ${CLIENT_PUBLIC}
AllowedIPs = 10.10.10.2/32
EOF
chmod 600 /etc/wireguard/wg0.conf
```
```bash
# Включение
sudo systemctl enable wg-quick@wg0
sudo wg-quick up wg0
# Открыть порт
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
sudo netfilter-persistent save
```
### 1.3 Настройка на core-gitlab-01 (WG-клиент)
```bash
CLIENT_PRIVATE=$(cat /etc/wireguard/client_private.key)
SERVER_PUBLIC=$(cat /etc/wireguard/server_public.key) # скопировать с sensey24.ru
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = 10.10.10.2/24
PrivateKey = ${CLIENT_PRIVATE}
# Keepalive через NAT
[Peer]
PublicKey = ${SERVER_PUBLIC}
Endpoint = 45.132.50.182:51820
AllowedIPs = 10.10.10.1/32
PersistentKeepalive = 25
EOF
chmod 600 /etc/wireguard/wg0.conf
```
```bash
sudo systemctl enable wg-quick@wg0
sudo wg-quick up wg0
```
### 1.4 Миграция Apache ProxyPass с ppp0 на wg0
**На sensey24.ru** — обновить конфиг Apache:
```bash
# Найти текущий конфиг с ProxyPass
grep -r "20.0.0.2" /etc/apache2/
# Заменить IP в ProxyPass
# Было: ProxyPass / http://20.0.0.2:3000/
# Стало: ProxyPass / http://10.10.10.2:3000/
sudo sed -i 's|20\.0\.0\.2|10.10.10.2|g' /etc/apache2/sites-enabled/*.conf
# Проверить синтаксис и перезагрузить
sudo apache2ctl configtest
sudo systemctl reload apache2
```
### 1.5 Тест
```bash
# На sensey24.ru:
ping -c 3 10.10.10.2
curl -s -o /dev/null -w "%{http_code}" http://10.10.10.2:3000/
# Проверить WG-туннель:
sudo wg show
# Ожидаемый результат:
# interface: wg0
# latest handshake: <недавнее время>
# transfer: X received, Y sent
```
### 1.6 Откат
```bash
# На sensey24.ru — вернуть Apache на ppp0:
sudo sed -i 's|10\.10\.10\.2|20.0.0.2|g' /etc/apache2/sites-enabled/*.conf
sudo systemctl reload apache2
# Остановить WG:
sudo wg-quick down wg0
sudo systemctl disable wg-quick@wg0
```
---
## Проблема 2: DPI/ТСПУ блокирует HTTPS → zapret (nfqws)
### Почему zapret
- zapret/nfqws модифицирует исходящие TLS ClientHello пакеты
- Ломает DPI-сигнатуры без изменения для клиента
- Работает прозрачно на уровне iptables/nfqueue
- Не требует клиентского ПО
### 2.1 Установка на sensey24.ru
```bash
ssh -p 31216 robot@sensey24.ru
cd /opt
sudo git clone --depth 1 https://github.com/bol-van/zapret.git
cd zapret
# Сборка
sudo make -C nfq
# Проверить бинарник
ls -la nfq/nfqws
```
### 2.2 Конфигурация nfqws для HTTPS
```bash
# Создать конфиг
sudo mkdir -p /etc/zapret
cat > /etc/zapret/nfqws.conf << 'EOF'
# Стратегия для обхода DPI на порту 443 (HTTPS/TLS)
# --dpi-desync=fake,split2 — отправляет фейковый ClientHello + разбивает настоящий
# --dpi-desync-ttl=6 — фейковый пакет "умирает" до DPI, но после маршрутизатора
NFQWS_ARGS="--dpi-desync=fake,split2 --dpi-desync-ttl=6 --dpi-desync-fooling=md5sig"
EOF
```
### 2.3 Правило iptables для перенаправления в NFQUEUE
```bash
# Перехватываем исходящий HTTPS-трафик
sudo iptables -t mangle -A POSTROUTING -p tcp --dport 443 -m connbytes --connbytes 1:6 --connbytes-mode packets --connbytes-dir=original -j NFQUEUE --queue-num 200 --queue-bypass
# Сохранить
sudo netfilter-persistent save
```
### 2.4 Systemd unit
```bash
cat > /etc/systemd/system/nfqws.service << 'EOF'
[Unit]
Description=nfqws DPI bypass
After=network.target
Wants=network.target
[Service]
Type=simple
EnvironmentFile=/etc/zapret/nfqws.conf
ExecStart=/opt/zapret/nfq/nfqws --qnum=200 $NFQWS_ARGS
Restart=always
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable nfqws
sudo systemctl start nfqws
```
### 2.5 Тест
```bash
# Проверить что nfqws запущен
sudo systemctl status nfqws
# Проверить что пакеты идут через NFQUEUE
sudo iptables -t mangle -L POSTROUTING -v -n | grep NFQUEUE
# Тест подключения с внешнего хоста
curl -v --connect-timeout 10 https://git.sensey24.ru/
```
### 2.6 Откат
```bash
sudo systemctl stop nfqws
sudo systemctl disable nfqws
sudo iptables -t mangle -D POSTROUTING -p tcp --dport 443 -m connbytes --connbytes 1:6 --connbytes-mode packets --connbytes-dir=original -j NFQUEUE --queue-num 200 --queue-bypass
sudo netfilter-persistent save
```
---
## Проблема 3: SNI в открытом виде
### Вариант A: zapret --dpi-desync (уже решает)
Конфигурация из Проблемы 2 уже разбивает TLS ClientHello. Параметр `split2` отделяет SNI от начала handshake, что ломает простые DPI-фильтры по SNI.
Для усиления можно добавить `--dpi-desync-split-pos=1` в `/etc/zapret/nfqws.conf`:
```bash
# Обновить конфиг
sudo sed -i 's|NFQWS_ARGS="|NFQWS_ARGS="--dpi-desync-split-pos=1 |' /etc/zapret/nfqws.conf
sudo systemctl restart nfqws
```
### Вариант B: Cloudflare Proxy (опционально)
Если запрос идёт через Cloudflare, SNI показывает IP Cloudflare, а не реальный сервер.
1. Добавить домен `git.sensey24.ru` в Cloudflare
2. Включить проксирование (оранжевое облачко)
3. В настройках SSL → Full (Strict)
4. В Apache добавить заголовки для реального IP:
```apache
# /etc/apache2/sites-enabled/git.sensey24.ru.conf
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22
```
> **Минус:** Cloudflare добавляет задержку и видит весь трафик. Для приватного Git-сервера zapret предпочтительнее.
---
## Проблема 4: DNS в открытом виде → dnscrypt-proxy
### 4.1 Установка на sensey24.ru
```bash
sudo apt update && sudo apt install -y dnscrypt-proxy
```
### 4.2 Конфигурация
```bash
sudo cp /etc/dnscrypt-proxy/dnscrypt-proxy.toml /etc/dnscrypt-proxy/dnscrypt-proxy.toml.bak
cat > /etc/dnscrypt-proxy/dnscrypt-proxy.toml << 'EOF'
listen_addresses = ['127.0.0.53:53']
max_clients = 250
# Серверы с поддержкой DoH и DNSSEC
server_names = ['cloudflare', 'google', 'scaleway-fr']
[query_log]
file = '/var/log/dnscrypt-proxy/query.log'
[sources]
[sources.'public-resolvers']
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md',
'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md']
cache_file = '/var/cache/dnscrypt-proxy/public-resolvers.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
EOF
```
### 4.3 Отключение systemd-resolved и переключение DNS
```bash
# Остановить systemd-resolved (если используется)
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
# Настроить resolv.conf
sudo rm -f /etc/resolv.conf
echo "nameserver 127.0.0.53" | sudo tee /etc/resolv.conf
sudo chattr +i /etc/resolv.conf # защита от перезаписи
# Запуск
sudo systemctl enable dnscrypt-proxy
sudo systemctl restart dnscrypt-proxy
```
### 4.4 Проверка
```bash
# Проверить что dnscrypt-proxy слушает
ss -ulnp | grep 53
# Проверить резолвинг
dig git.sensey24.ru @127.0.0.53
# Убедиться что DNS НЕ идёт в plaintext (должен быть пустой вывод)
sudo tcpdump -i eth0 -n port 53 -c 10 &
dig google.com
# Если tcpdump ничего не показывает — DNS зашифрован
```
### 4.5 Откат
```bash
sudo chattr -i /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
sudo systemctl stop dnscrypt-proxy
sudo systemctl disable dnscrypt-proxy
sudo systemctl enable systemd-resolved
sudo systemctl start systemd-resolved
```
---
## Проблема 5: Нет HSTS → Apache header
### 5.1 Включение
```bash
ssh -p 31216 robot@sensey24.ru
# Включить модуль headers
sudo a2enmod headers
# Добавить HSTS в конфиг SSL-сайта
sudo sed -i '/<VirtualHost \*:443>/a\ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"' /etc/apache2/sites-enabled/*ssl*.conf
# Если файл называется иначе — найти нужный:
grep -rl "443" /etc/apache2/sites-enabled/
# Перезагрузить
sudo apache2ctl configtest && sudo systemctl reload apache2
```
### 5.2 Проверка
```bash
curl -sI https://git.sensey24.ru/ | grep -i strict
# Ожидаемый результат:
# Strict-Transport-Security: max-age=31536000; includeSubDomains
```
### 5.3 Откат
```bash
sudo sed -i '/Strict-Transport-Security/d' /etc/apache2/sites-enabled/*ssl*.conf
sudo systemctl reload apache2
```
---
## Проблема 6: Apache SSE timeout → ProxyTimeout для EventSource
### Почему
Gitea использует Server-Sent Events (SSE) для real-time обновлений. Стандартный `ProxyTimeout 60` обрывает SSE-соединения.
### 6.1 Добавить location-specific timeout
```bash
ssh -p 31216 robot@sensey24.ru
# Найти конфиг виртуального хоста
grep -rl "ProxyPass" /etc/apache2/sites-enabled/
```
Добавить в конфиг виртуального хоста (внутри `<VirtualHost *:443>`):
```bash
cat >> /tmp/sse-patch.conf << 'EOF'
# SSE/EventSource — длинные соединения
<Location "/user/events">
ProxyPass http://10.10.10.2:3000/user/events
ProxyPassReverse http://10.10.10.2:3000/user/events
ProxyTimeout 3600
</Location>
# Gitea API streaming
<Location "/api/v1">
ProxyTimeout 300
</Location>
EOF
```
Вставить перед закрывающим `</VirtualHost>`:
```bash
# Определить номер строки </VirtualHost> в SSL-конфиге
CONF=$(grep -rl "ProxyPass.*3000" /etc/apache2/sites-enabled/)
LINE=$(grep -n "</VirtualHost>" "$CONF" | tail -1 | cut -d: -f1)
sudo sed -i "${LINE}r /tmp/sse-patch.conf" "$CONF"
sudo apache2ctl configtest && sudo systemctl reload apache2
```
### 6.2 Проверка
```bash
# SSE должен держаться дольше 60 секунд
curl -N -H "Accept: text/event-stream" https://git.sensey24.ru/user/events &
sleep 70
# Если соединение живо через 70 секунд — работает
kill %1
```
### 6.3 Откат
Удалить добавленные блоки `<Location>` из конфига и `sudo systemctl reload apache2`.
---
## Проблема 7: WireGuard через wstunnel (если WG блокируют)
### Когда нужно
Если провайдер/ТСПУ блокирует UDP 51820 (WireGuard), трафик оборачивается в WebSocket поверх HTTPS (порт 443), что выглядит как обычный HTTPS.
### 7.1 Установка wstunnel на обоих серверах
```bash
# На sensey24.ru и core-gitlab-01:
WSTUNNEL_VERSION="10.1.0"
wget "https://github.com/erebe/wstunnel/releases/download/v${WSTUNNEL_VERSION}/wstunnel_${WSTUNNEL_VERSION}_linux_amd64.tar.gz"
tar xzf "wstunnel_${WSTUNNEL_VERSION}_linux_amd64.tar.gz"
sudo mv wstunnel /usr/local/bin/
sudo chmod +x /usr/local/bin/wstunnel
# Проверка
wstunnel --version
```
### 7.2 Серверная сторона (sensey24.ru)
```bash
cat > /etc/systemd/system/wstunnel-server.service << 'EOF'
[Unit]
Description=wstunnel server (WireGuard over WSS)
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/wstunnel server \
--restrict-to 127.0.0.1:51820 \
wss://0.0.0.0:8443
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable wstunnel-server
sudo systemctl start wstunnel-server
# Открыть порт
sudo iptables -A INPUT -p tcp --dport 8443 -j ACCEPT
sudo netfilter-persistent save
```
При этом WireGuard должен слушать на `127.0.0.1:51820`. Обновить `/etc/wireguard/wg0.conf`:
```ini
[Interface]
Address = 10.10.10.1/24
ListenPort = 51820
# Привязать к localhost — трафик приходит через wstunnel
PostUp = ip route add 10.10.10.0/24 dev wg0
```
### 7.3 Клиентская сторона (core-gitlab-01)
```bash
cat > /etc/systemd/system/wstunnel-client.service << 'EOF'
[Unit]
Description=wstunnel client (WireGuard over WSS)
After=network.target
Before=wg-quick@wg0.service
[Service]
Type=simple
ExecStart=/usr/local/bin/wstunnel client \
--local-to-remote udp://127.0.0.1:51820:127.0.0.1:51820 \
wss://45.132.50.182:8443
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable wstunnel-client
sudo systemctl start wstunnel-client
```
Обновить `/etc/wireguard/wg0.conf` на core-gitlab-01:
```ini
[Interface]
Address = 10.10.10.2/24
PrivateKey = <CLIENT_PRIVATE_KEY>
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
# Endpoint теперь через wstunnel (localhost)
Endpoint = 127.0.0.1:51820
AllowedIPs = 10.10.10.1/32
PersistentKeepalive = 25
```
```bash
sudo systemctl restart wg-quick@wg0
```
### 7.4 Тест
```bash
# На core-gitlab-01:
ping -c 3 10.10.10.1
# Проверить что wstunnel работает:
sudo systemctl status wstunnel-client
ss -tnp | grep 8443
# Должно показать TCP-соединение к 45.132.50.182:8443
# На sensey24.ru:
sudo wg show
# Должен показать handshake и transfer
```
### 7.5 Откат
```bash
# На core-gitlab-01:
sudo systemctl stop wstunnel-client
sudo systemctl disable wstunnel-client
# Вернуть Endpoint в wg0.conf на 45.132.50.182:51820
sudo systemctl restart wg-quick@wg0
# На sensey24.ru:
sudo systemctl stop wstunnel-server
sudo systemctl disable wstunnel-server
sudo iptables -D INPUT -p tcp --dport 8443 -j ACCEPT
sudo netfilter-persistent save
```
---
## Верификация — полный чеклист
После применения всех изменений выполнить проверки:
### Инфраструктура
```bash
# 1. WireGuard туннель активен
sudo wg show
# Ожидание: handshake < 2 минут назад, transfer > 0
# 2. Ping через WG
ping -c 5 10.10.10.2 # с sensey24.ru
ping -c 5 10.10.10.1 # с core-gitlab-01
# 3. Gitea доступна через WG
curl -s -o /dev/null -w "%{http_code}" http://10.10.10.2:3000/
# Ожидание: 200
```
### DPI/ТСПУ обход
```bash
# 4. nfqws запущен
sudo systemctl is-active nfqws
# Ожидание: active
# 5. NFQUEUE ловит пакеты
sudo iptables -t mangle -L POSTROUTING -v -n | grep "NFQUEUE.*200"
# Ожидание: счётчик pkts > 0
# 6. Внешний доступ к Gitea
curl -v --connect-timeout 15 https://git.sensey24.ru/
# Ожидание: HTTP 200, без timeout
```
### DNS
```bash
# 7. dnscrypt-proxy работает
sudo systemctl is-active dnscrypt-proxy
# Ожидание: active
# 8. DNS через зашифрованный канал
dig +short google.com @127.0.0.53
# Ожидание: IP-адрес
# 9. Нет plaintext DNS
sudo timeout 10 tcpdump -i eth0 -n port 53 -c 1 2>/dev/null
dig google.com > /dev/null 2>&1
# Ожидание: tcpdump не перехватил пакетов
```
### HTTPS/TLS
```bash
# 10. HSTS заголовок присутствует
curl -sI https://git.sensey24.ru/ | grep -i "strict-transport"
# Ожидание: Strict-Transport-Security: max-age=31536000
# 11. SSL Labs (внешний тест)
# Открыть: https://www.ssllabs.com/ssltest/analyze.html?d=git.sensey24.ru
# Ожидание: A или A+
```
### Производительность
```bash
# 12. SSE держится > 60 сек
timeout 75 curl -s -N -H "Accept: text/event-stream" https://git.sensey24.ru/user/events > /dev/null 2>&1
echo "Exit code: $?"
# Ожидание: exit code 124 (killed by timeout, не обрыв соединения)
# 13. Git clone работает
cd /tmp && git clone https://git.sensey24.ru/aibot777/test-repo.git 2>&1 | tail -1
rm -rf test-repo
# Ожидание: успешное клонирование
```
### Сводная таблица (после применения)
| # | Проверка | Команда | Ожидание |
|---|----------|---------|----------|
| 1 | WG туннель | `sudo wg show` | handshake < 2 мин |
| 2 | WG ping | `ping 10.10.10.2` | 0% loss |
| 3 | Gitea через WG | `curl http://10.10.10.2:3000/` | 200 |
| 4 | nfqws | `systemctl is-active nfqws` | active |
| 5 | NFQUEUE | `iptables -t mangle -L` | pkts > 0 |
| 6 | Внешний доступ | `curl https://git.sensey24.ru/` | 200 |
| 7 | dnscrypt | `systemctl is-active dnscrypt-proxy` | active |
| 8 | DNS resolve | `dig @127.0.0.53 google.com` | ответ есть |
| 9 | No plaintext DNS | `tcpdump port 53` | пусто |
| 10 | HSTS | `curl -sI \| grep strict` | заголовок есть |
| 11 | SSL Labs | браузер | A/A+ |
| 12 | SSE > 60s | `timeout 75 curl SSE` | exit 124 |
| 13 | Git clone | `git clone` | success |
---
## Порядок применения (рекомендуемый)
1. **WireGuard** (Проблема 1) — база для шифрованного канала
2. **Миграция Apache** (1.4) — переключить ProxyPass на wg0
3. **zapret/nfqws** (Проблема 2) — обход DPI
4. **HSTS** (Проблема 5) — быстро, одна команда
5. **SSE timeout** (Проблема 6) — стабильность
6. **dnscrypt-proxy** (Проблема 4) — DNS шифрование
7. **wstunnel** (Проблема 7) — только если WG заблокирован
> Каждый шаг независим и может быть откачен отдельно. Рекомендуется применять по одному и тестировать после каждого.