Доска
Доска: Интернет

Jail на FreeBSD для мультисервисного VPS: сетевые стек, отдельные ZFS datasets и обновления без перезагрузки хоста

Один виртуальный сервер (VPS/VDS) нередко используется как «универсальная площадка» под несколько разнородных сервисов: фронтенд и reverse-proxy, прикладные компоненты, базу данных, очереди, DNS-резолвер, мониторинг. Если держать всё в одной системе, быстро возникают типовые проблемы: конфликт версий библиотек, конкуренция за порты и файлы конфигурации, усложнение обновлений, риск «уронить» соседние сервисы при банальном pkg upgrade.

FreeBSD jail на VPS: VNET-сеть, PF/NAT и отдельные ZFS datasets

В FreeBSD для подобных сценариев давно существует прикладной и относительно простой механизм – jail. Он позволяет разделить один хост на несколько изолированных окружений, сохранив единое ядро. Для аренды VPS/VDS такой подход особенно удобен: расход памяти и CPU обычно ниже, чем у нескольких виртуальных машин, а жизненный цикл сервисов становится управляемым на уровне отдельных jail.

Ниже описана практическая схема «мультисервисного VPS» на FreeBSD, где каждая jail получает: отдельный сетевой стек (VNET), свой ZFS dataset с квотами и снапшотами, а обновления выполняются без перезагрузки хоста (перезапускаются только затронутые jail и сервисы). Материал ориентирован на реальную эксплуатацию, а не на лабораторный пример.

Целевая архитектура и ограничения, о которых важно знать заранее

Опорные элементы схемы:

  • FreeBSD на хосте (как гостевая ОС на VPS/VDS) и включенные jails
  • ZFS для хранения rootfs jail и данных сервисов: отдельные datasets, квоты, снапшоты, rollback
  • VNET (VIMAGE) – отдельный сетевой стек на каждую jail, чтобы изолировать интерфейсы, маршрутизацию и сетевые настройки
  • pf на хосте – NAT и проброс портов на нужные jail (типично для VPS с одним публичным IPv4)

Ключевые ограничения:

  • Jails делят ядро с хостом. Это не «вторая виртуализация» и не гипервизор. При компрометации ядра затрагивается весь хост
  • Обновление ядра FreeBSD обычно требует перезагрузки. Тема «без перезагрузки хоста» относится к регулярным обновлениям userland и пакетов внутри jail и на хосте, где reboot не обязателен
  • В среде аренды VPS/VDS часто встречается фильтрация MAC-адресов на стороне провайдера. Поэтому внешние L2-мосты с «множеством MAC» могут не работать; более универсальна модель L3 (маршрутизация + NAT) внутри гостевой ОС

FreeBSD доступна у многих площадок аренды VDS/VPS. В качестве нейтрального примера приведем сервис VPS.house, где установка FreeBSD на виртуальный сервер обычно доступна как одна из опций – это упрощает старт именно с jail-хостом без дополнительной миграции ОС.

Сетевой стек для jail: shared-IP против VNET

Вариант 1. Jail в общем сетевом стеке (ip4.addr/ip6.addr)

Классическая модель jail использует общий сетевой стек хоста. IP-адреса задаются в конфигурации jail, а внутри окружения нет «своих» сетевых интерфейсов. Обычно это выглядит так:

  • на хосте поднимаются alias-адреса или используются несколько публичных IP
  • jail получает один или несколько IP через ip4.addr/ip6.addr
  • маршрутизация и firewall остаются общими на хосте

Плюсы: проще конфигурация, меньше «подвижных частей», меньше шансов упереться в особенности виртуальной сети провайдера.

Минусы: изоляция ограничена – нет отдельной таблицы маршрутизации, нет собственных интерфейсов; сложнее моделировать полноценные сетевые топологии (например, «внутренняя сеть приложений» плюс «DMZ»), а сетевые настройки отдельных окружений неизбежно завязаны на хост.

Вариант 2. VNET (VIMAGE): отдельный сетевой стек на каждую jail

