Один взломанный сайт положил весь сервер: LXC-контейнеры для веб-хостинга

Задача клиента: один взлом — и 200 сайтов лежат

В декабре 2025 года к нам в АйТи Фреш обратился «ВебХаус» — хостинг-провайдер из Тулы, обслуживающий около 200 клиентских сайтов на shared-хостинге. Проблема была острой: один взломанный WordPress-сайт с устаревшим плагином стал точкой входа для атаки, которая положила весь сервер целиком.

Злоумышленник через уязвимость в плагине WP File Manager получил web-shell, эскалировал привилегии через уязвимость в ядре и запустил криптомайнер, который загрузил все 32 ядра CPU на 100%. Все 200 сайтов на сервере стали недоступны на 14 часов, пока администратор вручную вычислял источник проблемы.

«Один клиент не обновлял WordPress три года. Из-за него мы потеряли выручку за сутки и получили шквал жалоб от остальных 199 клиентов. Мы поняли, что shared-хостинг в классическом виде — это русская рулетка» — основатель «ВебХаус».

Аудит текущей архитектуры shared-хостинга

Наши инженеры подключились к серверу и зафиксировали архитектуру:

  • 1 физический сервер: Intel Xeon E-2288G (8C/16T), 64 ГБ RAM, 2×1 ТБ NVMe в RAID 1
  • ISPmanager как панель управления хостингом
  • Apache + PHP-FPM (5 версий PHP: 7.4, 8.0, 8.1, 8.2, 8.3)
  • MySQL 8.0 — общий инстанс для всех клиентов
  • Изоляция: только open_basedir и разделение по uid/gid
# Проверяем, как изолированы клиенты
ls -la /var/www/clients/ | head -10
# drwxr-x--- 12 web1  client1 4096 Dec 10 web1/
# drwxr-x--- 15 web2  client2 4096 Dec 10 web2/
# drwxr-x--- 8  web3  client3 4096 Dec 10 web3/
# ... всего 200 директорий

# Проверяем изоляцию PHP
grep open_basedir /etc/php/8.2/fpm/pool.d/web47.conf
# php_admin_value[open_basedir] = /var/www/clients/web47:/tmp

# Но PHP — не единственный вектор!
# Пользователь web47 может видеть процессы других пользователей
ps aux | grep php-fpm | wc -l
# 312 — все PHP-FPM процессы видны всем

