- Автор темы
- Администратор
- Модер.
- #1
FreeBSD 14.5 + pf: часть 3. Изолированные jail-ы для nginx, PHP-FPM и MySQL
В первой и второй частях мы настроили pf, защиту от брутфорса, blacklistd, Cloudflare и GeoIP. Теперь разнесём веб-стек по отдельным jail-ам:
- nginx — единственный сервис, доступный из интернета;
- php — PHP-FPM, принимает запросы только от nginx;
- db — MySQL, принимает запросы только от PHP и вообще не имеет выхода в интернет.
Зачем это нужно: если через уязвимость в плагине или движке злоумышленник получит доступ к PHP, он окажется заперт в jail-е. Он не увидит процессы хоста, не прочитает конфиги других сервисов и сможет подключиться только к MySQL, и то лишь на порт 3306. Всё остальное режет pf.
Схема
Код:
Интернет
│
┌──────┴──────┐
│ vtnet0 │ внешний IP
│ pf: rdr │ 80/443 → 10.10.0.10
│ pf: nat │ 10.10.0.0/24 → внешний IP
└──────┬──────┘
│ lo1 (10.10.0.0/24)
┌───────────────┼───────────────┐
┌────┴─────┐ ┌─────┴────┐ ┌─────┴────┐
│ nginx │──►│ php │───►│ db │
│10.10.0.10│ │10.10.0.20│ │10.10.0.30│
│ :80 :443 │ │ :9000 │ │ :3306 │
└──────────┘ └──────────┘ └──────────┘
интернет: да интернет: да интернет: НЕТ
1. Виртуальный интерфейс lo1
Jail-ы получат адреса из приватной сети на отдельном loopback-интерфейсе:
Код:
sysrc cloned_interfaces+="lo1"
service netif cloneup
ifconfig lo1
2. Создаём jail-ы
Скачиваем базовую систему 14.5 и разворачиваем её в три каталога:
Код:
mkdir -p /usr/local/jails/media /usr/local/jails/containers
fetch https://download.freebsd.org/releases/amd64/14.5-RELEASE/base.txz \
-o /usr/local/jails/media/14.5-RELEASE-base.txz
for j in nginx php db; do
mkdir -p /usr/local/jails/containers/$j
tar -xf /usr/local/jails/media/14.5-RELEASE-base.txz \
-C /usr/local/jails/containers/$j --unlink
cp /etc/resolv.conf /etc/localtime /usr/local/jails/containers/$j/etc/
freebsd-update -b /usr/local/jails/containers/$j \
--currently-running 14.5-RELEASE fetch install
done
Если сервер на ZFS, удобнее создать для каждого jail-а отдельный датасет (zfs create -p zroot/jails/containers/nginxи т.д.). Так можно делать снапшоты перед обновлениями и откатываться за секунду.
3. Общий каталог сайта
Файлы сайта нужны и nginx (статика), и PHP (скрипты). Храним их на хосте и монтируем в оба jail-а через nullfs. В nginx — только для чтения:
Код:
mkdir -p /usr/local/jails/www/site
mkdir -p /usr/local/jails/containers/nginx/usr/local/www/site
mkdir -p /usr/local/jails/containers/php/usr/local/www/site
Файл
/usr/local/jails/nginx.fstab:
Код:
/usr/local/jails/www/site /usr/local/jails/containers/nginx/usr/local/www/site nullfs ro 0 0
Файл
/usr/local/jails/php.fstab:
Код:
/usr/local/jails/www/site /usr/local/jails/containers/php/usr/local/www/site nullfs rw 0 0
Пустой файл для db:
Код:
touch /usr/local/jails/db.fstab
Пользователь
www во всех jail-ах имеет одинаковый UID 80, поэтому с правами на файлы проблем не будет:
Код:
chown -R 80:80 /usr/local/jails/www/site
4. /etc/jail.conf
Код:
# Общие параметры
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.clean;
mount.devfs;
devfs_ruleset = 4;
path = "/usr/local/jails/containers/$name";
host.hostname = "$name.jail";
mount.fstab = "/usr/local/jails/$name.fstab";
# Ужесточение
enforce_statfs = 2;
allow.raw_sockets = 0;
children.max = 0;
securelevel = 2;
nginx {
ip4.addr = "lo1|10.10.0.10/32";
}
php {
ip4.addr = "lo1|10.10.0.20/32";
}
db {
ip4.addr = "lo1|10.10.0.30/32";
}
| Параметр | Назначение |
|---|---|
enforce_statfs = 2 | Jail видит только свои точки монтирования |
allow.raw_sockets = 0 | Запрет raw-сокетов: никаких сниферов и самодельных пакетов изнутри |
children.max = 0 | Нельзя создавать вложенные jail-ы |
securelevel = 2 | Нельзя снимать флаги immutable и писать напрямую в диски |
Для PostgreSQL вместо MySQL в блок db нужно добавить
allow.sysvipc = 1;.Автозапуск в правильном порядке (сначала база, потом PHP, потом nginx):
Код:
sysrc jail_enable="YES"
sysrc jail_list="db php nginx"
5. pf.conf с NAT и rdr
Это главная часть. Прежде чем писать правила, важно понять одну тонкость.
Ловушка: во FreeBSD маршрут к любому локальному адресу (в том числе к адресам на lo1) идёт через lo0. Значит, трафик между jail-ами проходит через lo0. Если оставить
set skip on lo0из первой части, jail-ы смогут свободно общаться друг с другом, и вся изоляция пропадёт. Поэтомуset skip on lo0убираем, а loopback хоста разрешаем отдельным правилом.
Проверить маршрут можно командойnetstat -rn | grep 10.10.0.
Также учтите про синтаксис: в FreeBSD 14.x используется классический синтаксис трансляции
nat on ... -> и rdr on ... ->. Стиль OpenBSD (nat-to / rdr-to) поддерживается начиная с ветки 15.Полный
/etc/pf.conf с учётом всех частей:
Код:
#==============================================
# /etc/pf.conf — FreeBSD 14.5, веб-стек в jail-ах
#==============================================
### Макросы
ext_if = "vtnet0"
ssh_port = "22"
web_ports = "{ 80, 443 }"
admin_ips = "{ 203.0.113.10 }"
icmp_types = "{ echoreq, unreach }"
jail_net = "10.10.0.0/24"
nginx_ip = "10.10.0.10"
php_ip = "10.10.0.20"
db_ip = "10.10.0.30"
# Каким jail-ам разрешён выход в интернет
nat_jails = "{ 10.10.0.10, 10.10.0.20 }"
### Таблицы
table <bruteforce> persist file "/etc/pf.bruteforce"
table <abusers> persist
table <blacklist> persist file "/etc/pf.blacklist"
### Опции (set skip on lo0 НЕТ — см. выше)
set block-policy drop
set loginterface $ext_if
set limit states 200000
### Нормализация
scrub in on $ext_if all fragment reassemble
### Трансляция (строго до правил фильтрации!)
# Выход в интернет для nginx и php
nat on $ext_if inet from $nat_jails to any -> ($ext_if)
# Входящий веб-трафик — в jail nginx
rdr on $ext_if inet proto tcp from any to ($ext_if) port $web_ports -> $nginx_ip
rdr on $ext_if inet proto udp from any to ($ext_if) port 443 -> $nginx_ip
### Политика по умолчанию
block log all
### Loopback хоста — свободно, межджейловый трафик — по правилам
pass quick on lo0 from ! $jail_net to ! $jail_net
antispoof quick for $ext_if
anchor "blacklistd/*" in on $ext_if
### Блок-листы
block drop in quick on $ext_if from <blacklist>
block drop in quick on $ext_if from <bruteforce>
block drop in quick on $ext_if from <abusers>
### Не выпускать в интернет непротранслированные адреса jail-ов (это db)
block out log quick on $ext_if from $jail_net
### Исходящий трафик
pass out quick on $ext_if keep state
### SSH на хост
pass in on $ext_if inet proto tcp from any to ($ext_if) port $ssh_port \
flags S/SA keep state \
(max-src-conn 10, max-src-conn-rate 5/30, \
overload <bruteforce> flush global)
### Веб → nginx
# после rdr правило видит уже адрес jail-а, а не внешний IP!
pass in on $ext_if inet proto tcp from any to $nginx_ip port $web_ports \
flags S/SA keep state \
(max-src-conn 200, max-src-conn-rate 100/5, \
overload <abusers> flush global)
pass in on $ext_if inet proto udp from any to $nginx_ip port 443 keep state
### Межджейловый трафик: только то, что нужно
pass quick proto tcp from $nginx_ip to $php_ip port 9000 flags S/SA keep state
pass quick proto tcp from $php_ip to $db_ip port 3306 flags S/SA keep state
### Каждый jail может обращаться сам к себе (127.0.0.1 внутри jail = его IP)
pass quick from $nginx_ip to $nginx_ip
pass quick from $php_ip to $php_ip
pass quick from $db_ip to $db_ip
### ICMP
pass in on $ext_if inet proto icmp all icmp-type $icmp_types keep state
pass in on $ext_if inet6 proto ipv6-icmp all keep state
Как это работает
| Правило | Что делает |
|---|---|
nat ... from $nat_jails | nginx и php выходят в интернет от имени внешнего IP (обновления, письма с форума, проверка обновлений XenForo) |
rdr ... -> $nginx_ip | Запросы на 80/443 внешнего IP перенаправляются в jail nginx |
block out ... from $jail_net | db не попадает под NAT, и его пакеты в интернет отбрасываются |
nginx → php :9000 | Единственный разрешённый путь к PHP-FPM |
php → db :3306 | Единственный разрешённый путь к MySQL |
block log all | nginx не достучится до MySQL, db не достучится до PHP, никакой jail не достучится до SSH хоста |
Пересылку пакетов (
gateway_enable) включать не нужно: трафик jail-ов локальный для хоста.Применяем по правилу из первой части:
Код:
pfctl -nf /etc/pf.conf
nohup sh -c 'sleep 300 && pfctl -d' >/dev/null 2>&1 &
pfctl -f /etc/pf.conf
# проверили, что SSH жив, — отменили таймер:
pkill -f "sleep 300"
6. Запуск jail-ов и установка софта
Код:
service jail start
jls
Должно получиться:
Код:
JID IP Address Hostname Path
1 10.10.0.30 db.jail /usr/local/jails/containers/db
2 10.10.0.20 php.jail /usr/local/jails/containers/php
3 10.10.0.10 nginx.jail /usr/local/jails/containers/nginx
Jail nginx
Код:
pkg -j nginx install -y nginx
sysrc -j nginx nginx_enable="YES"
Фрагмент
/usr/local/jails/containers/nginx/usr/local/etc/nginx/nginx.conf:
Код:
server {
listen 80;
server_name example.com;
root /usr/local/www/site;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$uri&$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass 10.10.0.20:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# закрываем служебные каталоги XenForo
location ~ ^/(internal_data|library|src)/ {
deny all;
}
}
Код:
service -j nginx nginx start
Jail php
Код:
pkg -j php install -y php83 php83-extensions php83-mysqli php83-pdo_mysql \
php83-gd php83-curl php83-mbstring php83-intl php83-zip php83-exif
sysrc -j php php_fpm_enable="YES"
cp /usr/local/jails/containers/php/usr/local/etc/php.ini-production \
/usr/local/jails/containers/php/usr/local/etc/php.ini
В
/usr/local/jails/containers/php/usr/local/etc/php-fpm.d/www.conf измените:
Код:
listen = 10.10.0.20:9000
listen.allowed_clients = 10.10.0.10
listen.allowed_clients — второй рубеж защиты: даже если pf случайно пропустит лишнее, PHP-FPM ответит только nginx.
Код:
service -j php php_fpm start
Jail db
У db нет выхода в интернет, поэтому
pkg -j внутри него не сработает. Ставим пакет с хоста прямо в каталог jail-а:
Код:
pkg -r /usr/local/jails/containers/db install -y mysql84-server
sysrc -j db mysql_enable="YES"
sysrc -j db mysql_args="--bind-address=10.10.0.30"
service -j db mysql-server start
Создаём базу и пользователя, которому разрешено подключаться только с адреса jail-а php:
Код:
jexec db mysql -u root
CREATE DATABASE xenforo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'xf'@'10.10.0.20' IDENTIFIED BY 'сложный_пароль';
GRANT ALL PRIVILEGES ON xenforo.* TO 'xf'@'10.10.0.20';
FLUSH PRIVILEGES;
В
src/config.php XenForo:
Код:
$config['db']['host'] = '10.10.0.30';
$config['db']['port'] = '3306';
$config['db']['username'] = 'xf';
$config['db']['password'] = 'сложный_пароль';
$config['db']['dbname'] = 'xenforo';
7. Проверяем изоляцию
Пока не проверите изоляцию руками, не считайте её настроенной. Все команды ниже выполняются с хоста.
Код:
# nginx → php:9000 — должно работать
jexec nginx nc -zv -w 3 10.10.0.20 9000
# nginx → db:3306 — должно висеть и отвалиться по таймауту
jexec nginx nc -zv -w 3 10.10.0.30 3306
# php → db:3306 — должно работать
jexec php nc -zv -w 3 10.10.0.30 3306
# db → интернет — должно НЕ работать
jexec db fetch -T 5 -o /dev/null https://www.freebsd.org
# php → SSH хоста — должно НЕ работать
jexec php nc -zv -w 3 ВАШ_ВНЕШНИЙ_IP 22
Заблокированные попытки видны в логе, вместе с интерфейсом, через который шёл пакет:
Код:
tcpdump -n -e -ttt -i pflog0
Полезные команды:
Код:
pfctl -s nat # активные правила nat/rdr
pfctl -s states | grep 10.10 # соединения jail-ов
sockstat -4 -j nginx # кто слушает порты в jail-е
jexec php top # процессы внутри jail-а
8. Обслуживание
Код:
# Обновление базовой системы jail-ов
for j in nginx php db; do
freebsd-update -b /usr/local/jails/containers/$j \
--currently-running 14.5-RELEASE fetch install
done
# Обновление пакетов
pkg -j nginx upgrade -y
pkg -j php upgrade -y
pkg -r /usr/local/jails/containers/db upgrade -y
# Перезапуск отдельного jail-а
service jail restart php
# Проверка уязвимостей в пакетах
pkg -j nginx audit -F
pkg -j php audit -F
Частые ошибки
- Оставили
set skip on lo0. Jail-ы общаются друг с другом без ограничений, изоляции нет. - Правило pass для веба написано на
($ext_if). После rdr адрес назначения уже 10.10.0.10, поэтому правило не срабатывает, и сайт не открывается. - nat/rdr стоят ниже правил фильтрации. В FreeBSD 14 pf откажется загружать такой конфиг.
- Не скопировали resolv.conf в jail. pkg и PHP не могут резолвить домены.
- MySQL слушает 127.0.0.1. Внутри jail-а это превращается в его собственный IP, но явный
--bind-addressнадёжнее. - XenForo не может записать в data/ и internal_data/. Проверьте, что в jail php каталог смонтирован
rwи принадлежит UID 80.
Итог
- Из интернета доступен только nginx, а сам хост — только по SSH.
- PHP отвечает только nginx, MySQL — только PHP.
- База данных физически не может ничего отправить в интернет.
- Взлом одного сервиса не даёт доступа к остальным и к хосту.
- Каждый jail обновляется и перезапускается независимо.
Следующий шаг для тех, кому нужна ещё более жёсткая изоляция, — VNET-jail-ы. У каждого из них свой сетевой стек, а связь идёт через epair и bridge. Если тема интересна, пишите в обсуждении!