VNET-jail получает приватный сетевой стек: внутри видны интерфейсы, можно назначать адреса, поднимать дополнительные интерфейсы, использовать свои маршруты и таблицы. Для мультисервисного VPS это даёт два практических эффекта:

  • Сетевые настройки сервисов (адреса, маршруты, MTU) локализованы в jail и меньше «протекают» в хост
  • Проще строить предсказуемую схему NAT/проброса портов: каждой jail выдаётся адрес во внутренней подсети, наружу публикуются только нужные порты

Плюсы: более чистая изоляция, проще «контейнерная» модель с внутренним сегментом, легче переносить jail на другой хост с похожей схемой.

Минусы: выше сложность (epair/bridge, prestart/poststop), требуется поддержка VIMAGE в ядре, а на некоторых площадках аренды VPS приходится учитывать сетевые ограничения (см. ниже).

Особенность VPS/VDS: почему внешний bridge не всегда работает

На «железном» сервере типична схема: epair интерфейсы jail подключаются к bridge0, а bridge соединяется с физическим интерфейсом, получая L2-выход в сеть. На VPS/VDS внешняя сторона часто контролируется гипервизором и провайдерской сетью:

  • может быть разрешен только один MAC-адрес на vNIC
  • может быть запрещён promisc-режим
  • возможна фильтрация исходящих кадров по MAC

Если попытаться «пробриджить наружу» множество epair-интерфейсов, наружу полетят кадры с разными MAC, и часть трафика может быть отброшена.

Для мультисервисного VPS более универсальна схема L3 внутри гостевой ОС: внутренний bridge используется только как «виртуальный коммутатор» между jail, а наружу трафик выходит через NAT хоста с единственным MAC публичного интерфейса.

Практическая настройка сети: внутренняя подсеть, pf/NAT и проброс портов

В примере ниже используется внутренняя сеть 10.0.0.0/24:

  • хост: 10.0.0.1 на bridge0
  • jail «front»: 10.0.0.10 (reverse-proxy, 80/443)
  • jail «app»: 10.0.0.20 (приложение)
  • jail «db»: 10.0.0.30 (PostgreSQL и только внутренняя доступность)

Проверка поддержки VNET

На современных релизах FreeBSD VNET часто доступен «из коробки», но проверка лишней не бывает:

# sysctl kern.features.vimage

Если значение отсутствует или равно 0, для VNET потребуется ядро с опцией VIMAGE (на VPS это означает плановую перезагрузку после смены ядра).

/etc/rc.conf на хосте: bridge и включение pf

Пример фрагмента /etc/rc.conf (хост):

cloned_interfaces=«bridge0»
ifconfig_bridge0=«inet 10.0.0.1/24 up»
gateway_enable=«YES»
pf_enable=«YES»
pf_rules=«/etc/pf.conf»
jail_enable=«YES»
jail_parallel_start=«YES»

Для IPv6 при наличии выделенного префикса обычно добавляется ipv6_gateway_enable=«YES». Если у провайдера только один IPv6-адрес /128, публичная адресация всех jail по IPv6 становится нетривиальной (придётся проектировать маршрутизацию или применять NAT66, что подходит не для всех случаев).

pf.conf: NAT наружу и rdr на нужные порты

Ниже – упрощённый пример, где наружный интерфейс обозначен как vtnet0. Важно подставить фактическое имя интерфейса (в VPS это часто vtnet0, xn0, em0 и т. п.).

/etc/pf.conf (пример):

ext_if=«vtnet0»
int_if=«bridge0»
jails_net=«10.0.0.0/24»
front_ip=«10.0.0.10»

set block-policy drop set skip on lo
block all
# Выход хоста и jail наружу
nat on $ext_if from $jails_net to any -> ($ext_if) pass out on $ext_if inet from ($ext_if) to any keep state pass in on $int_if from $jails_net to any keep state
# Публикация веба (80/443. на jail front rdr on $ext_if proto tcp to ($ext_if) port {80, 443} -> $front_ip pass in on $ext_if proto tcp to ($ext_if) port {80, 443} keep state