# И читать /proc
ls /proc/*/cmdline 2>/dev/null | wc -l
# 847 — полный доступ к /proc

Shared-хостинг полагался на open_basedir и UNIX-права для изоляции. Но open_basedir — это ограничение PHP, а не ядра. Любая уязвимость, дающая shell-доступ, полностью обходит эту защиту. Фактической изоляции между клиентами не было.

Выбор технологии: LXC vs Docker vs KVM

Перед миграцией нужно было выбрать технологию изоляции. 200 сайтов — это серьёзная плотность, поэтому overhead имеет значение:

ТехнологияИзоляцияOverheadПлотностьУправление
KVM (полные ВМ)Полная (своё ядро)Высокий (512+ МБ/ВМ)~30–40 ВМ на серверProxmox GUI
DockerХорошая (namespaces)МинимальныйВысокаяНе подходит для «классических» сайтов
LXCХорошая (namespaces + AppArmor)Минимальный (20–50 МБ)100–200+ контейнеровProxmox GUI + CLI

LXC выиграл — это полноценная изоляция на уровне ядра (namespaces, cgroups, AppArmor), но с overhead в 10–20 раз меньше, чем у KVM. Каждый контейнер — это полноценная «виртуальная машина» с точки зрения пользователя (свой init, свой /etc, свои пользователи), но без накладных расходов на эмуляцию оборудования. Docker не подходил: для хостинга нужна полная ОС в контейнере, а не один процесс.

Установка Proxmox и подготовка хранилища ZFS

Мы закупили для «ВебХаус» новый сервер (AMD EPYC 7443P, 128 ГБ ECC RAM, 4×2 ТБ NVMe) и установили Proxmox VE 8.1 — гипервизор с нативной поддержкой LXC и KVM, веб-интерфейсом и API.

Установка Proxmox VE и настройка ZFS

Proxmox устанавливается как полноценная ОС (на базе Debian 12). Для хранилища мы выбрали ZFS — файловую систему с встроенными снапшотами, компрессией и контролем целостности:

# После установки Proxmox — создаём ZFS pool
# 4 диска NVMe в RAID-Z1 (аналог RAID5)
zpool create -f -o ashift=12 \
  -O compression=lz4 \
  -O atime=off \
  -O xattr=sa \
  -O dnodesize=auto \
  tank raidz1 \
  /dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1

# Проверяем пул
zpool status tank
# NAME                     STATE     READ WRITE CKSUM
# tank                     ONLINE       0     0     0
#   raidz1-0               ONLINE       0     0     0
#     nvme0n1               ONLINE       0     0     0
#     nvme1n1               ONLINE       0     0     0
#     nvme2n1               ONLINE       0     0     0
#     nvme3n1               ONLINE       0     0     0

# Общая ёмкость: ~5.5 ТБ (usable)
zpool list tank
# NAME  SIZE  ALLOC  FREE  CKPOINT  EXPANDSZ  FRAG  CAP  HEALTH
# tank  7.27T  124K  7.27T  -        -         0%    0%   ONLINE

# Создаём отдельные datasets для контейнеров
zfs create tank/containers
zfs create tank/backups
zfs create tank/templates

# Настраиваем квоты
zfs set quota=4T tank/containers
zfs set quota=1.5T tank/backups

# Добавляем ZFS storage в Proxmox
pvesm add zfspool local-zfs -pool tank/containers -content rootdir,images
pvesm add dir local-backups -path /tank/backups -content backup,vztmpl

ZFS даёт нам ключевое преимущество: datasets с квотами для каждого контейнера. Если клиент заполнит свой диск, это не повлияет на остальных. Плюс мгновенные снапшоты для бекапов.

Загрузка шаблонов и создание базового контейнера

Proxmox поддерживает готовые шаблоны LXC. Мы скачали шаблон Ubuntu 22.04 и создали «золотой образ» — базовый контейнер со всем необходимым для хостинга:

# Скачиваем шаблон
pveam update
pveam download local debian-12-standard_12.2-1_amd64.tar.zst

# Создаём базовый контейнер (будет шаблоном)
pct create 9000 local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst \
  --hostname template-web \
  --memory 1024 \
  --swap 512 \
  --cores 2 \
  --rootfs local-zfs:10 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1 \
  --features nesting=1 \
  --ostype debian \
  --password

# Запускаем и заходим
pct start 9000
pct enter 9000

# Внутри контейнера устанавливаем стек хостинга
apt update && apt upgrade -y
apt install -y nginx php8.2-fpm php8.2-mysql php8.2-gd php8.2-xml \
  php8.2-mbstring php8.2-curl php8.2-zip php8.2-intl php8.2-bcmath \
  mariadb-client curl wget unzip cron logrotate

# Настраиваем nginx + PHP-FPM для одного сайта
# (стандартная конфигурация, специфичная для каждого клиента)

# Останавливаем и конвертируем в шаблон
pct stop 9000
pct template 9000

# Теперь можем клонировать шаблон для каждого клиента
pct clone 9000 101 --hostname client-site-01 --full

Конвертация в шаблон делает контейнер read-only. Клонирование из шаблона занимает 3–5 секунд благодаря ZFS (copy-on-write). Это позволило нам быстро развернуть 200 контейнеров.

Конфигурация LXC: ресурсы, сеть, безопасность

Каждый контейнер клиента получал индивидуальные лимиты ресурсов, изолированную сеть и профиль безопасности. Это ключевое отличие от shared-хостинга — теперь один клиент физически не может повлиять на другого.

Лимиты CPU, RAM и дискового пространства

Мы разработали три тарифных плана с разными лимитами:

# Конфигурация контейнера — /etc/pve/lxc/101.conf

# Тариф "Стартовый"
arch: amd64
cores: 1
memory: 512
swap: 256
hostname: client-site-01
unprivileged: 1
onboot: 1
features: nesting=1

# Storage с квотой 5 ГБ
rootfs: local-zfs:subvol-101-disk-0,size=5G

# Сеть
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:01:01:01,ip=10.10.1.101/24,gw=10.10.1.1

# CPU лимиты (cgroups v2)
lxc.cgroup2.cpu.max: 100000 100000
# Ограничение I/O
lxc.cgroup2.io.max: 253:0 rbps=52428800 wbps=26214400

# --------------------------------------------------
# Тариф "Бизнес" (пример для контейнера 150)
cores: 2
memory: 2048
swap: 1024
rootfs: local-zfs:subvol-150-disk-0,size=20G

# --------------------------------------------------
# Тариф "Премиум" (пример для контейнера 180)
cores: 4
memory: 4096
swap: 2048
rootfs: local-zfs:subvol-180-disk-0,size=50G

Квота на ZFS-dataset гарантирует, что клиент не может занять больше выделенного пространства. Параметр lxc.cgroup2.cpu.max ограничивает использование CPU — даже если клиент запустит криптомайнер, он получит не более выделенного количества ядер.

# Применение лимитов к работающему контейнеру
pct set 101 --memory 1024 --cores 2 --swap 512

# Изменение квоты диска
zfs set quota=10G tank/containers/subvol-101-disk-0

# Мониторинг ресурсов контейнера
pct status 101 --verbose
# Status: running
# CPU: 0.03 (1 cores)
# Memory: 287.42 MB / 512.00 MB
# Swap: 0 B / 256.00 MB
# Disk: 1.82 GB / 5.00 GB

Сетевая изоляция: veth, bridge и firewall

Каждый контейнер получал свой виртуальный интерфейс (veth pair), подключённый к мосту. Мы настроили несколько уровней сетевой изоляции:

# Структура сети:
# vmbr0 — публичный мост (внешние IP, NAT)
# vmbr1 — приватный мост (межконтейнерная связь, DB)

# Создаём приватный мост для базы данных
auto vmbr1
iface vmbr1 inet static
    address 10.10.2.1/24
    bridge-ports none
    bridge-stp off
    bridge-fd 0
    post-up echo 1 > /proc/sys/net/ipv4/ip_forward

# Firewall-правила на уровне Proxmox
# /etc/pve/firewall/cluster.fw
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT

[RULES]
# Разрешаем HTTP/HTTPS извне
IN ACCEPT -p tcp -dport 80
IN ACCEPT -p tcp -dport 443

# Запрещаем связь между контейнерами
# Каждый контейнер видит только свой IP и сервер БД

# Firewall для каждого контейнера
# /etc/pve/firewall/101.fw
[OPTIONS]
enable: 1
ipfilter: 1

[RULES]
IN ACCEPT -source 10.10.2.200 -p tcp -dport 3306 -log nolog
# Контейнер 101 может подключиться только к серверу БД

Параметр ipfilter: 1 предотвращает IP-spoofing — контейнер не может подделать свой IP-адрес. Межконтейнерный трафик блокируется, каждый сайт изолирован от соседей на сетевом уровне.

Безопасность: AppArmor, seccomp и unprivileged containers

Три уровня защиты делают LXC-контейнеры безопасной средой для ненадёжного кода клиентов:

# 1. Unprivileged containers — UID mapping
# Контейнер работает от непривилегированного пользователя хоста
# UID 0 (root) в контейнере = UID 100000 на хосте
grep 'lxc.idmap' /etc/pve/lxc/101.conf
# lxc.idmap: u 0 100000 65536
# lxc.idmap: g 0 100000 65536

# Даже если атакующий получит root в контейнере,
# на хосте он будет пользователем 100000 — без привилегий

# 2. AppArmor профиль
# Proxmox применяет AppArmor-профиль по умолчанию
aa-status | grep lxc
# /usr/bin/lxc-start (enforce)
# lxc-container-default-cgns (enforce)

# Профиль запрещает:
# - mount файловых систем (кроме разрешённых)
# - доступ к /proc/sys (запись)
# - загрузку модулей ядра
# - изменение системного времени

# 3. Seccomp фильтр — блокируем опасные системные вызовы
cat /usr/share/lxc/config/common.seccomp
# 2
# blacklist
# [all]
# kexec_load errno 1
# open_by_handle_at errno 1
# init_module errno 1
# finit_module errno 1
# delete_module errno 1
# ...

# Кастомный seccomp-профиль для хостинговых контейнеров
cat /etc/pve/lxc/seccomp-hosting.conf
# 2
# blacklist
# [all]
# kexec_load errno 1
# open_by_handle_at errno 1
# init_module errno 1
# finit_module errno 1
# delete_module errno 1
# pivot_root errno 1
# swapon errno 1
# swapoff errno 1
# syslog errno 1
# mount errno 1 [1,!=tmpfs]
# umount2 errno 1

Unprivileged containers — самый важный элемент. Root внутри контейнера не имеет никаких привилегий на хосте. Даже при обнаружении уязвимости в ядре (container escape), атакующий окажется под UID 100000 — обычным пользователем без прав.

Миграция 200 сайтов и автоматизация

Миграция 200 сайтов — масштабная операция. Мы разработали скрипт автоматизации, который создавал контейнер, переносил файлы и базу данных, настраивал nginx и DNS.

Скрипт автоматической миграции

#!/bin/bash
# migrate-site.sh — Миграция одного сайта в LXC-контейнер
# Использование: ./migrate-site.sh client_name domain ctid

CLIENT=$1
DOMAIN=$2
CTID=$3
TEMPLATE_ID=9000
STORAGE="local-zfs"
BRIDGE="vmbr0"
GATEWAY="10.10.1.1"
IP="10.10.1.${CTID}"
DB_SERVER="10.10.2.200"

echo "[*] Создаём контейнер ${CTID} для ${DOMAIN}..."
pct clone ${TEMPLATE_ID} ${CTID} \
  --hostname "${CLIENT}" \
  --storage ${STORAGE} \
  --full

# Настраиваем ресурсы (тариф "Стартовый")
pct set ${CTID} \
  --memory 512 \
  --swap 256 \
  --cores 1 \
  --net0 name=eth0,bridge=${BRIDGE},ip=${IP}/24,gw=${GATEWAY} \
  --rootfs ${STORAGE}:5

pct start ${CTID}
sleep 5

echo "[*] Копируем файлы сайта..."
rsync -avz /var/www/clients/${CLIENT}/ \
  /tank/containers/subvol-${CTID}-disk-0/var/www/html/

echo "[*] Экспортируем базу данных..."
mysqldump -u root -p"${MYSQL_PASS}" ${CLIENT}_db | \
  pct exec ${CTID} -- mysql -u root ${CLIENT}_db

echo "[*] Настраиваем nginx..."
pct exec ${CTID} -- bash -c "cat > /etc/nginx/sites-available/${DOMAIN} << 'NGINX'
server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};
    root /var/www/html;
    index index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        include fastcgi_params;
    }
}
NGINX"

pct exec ${CTID} -- ln -s /etc/nginx/sites-available/${DOMAIN} /etc/nginx/sites-enabled/
pct exec ${CTID} -- nginx -t && pct exec ${CTID} -- systemctl reload nginx

echo "[+] Контейнер ${CTID} для ${DOMAIN} готов: ${IP}"

Скрипт мигрировал один сайт за 2–3 минуты. За два вечера (с 23:00 до 05:00) мы перенесли все 200 сайтов, параллельно запуская по 10 миграций. DNS переключался последними — с TTL 300 секунд даунтайм составил не более 5 минут на сайт.

Бекапы и восстановление через Proxmox

Proxmox предоставляет встроенную систему бекапов с поддержкой ZFS-снапшотов:

# Бекап одного контейнера (snapshot mode — мгновенный)
vzdump 101 --storage local-backups --mode snapshot --compress zstd

# Расписание бекапов через Proxmox
# Ежедневно в 03:00, хранить 7 последних
cat /etc/pve/jobs.cfg
# vzdump: backup-daily
#     enabled 1
#     schedule 0 3 * * *
#     storage local-backups
#     mode snapshot
#     compress zstd
#     prune-backups keep-daily=7,keep-weekly=4,keep-monthly=3
#     all 1

# Восстановление контейнера из бекапа
pct restore 101 /tank/backups/dump/vzdump-lxc-101-2026_01_15-03_00_01.tar.zst \
  --storage local-zfs

# ZFS-снапшоты для мгновенного отката
zfs snapshot tank/containers/subvol-101-disk-0@before-update
# Откат:
zfs rollback tank/containers/subvol-101-disk-0@before-update

# Размер снапшота — только дельта изменений
zfs list -t snapshot -r tank/containers/subvol-101-disk-0
# NAME                                              USED  AVAIL  REFER
# ...@before-update                                 12.4M  -     1.82G

Snapshot-бекап всех 200 контейнеров занимает 15 минут без остановки сервисов. Для сравнения, на старом сервере бекап через tar занимал 4 часа и требовал maintenance window.

Мониторинг и обслуживание контейнеров

200 контейнеров — это 200 отдельных серверов, каждый из которых нужно обновлять, мониторить и обслуживать. Мы автоматизировали рутинные операции через скрипты и Proxmox API.

Массовое обновление контейнеров

Обслуживание 200 контейнеров вручную невозможно. Мы написали набор скриптов для массовых операций — обновления, перезапуска и диагностики:

#!/bin/bash
# update-all-containers.sh — обновление пакетов во всех контейнерах

# Получаем список работающих контейнеров
CONTAINERS=$(pct list | awk '/running/ {print $1}')
TOTAL=$(echo $CONTAINERS | wc -w)
COUNT=0

for CTID in $CONTAINERS; do
    COUNT=$((COUNT + 1))
    echo "[${COUNT}/${TOTAL}] Обновляем контейнер ${CTID}..."
    pct exec ${CTID} -- bash -c "apt update -qq && apt upgrade -y -qq" &
    
    # Не более 10 параллельных обновлений
    while [ $(jobs -r | wc -l) -ge 10 ]; do
        sleep 1
    done
done

wait
echo "[+] Все ${TOTAL} контейнеров обновлены"

# Результат: обновление 200 контейнеров за ~20 минут
# (вместо ручного обновления одного за другим)

# Скрипт проверки состояния всех контейнеров
# health-check.sh
for CTID in $(pct list | awk '/running/ {print $1}'); do
    HOSTNAME=$(pct config $CTID | grep hostname | awk '{print $2}')
    MEM=$(pct status $CTID | grep mem | awk '{print $2}')
    DISK=$(pct exec $CTID -- df -h / | awk 'NR==2{print $5}')
    NGINX=$(pct exec $CTID -- systemctl is-active nginx 2>/dev/null)
    PHP=$(pct exec $CTID -- systemctl is-active php8.2-fpm 2>/dev/null)
    echo "${CTID} ${HOSTNAME}: mem=${MEM}, disk=${DISK}, nginx=${NGINX}, php=${PHP}"
done

Скрипт health-check выполняется ежедневно через cron и отправляет отчёт администратору. Контейнеры с использованием диска выше 85% или упавшими сервисами попадают в алерт.

Мониторинг через Proxmox API и Prometheus

Proxmox API позволяет получать метрики всех контейнеров. Мы настроили Prometheus + Grafana для визуализации:

# Prometheus exporter для Proxmox
# pve-exporter — собирает метрики через API
pip3 install prometheus-pve-exporter

# Конфигурация /etc/pve-exporter/pve.yml
default:
    user: monitoring@pve
    password: MonitorPass2026!
    verify_ssl: false

# Запуск
pve_exporter --config.file /etc/pve-exporter/pve.yml

# Prometheus config
# /etc/prometheus/prometheus.yml
scrape_configs:
  - job_name: 'proxmox'
    static_configs:
      - targets: ['10.10.1.1:9221']
    metrics_path: /pve
    params:
      module: [default]
      target: ['10.10.1.1']

# Полезные метрики для мониторинга хостинга:
# pve_lxc_cpu_usage_ratio — CPU контейнера
# pve_lxc_memory_usage_bytes — RAM контейнера
# pve_lxc_disk_usage_bytes — Диск контейнера
# pve_lxc_network_transmit_bytes — Исходящий трафик

# Alertmanager — оповещение при превышении 90% ресурсов
groups:
  - name: lxc-alerts
    rules:
      - alert: LXC_HighMemory
        expr: pve_lxc_memory_usage_bytes / pve_lxc_memory_size_bytes > 0.9
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Контейнер {{ $labels.id }} использует >90% RAM"

Результаты внедрения: цифры и выводы

Миграция с shared-хостинга на LXC-контейнеры заняла 3 недели, включая тестирование и постепенный перенос клиентов.

Ключевые метрики

МетрикаShared-хостинг (до)LXC-контейнеры (после)
Изоляция между клиентамиopen_basedir (обходится)Ядро: namespaces, cgroups, AppArmor
Последствия взлома одного сайтаПадение всего сервераТолько один контейнер
Время развёртывания нового клиента15 минут (ручная настройка)3 секунды (клон из шаблона)
Время полного бекапа4 часа (tar)15 минут (ZFS snapshot)
Overhead памяти на 200 сайтов~48 ГБ (один монолит)~32 ГБ (200 контейнеров по 150 МБ avg)
Инцидентов безопасности (за 3 месяца)3 взлома → общий downtime2 взлома → 0 влияние на соседей
Время восстановления после взлома4–14 часов30 секунд (ZFS rollback)

Уроки и рекомендации

Опыт «ВебХаус» подтвердил несколько важных принципов:

  • Unprivileged LXC — обязательно. Privileged-контейнеры сводят на нет всю безопасность. Все 200 контейнеров работают в unprivileged-режиме.
  • ZFS — идеальная пара для LXC. Снапшоты, квоты, компрессия, контроль целостности — всё это бесплатно и мгновенно.
  • Автоматизация — ключ к масштабу. 200 контейнеров невозможно обслуживать вручную. Скрипты обновления, мониторинг и алерты обязательны.
  • Сетевая изоляция не менее важна, чем FS-изоляция. Без firewall между контейнерами взломанный сайт может атаковать соседей по сети.

LXC-контейнеры в Proxmox — оптимальное решение для хостинг-провайдеров: изоляция уровня виртуальных машин при overhead уровня обычных процессов.

Часто задаваемые вопросы

LXC-контейнеры разделяют ядро с хостом, поэтому теоретически уязвимость в ядре может привести к «побегу» из контейнера. Однако unprivileged-контейнеры с AppArmor и seccomp значительно снижают этот риск. На практике для веб-хостинга LXC обеспечивает достаточный уровень изоляции, а overhead в 10–20 раз меньше, чем у KVM.

Зависит от ресурсов сервера и нагрузки контейнеров. На сервере с 128 ГБ RAM и 24 ядрами мы запускаем 200+ контейнеров. Минимальный контейнер потребляет 20–50 МБ RAM. Для типичного сайта на WordPress достаточно 256–512 МБ RAM и 0.5 CPU core.

Да. Proxmox поддерживает online-миграцию контейнеров между нодами кластера (при общем хранилище) и offline-миграцию через vzdump/restore. Для ZFS-хранилища можно использовать zfs send/receive для инкрементальной репликации. Миграция контейнера 5 ГБ занимает 30–60 секунд.

Docker ориентирован на запуск отдельных приложений (один процесс на контейнер), а LXC — на запуск полноценной ОС. Для веб-хостинга нужны nginx, PHP-FPM, cron, SSH и другие сервисы в одном окружении — это задача LXC. Docker подойдёт для микросервисной архитектуры, но не для классического хостинга.

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#LXC контейнеры Proxmox#LXC vs Docker отличия#Proxmox LXC templates#unprivileged containers безопасность#LXC resource limits CPU RAM#LXC networking veth bridge#ZFS datasets контейнеры#AppArmor seccomp LXC