FreeBSD 14.5 + pf: часть 3. Изолированные jail-ы для nginx, PHP-FPM и MySQL

Itnull

Admin
Регистрация
22.05.13
Сообщения
29.228
Реакции
13.526
Веб-сайт
itnull.me

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 = 2Jail видит только свои точки монтирования
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_jailsnginx и php выходят в интернет от имени внешнего IP (обновления, письма с форума, проверка обновлений XenForo)
rdr ... -> $nginx_ipЗапросы на 80/443 внешнего IP перенаправляются в jail nginx
block out ... from $jail_netdb не попадает под NAT, и его пакеты в интернет отбрасываются
nginx → php :9000Единственный разрешённый путь к PHP-FPM
php → db :3306Единственный разрешённый путь к MySQL
block log allnginx не достучится до 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

Частые ошибки​


  1. Оставили set skip on lo0. Jail-ы общаются друг с другом без ограничений, изоляции нет.
  2. Правило pass для веба написано на ($ext_if). После rdr адрес назначения уже 10.10.0.10, поэтому правило не срабатывает, и сайт не открывается.
  3. nat/rdr стоят ниже правил фильтрации. В FreeBSD 14 pf откажется загружать такой конфиг.
  4. Не скопировали resolv.conf в jail. pkg и PHP не могут резолвить домены.
  5. MySQL слушает 127.0.0.1. Внутри jail-а это превращается в его собственный IP, но явный --bind-address надёжнее.
  6. XenForo не может записать в data/ и internal_data/. Проверьте, что в jail php каталог смонтирован rw и принадлежит UID 80.

Итог​


  • Из интернета доступен только nginx, а сам хост — только по SSH.
  • PHP отвечает только nginx, MySQL — только PHP.
  • База данных физически не может ничего отправить в интернет.
  • Взлом одного сервиса не даёт доступа к остальным и к хосту.
  • Каждый jail обновляется и перезапускается независимо.



Следующий шаг для тех, кому нужна ещё более жёсткая изоляция, — VNET-jail-ы. У каждого из них свой сетевой стек, а связь идёт через epair и bridge. Если тема интересна, пишите в обсуждении!
 

Создайте аккаунт или войдите в систему, чтобы комментировать

Вы должны быть зарегистрированным, чтобы оставить комментарий

Создать аккаунт

Создайте аккаунт в нашем сообществе.

Войти

У вас уже есть аккаунт? Войдите здесь.

Назад
Сверху Снизу