Такой подход хорошо ложится на модель «один публичный IPv4 на VPS»: сервисы в разных jail живут на внутренних адресах, наружу выставляются только нужные порты. Для мультисервисного хоста это снижает вероятность случайной экспозиции административных портов.

ZFS datasets для jail: изоляция, квоты, снапшоты и «быстрые» клоны

При использовании ZFS отдельный dataset под каждую jail решает сразу несколько эксплуатационных задач:

  • Квоты: один сервис не «съедает» диск целиком
  • Снапшоты: обновления и изменения конфигурации откатываются точечно, без влияния на соседей
  • Репликация: резервные копии удобнее делать на уровне ZFS send/receive

Базовые свойства datasets для jail-root

Часто применяются свойства, уменьшающие «шум» и повышающие предсказуемость:

  • compression=lz4 – компрессия без заметных потерь производительности в типовых сценариях
  • atime=off – снижает лишние записи
  • recordsize – имеет смысл настраивать отдельно для datasets с базами данных (см. ниже)

Создание корневого каталога под jail:

# zfs create -o mountpoint=/jails zroot/jails
# zfs set compression=lz4 atime=off zroot/jails

Шаблон (template) и ZFS clone: быстрый выпуск новых jail

В мультисервисной конфигурации удобно иметь «чистый» шаблон базовой системы и создавать jail как ZFS-clone. Клон занимает минимум места на старте и расходует диск по мере расхождения данных.

Подготовка шаблона:

# zfs create -o mountpoint=/jails/_templates zroot/jails/_templates
# zfs create -o mountpoint=/jails/_templates/14_0 zroot/jails/_templates/14_0

Далее в каталог шаблона устанавливается базовая система FreeBSD (см. следующий раздел), после чего делается снапшот:

# zfs snapshot zroot/jails/_templates/14_0@clean

Создание jail как клонов:

# zfs clone -o mountpoint=/jails/front zroot/jails/_templates/14_0@clean zroot/jails/front
# zfs clone -o mountpoint=/jails/app zroot/jails/_templates/14_0@clean zroot/jails/app
# zfs clone -o mountpoint=/jails/db zroot/jails/_templates/14_0@clean zroot/jails/db

Пример квот:

# zfs set quota=8G zroot/jails/front
# zfs set quota=12G zroot/jails/app
# zfs set quota=40G zroot/jails/db

Квоты стоит назначать исходя из профиля нагрузки и политики логирования. Частая ошибка на VPS – оставить datasets без ограничений и внезапно получить «disk full» на всём хосте из-за разросшихся логов одного сервиса.

Отдельные datasets под данные сервисов

Даже при «полном» rootfs на один dataset полезно выносить данные с особыми требованиями в отдельные datasets. Пример – PostgreSQL: у него страничная организация данных, и ZFS recordsize может влиять на read-amplification.

Пример выделения dataset под PostgreSQL с меньшим recordsize:

# zfs create -o mountpoint=/jails/db/var/db/postgres -o recordsize=8K -o atime=off zroot/jails/db_pgdata

Точное значение recordsize зависит от конкретной СУБД и профиля запросов. На практике целесообразно тестировать, а не переносить настройки «вслепую»: универсальной цифры не существует.

Создание jail: установка base, конфигурация jail.conf и сеть внутри окружения

Установка базовой системы в шаблон

Для установки base в каталог jail часто используется штатный установщик:

# bsdinstall jail /jails/_templates/14_0

В типовом сценарии выбирается только base.txz (без kernel.txz). После создания клона базовая настройка выполняется в самом clone-дataset: hostname, сеть, резолвер, включение/отключение сервисов по умолчанию.

Практичный минимум внутри jail:

  • корректный /etc/resolv.conf (часто достаточно копии с хоста)
  • настройка сетевого интерфейса и default route
  • отключение лишних демонов (например, sendmail), если они не нужны
  • инициализация pkg: pkg bootstrap -y

devfs rules и базовые ограничения

Чтобы jail не получала лишние устройства, обычно задаётся отдельный devfs ruleset. Пример (номер 5 условный):

/etc/devfs.rules (фрагмент):

[devfsrules_jail=5]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add path «null» unhide
add path «zero» unhide
add path «random» unhide
add path «urandom» unhide
add path «tty» unhide
add path «ptmx» unhide

Для отдельных сервисов могут потребоваться дополнительные устройства (например, tun для VPN). Такие исключения лучше добавлять точечно, только в нужные jail, а не расширять правила глобально.

jail.conf: общие параметры и VNET-jail с epair

Удобная практика – вынести общие параметры в начало /etc/jail.conf, а конкретные jail описывать ниже или в отдельных файлах, подключаемых include.

/etc/jail.conf (упрощённый пример):

# Общие настройки
exec.clean;
mount.devfs;
devfs_ruleset = 5;
path = «/jails/$name»;
exec.start = «/bin/sh /etc/rc»;
exec.stop = «/bin/sh /etc/rc.shutdown»;
persist;

# Jail front (VNET) front { host.hostname = «front.local»; vnet; vnet.interface = «epair10b»; exec.prestart = «ifconfig epair create»; }

Пример выше специально сокращён: реальная эксплуатация требует корректного управления именами epair и очистки интерфейсов при остановке jail. На практике встречаются два устойчивых подхода:

  • Статические epair-юниты: заранее назначается «номер» каждой jail (epair10, epair11…), а в prestart/poststop выполняются create/destroy и добавление в bridge0
  • Скрипт-обвязка: единый скрипт создаёт epair, подключает к bridge0, логирует результат и корректно чистит ресурсы при stop (удобно, когда jail десятки)

Ниже приведён вариант со статическим юнитом, который проще повторить на одном VPS.

front jail (VNET + epair10):

front {
host.hostname = «front.local»;
path = «/jails/front»;
vnet;
vnet.interface = «epair10b»;
exec.prestart = «ifconfig epair10 create»;
exec.prestart += «ifconfig epair10a up»;
exec.prestart += «ifconfig bridge0 addm epair10a up»;
exec.poststop = «ifconfig bridge0 deletem epair10a»;
exec.poststop += «ifconfig epair10a destroy»;
}

Аналогично описываются app (например, epair11. и db (epair12).

Сеть внутри jail: адрес и маршрут

Внутри jail адрес назначается уже «по месту», в /etc/rc.conf соответствующего окружения. Пример для front jail:

/jails/front/etc/rc.conf (фрагмент):

hostname=«front.local»
ifconfig_epair10b=«inet 10.0.0.10/24»
defaultrouter=«10.0.0.1»

После этого jail получает связность с другими jail по внутренней сети и доступ наружу через NAT на хосте.

Обновления без перезагрузки хоста: схема, которая реально экономит простой

В мультисервисном VPS основная боль обновлений – «цепная реакция»: обновление одного компонента вынуждает останавливать всё, потому что зависимости и библиотеки общие. Jails позволяют локализовать обновление:

  • обновляется base и пакеты только в нужной jail
  • перезапускается только соответствующий сервис или сама jail
  • соседние окружения продолжают работать на своих версиях библиотек, пока не наступит их окно обслуживания

Обновление базовой системы jail через freebsd-update -b

Для base-обновлений FreeBSD применяется freebsd-update. Важно помнить, что jail не требует обновления ядра, поэтому конфигурацию для jail часто делают отдельной (чтобы не тянуть kernel-компоненты).

Практичный вариант – отдельный конфиг для jail:

# cp /etc/freebsd-update.conf /etc/freebsd-update-jail.conf

В файле /etc/freebsd-update-jail.conf обычно корректируют Components (оставляют только world) и рабочий каталог (чтобы несколько параллельных обновлений не конфликтовали). Затем обновление запускается с указанием базового каталога jail:

Пример для front jail:

# freebsd-update -f /etc/freebsd-update-jail.conf -b /jails/front fetch
# freebsd-update -f /etc/freebsd-update-jail.conf -b /jails/front install

Если обновление затрагивает библиотеки, которые уже загружены в память процессами, потребуется перезапуск сервисов в jail (а иногда проще перезапустить всю jail). При этом хост продолжает работать, и другие jail не затрагиваются.

Обновление пакетов внутри jail через pkg

Пакеты обновляются стандартно. Варианты запуска:

  • через jexec (выполнение команды внутри jail)
  • через pkg -j (если jail запущена и корректно определяется)

Пример через jexec:

# jexec front pkg update -f
# jexec front pkg upgrade -y

Для аккуратного обслуживания полезно совмещать обновление пакетов со списком изменённых сервисов и планом перезапусков. Например, после обновления OpenSSL перезапускаются только сервисы, которые линкуются с обновлённой библиотекой (nginx, haproxy, почтовые демоны и т. п.), а не всё окружение целиком.

Снапшот перед обновлением и быстрый откат (rollback)

Сочетание jail + ZFS часто используется как страховка от «неудачного апдейта».

Шаг 1. Снапшот перед изменениями:

# zfs snapshot zroot/jails/front@before-update-2026-01-14

Шаг 2. Обновление base и пакетов (см. выше).

Шаг 3. Если что-то пошло не так – откат:

# service jail stop front
# zfs rollback -r zroot/jails/front@before-update-2026-01-14
# service jail start front

Откат затрагивает только одну jail и, как правило, выполняется за минуты. Это заметно снижает риск затяжного простоя на одном виртуальном сервере.

Что всё-таки может потребовать reboot

Обновления «без перезагрузки хоста» реалистичны в следующих случаях:

  • обновление пакетов в jail (pkg upgrade)
  • обновление base userland в jail (freebsd-update с компонентом world)
  • обновление userland на хосте, если не меняется ядро (часто достаточно перезапуска отдельных демонов)

Перезагрузка хоста обычно требуется при обновлениях ядра и некоторых низкоуровневых компонентов. Для контроля полезно сверять версии:

# freebsd-version -ku

Если версии ядра и userland разъехались после установки обновлений ядра, планируется окно на reboot. В контексте VPS/VDS это неизбежная часть жизненного цикла, но jails позволяют существенно сократить количество «вынужденных» перезагрузок ради прикладных обновлений.

Операционная рутина: обслуживание мультисервисного VPS на jail

В эксплуатации обычно помогает простой регламент, который хорошо масштабируется от 2-3 jail до десятков:

  1. 1) Еженедельно: снапшоты datasets (перед обновлениями и по расписанию), проверка использования квот
  2. 2) Регулярно: обновление пакетов внутри jail с перезапуском только затронутых сервисов
  3. 3) По advisories: обновление base world внутри jail; при необходимости – обновление userland хоста
  4. 4) Планово: обновление ядра хоста и reboot (реже, но неизбежно)

Для резервного копирования удобно применять ZFS send/receive на внешний сервер. На VPS это особенно актуально: отказоустойчивость «одного виртуального сервера» всегда ограничена, а перенос/восстановление из ZFS-реплики заметно проще, чем сбор «ручных» архивов.

Частые проблемы и чек-лист диагностики

  • Jail не видит сеть: проверить, что epair* создан, добавлен в bridge0, а в jail назначен адрес на правильный интерфейс (epairXXb)
  • NAT не работает: убедиться, что включен gateway_enable, pf поднят, и правило NAT соответствует внешнему интерфейсу. Проверить отсутствие конфликтующих правил
  • Не открываются порты снаружи: кроме rdr нужно «pass in» на внешний интерфейс. Также стоит проверить, что сервис слушает на внутреннем IP jail, а не только на loopback
  • Обновление freebsd-update ломается в jail: часто причина в конфигурации компонентов (попытка обновлять kernel) или в конфликте WorkDir. Практика – отдельный конфиг и отдельный WorkDir для jail
  • Внешний bridge с public-интерфейсом «странно» себя ведёт: типичный признак MAC-фильтрации в виртуальной сети. В таком случае безопаснее перейти на L3-модель (внутренний bridge без подключения к внешнему интерфейсу + NAT)

Войти и комментировать [ Вход | Регистрация ]